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:
blam
2021-12-01 14:13:07 +01:00
parent 64c3fc492e
commit 29d0b45c6a
10 changed files with 200 additions and 27 deletions
@@ -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;
+2 -2
View File
@@ -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 };