chore: fixing issue with multi signin providers
Co-authored-by: Johan Haals <johan.haals@gmail.com> Co-authored-by: Fredrik Adelöw <freben@gmail.com> Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
BackstageUserIdentity,
|
||||
IdentityApi,
|
||||
ProfileInfo,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export class IdentityApiSignOutProxy implements IdentityApi {
|
||||
private constructor(
|
||||
private readonly config: {
|
||||
identityApi: IdentityApi;
|
||||
signOut: IdentityApi['signOut'];
|
||||
},
|
||||
) {}
|
||||
|
||||
static from(config: {
|
||||
identityApi: IdentityApi;
|
||||
signOut: IdentityApi['signOut'];
|
||||
}): IdentityApi {
|
||||
return new IdentityApiSignOutProxy(config);
|
||||
}
|
||||
|
||||
getUserId(): string {
|
||||
return this.config.identityApi.getUserId();
|
||||
}
|
||||
|
||||
getIdToken(): Promise<string | undefined> {
|
||||
return this.config.identityApi.getIdToken();
|
||||
}
|
||||
|
||||
getProfile(): ProfileInfo {
|
||||
return this.config.identityApi.getProfile();
|
||||
}
|
||||
|
||||
getProfileInfo(): Promise<ProfileInfo> {
|
||||
return this.config.identityApi.getProfileInfo();
|
||||
}
|
||||
|
||||
getBackstageIdentity(): Promise<BackstageUserIdentity> {
|
||||
return this.config.identityApi.getBackstageIdentity();
|
||||
}
|
||||
|
||||
getCredentials(): Promise<{ token?: string | undefined }> {
|
||||
return this.config.identityApi.getCredentials();
|
||||
}
|
||||
|
||||
signOut(): Promise<void> {
|
||||
return this.config.signOut();
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,6 @@ export const SingleSignInPage = ({
|
||||
}
|
||||
|
||||
const profile = await authApi.getProfile();
|
||||
|
||||
onSignInSuccess(
|
||||
UserIdentity.from({
|
||||
identity: identityResponse.identity,
|
||||
@@ -149,6 +148,7 @@ export const SingleSignInPage = ({
|
||||
setShowLoginPage(true);
|
||||
}
|
||||
};
|
||||
|
||||
useMount(() => login({ checkExisting: true }));
|
||||
|
||||
return showLoginPage ? (
|
||||
|
||||
@@ -26,7 +26,7 @@ import isEmpty from 'lodash/isEmpty';
|
||||
import { InfoCard } from '../InfoCard/InfoCard';
|
||||
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
|
||||
import { GridItem } from './styles';
|
||||
import { LegacyUserIdentity } from './LegacyUserIdentity';
|
||||
import { UserIdentity } from './UserIdentity';
|
||||
|
||||
// accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3)
|
||||
const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i;
|
||||
@@ -69,14 +69,15 @@ const Component: ProviderComponent = ({ onSignInSuccess }) => {
|
||||
|
||||
const { errors } = formState;
|
||||
|
||||
const handleResult = ({ userId, idToken }: Data) => {
|
||||
const handleResult = ({ userId }: Data) => {
|
||||
onSignInSuccess(
|
||||
LegacyUserIdentity.fromResult({
|
||||
userId,
|
||||
profile: {
|
||||
email: `${userId}@example.com`,
|
||||
UserIdentity.fromLegacy({
|
||||
result: {
|
||||
userId,
|
||||
profile: {
|
||||
email: `${userId}@example.com`,
|
||||
},
|
||||
},
|
||||
getIdToken: idToken ? async () => idToken : undefined,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { commonProvider } from './commonProvider';
|
||||
import { guestProvider } from './guestProvider';
|
||||
import { customProvider } from './customProvider';
|
||||
import { IdentityApiProxy } from './IdentityApiProxy';
|
||||
|
||||
const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider';
|
||||
|
||||
@@ -90,13 +91,15 @@ export const useSignInProviders = (
|
||||
// This decorates the result with sign out logic from this hook
|
||||
const handleWrappedResult = useCallback(
|
||||
(identityApi: IdentityApi) => {
|
||||
onSignInSuccess({
|
||||
...identityApi,
|
||||
signOut: async () => {
|
||||
localStorage.removeItem(PROVIDER_STORAGE_KEY);
|
||||
await identityApi.signOut?.();
|
||||
},
|
||||
});
|
||||
onSignInSuccess(
|
||||
IdentityApiProxy.from({
|
||||
identityApi,
|
||||
signOut: async () => {
|
||||
localStorage.removeItem(PROVIDER_STORAGE_KEY);
|
||||
await identityApi.signOut?.();
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
[onSignInSuccess],
|
||||
);
|
||||
|
||||
@@ -233,7 +233,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(identity.token || identity.id)) {
|
||||
if (!identity.token) {
|
||||
identity.token = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: identity.id },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstageIdentityResponse } from './types';
|
||||
|
||||
function parseJwtPayload(token: string) {
|
||||
const [_header, payload, _signature] = token.split('.');
|
||||
return JSON.parse(Buffer.from(payload, 'base64').toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token
|
||||
*/
|
||||
export function decorateWithIdentity(
|
||||
signInResolverResponse: Omit<BackstageIdentityResponse, 'identity'>,
|
||||
): BackstageIdentityResponse {
|
||||
const { sub, ent } = parseJwtPayload(signInResolverResponse.token);
|
||||
return {
|
||||
...signInResolverResponse,
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: sub,
|
||||
ownershipEntityRefs: ent ?? [sub],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -41,7 +41,12 @@ describe('GithubAuthProvider', () => {
|
||||
const tokenIssuer: TokenIssuer = {
|
||||
listPublicKeys: jest.fn(),
|
||||
async issueToken(params) {
|
||||
return `token-for-${params.claims.sub}`;
|
||||
const tokenContents = {
|
||||
sub: params.claims.sub,
|
||||
ent: params.claims.ent ?? [],
|
||||
};
|
||||
|
||||
return `eyblob.${btoa(JSON.stringify(tokenContents))}.eyblob`;
|
||||
},
|
||||
};
|
||||
const catalogIdentityClient = {
|
||||
@@ -93,7 +98,13 @@ describe('GithubAuthProvider', () => {
|
||||
const expected = {
|
||||
backstageIdentity: {
|
||||
id: 'jimmymarkum',
|
||||
token: 'token-for-jimmymarkum',
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob',
|
||||
identity: {
|
||||
ownershipEntityRefs: ['user:default/jimmymarkum'],
|
||||
type: 'user',
|
||||
userEntityRef: 'jimmymarkum',
|
||||
},
|
||||
},
|
||||
providerInfo: {
|
||||
accessToken: '19xasczxcm9n7gacn9jdgm19me',
|
||||
@@ -138,7 +149,13 @@ describe('GithubAuthProvider', () => {
|
||||
const expected = {
|
||||
backstageIdentity: {
|
||||
id: 'jimmymarkum',
|
||||
token: 'token-for-jimmymarkum',
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob',
|
||||
identity: {
|
||||
type: 'user',
|
||||
ownershipEntityRefs: ['user:default/jimmymarkum'],
|
||||
userEntityRef: 'jimmymarkum',
|
||||
},
|
||||
},
|
||||
providerInfo: {
|
||||
accessToken: '19xasczxcm9n7gacn9jdgm19me',
|
||||
@@ -181,7 +198,13 @@ describe('GithubAuthProvider', () => {
|
||||
const expected = {
|
||||
backstageIdentity: {
|
||||
id: 'jimmymarkum',
|
||||
token: 'token-for-jimmymarkum',
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob',
|
||||
identity: {
|
||||
type: 'user',
|
||||
ownershipEntityRefs: ['user:default/jimmymarkum'],
|
||||
userEntityRef: 'jimmymarkum',
|
||||
},
|
||||
},
|
||||
providerInfo: {
|
||||
accessToken: '19xasczxcm9n7gacn9jdgm19me',
|
||||
@@ -224,7 +247,13 @@ describe('GithubAuthProvider', () => {
|
||||
const expected = {
|
||||
backstageIdentity: {
|
||||
id: 'daveboyle',
|
||||
token: 'token-for-daveboyle',
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJkYXZlYm95bGUiLCJlbnQiOlsidXNlcjpkZWZhdWx0L2RhdmVib3lsZSJdfQ==.eyblob',
|
||||
identity: {
|
||||
type: 'user',
|
||||
ownershipEntityRefs: ['user:default/daveboyle'],
|
||||
userEntityRef: 'daveboyle',
|
||||
},
|
||||
},
|
||||
providerInfo: {
|
||||
accessToken:
|
||||
@@ -268,7 +297,13 @@ describe('GithubAuthProvider', () => {
|
||||
response: {
|
||||
backstageIdentity: {
|
||||
id: 'ipd12039',
|
||||
token: 'token-for-ipd12039',
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJpcGQxMjAzOSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvaXBkMTIwMzkiXX0=.eyblob',
|
||||
identity: {
|
||||
type: 'user',
|
||||
ownershipEntityRefs: ['user:default/ipd12039'],
|
||||
userEntityRef: 'ipd12039',
|
||||
},
|
||||
},
|
||||
providerInfo: {
|
||||
accessToken: 'a.b.c',
|
||||
@@ -321,7 +356,13 @@ describe('GithubAuthProvider', () => {
|
||||
expect(response).toEqual({
|
||||
backstageIdentity: {
|
||||
id: 'mockuser',
|
||||
token: 'token-for-mockuser',
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJtb2NrdXNlciIsImVudCI6WyJ1c2VyOmRlZmF1bHQvbW9ja3VzZXIiXX0=.eyblob',
|
||||
identity: {
|
||||
type: 'user',
|
||||
ownershipEntityRefs: ['user:default/mockuser'],
|
||||
userEntityRef: 'mockuser',
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
displayName: 'Mocked User',
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
AuthHandler,
|
||||
SignInResolver,
|
||||
StateEncoder,
|
||||
BackstageIdentityResponse,
|
||||
} from '../types';
|
||||
import {
|
||||
OAuthAdapter,
|
||||
@@ -167,7 +168,26 @@ export class GithubAuthProvider implements OAuthHandlers {
|
||||
};
|
||||
|
||||
if (this.signInResolver) {
|
||||
response.backstageIdentity = await this.signInResolver(
|
||||
const decorateWithIdentity = (
|
||||
signInResolverResponse: Omit<BackstageIdentityResponse, 'identity'>,
|
||||
): BackstageIdentityResponse => {
|
||||
function parseJwtPayload(token: string) {
|
||||
const [_header, payload, _signature] = token.split('.');
|
||||
return JSON.parse(Buffer.from(payload, 'base64').toString());
|
||||
}
|
||||
const { sub, ent } = parseJwtPayload(signInResolverResponse.token);
|
||||
return {
|
||||
...signInResolverResponse,
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: sub,
|
||||
ownershipEntityRefs: ent ?? [sub],
|
||||
},
|
||||
};
|
||||
// parse token
|
||||
// build identity
|
||||
};
|
||||
const signInResolverResult = await this.signInResolver(
|
||||
{
|
||||
result,
|
||||
profile,
|
||||
@@ -178,6 +198,8 @@ export class GithubAuthProvider implements OAuthHandlers {
|
||||
logger: this.logger,
|
||||
},
|
||||
);
|
||||
|
||||
response.backstageIdentity = decorateWithIdentity(signInResolverResult);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -159,7 +159,7 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
};
|
||||
|
||||
if (this.signInResolver) {
|
||||
response.backstageIdentity = await this.signInResolver(
|
||||
const signInResolverResult = await this.signInResolver(
|
||||
{
|
||||
result,
|
||||
profile,
|
||||
@@ -170,6 +170,8 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
logger: this.logger,
|
||||
},
|
||||
);
|
||||
|
||||
console.log(signInResolverResult);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -196,7 +196,7 @@ export type BackstageIdentityResponse = {
|
||||
/**
|
||||
* A plaintext description of the identity that is encapsulated within the token.
|
||||
*/
|
||||
identity?: BackstageUserIdentity;
|
||||
identity: BackstageUserIdentity;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -242,7 +242,7 @@ export type SignInResolver<AuthResult> = (
|
||||
catalogIdentityClient: CatalogIdentityClient;
|
||||
logger: Logger;
|
||||
},
|
||||
) => Promise<BackstageIdentityResponse>;
|
||||
) => Promise<Omit<BackstageIdentityResponse, 'identity'>>;
|
||||
|
||||
export type AuthHandlerResult = { profile: ProfileInfo };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user