diff --git a/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts b/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts new file mode 100644 index 0000000000..ec45bc7399 --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts @@ -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 { + return this.config.identityApi.getIdToken(); + } + + getProfile(): ProfileInfo { + return this.config.identityApi.getProfile(); + } + + getProfileInfo(): Promise { + return this.config.identityApi.getProfileInfo(); + } + + getBackstageIdentity(): Promise { + return this.config.identityApi.getBackstageIdentity(); + } + + getCredentials(): Promise<{ token?: string | undefined }> { + return this.config.identityApi.getCredentials(); + } + + signOut(): Promise { + return this.config.signOut(); + } +} diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index d0704c2b85..3a068decd5 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -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 ? ( diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index a6e46aee6a..9863234a06 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -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, }), ); }; diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx index 1ab7369dab..2abe6b3d51 100644 --- a/packages/core-components/src/layout/SignInPage/providers.tsx +++ b/packages/core-components/src/layout/SignInPage/providers.tsx @@ -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], ); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index bbef4790fe..d6a825f3fd 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -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 }, }); diff --git a/plugins/auth-backend/src/providers/decorateWithIdentity.ts b/plugins/auth-backend/src/providers/decorateWithIdentity.ts new file mode 100644 index 0000000000..dab1480db4 --- /dev/null +++ b/plugins/auth-backend/src/providers/decorateWithIdentity.ts @@ -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 { + const { sub, ent } = parseJwtPayload(signInResolverResponse.token); + return { + ...signInResolverResponse, + identity: { + type: 'user', + userEntityRef: sub, + ownershipEntityRefs: ent ?? [sub], + }, + }; +} diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index e418ab22c2..02c3367914 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -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', diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index c7e82cc5ff..eb15afa4ab 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -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 => { + 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; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index d85fc2de2d..293f738dfd 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -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; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 1d2c6f5134..919cbe42b4 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -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 = ( catalogIdentityClient: CatalogIdentityClient; logger: Logger; }, -) => Promise; +) => Promise>; export type AuthHandlerResult = { profile: ProfileInfo };