From 6304c8f94714b6fecb6759d831d5c1d2214b5aa2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 24 Nov 2021 11:59:59 +0100 Subject: [PATCH 01/23] core-plugin-api: Refactor IdentityApi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .../IdentityApi/AppIdentityProxy.ts | 90 ++++++++++++++++++ .../IdentityApi/GuestUserIdentity.ts | 60 ++++++++++++ .../IdentityApi/LegacyUserIdentity.ts | 76 ++++++++++++++++ packages/core-app-api/src/app/AppIdentity.ts | 85 ----------------- packages/core-app-api/src/app/AppManager.tsx | 89 +++++++++++++++--- packages/core-app-api/src/app/types.ts | 8 ++ .../src/layout/SignInPage/SignInPage.tsx | 38 ++++---- .../src/layout/SignInPage/UserIdentity.ts | 91 +++++++++++++++++++ .../src/apis/definitions/IdentityApi.ts | 64 +++++++++++-- .../src/apis/definitions/auth.ts | 46 ++++++++-- plugins/auth-backend/src/providers/types.ts | 45 ++++++--- 11 files changed, 543 insertions(+), 149 deletions(-) create mode 100644 packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts create mode 100644 packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts create mode 100644 packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts delete mode 100644 packages/core-app-api/src/app/AppIdentity.ts create mode 100644 packages/core-components/src/layout/SignInPage/UserIdentity.ts diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts new file mode 100644 index 0000000000..8a2f12594c --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts @@ -0,0 +1,90 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + BackstageUserIdentity, +} from '@backstage/core-plugin-api'; + +function mkError(thing: string) { + return new Error( + `Tried to access IdentityApi ${thing} before app was loaded`, + ); +} + +/** + * Implementation of the connection between the App-wide IdentityApi + * and sign-in page. + */ +export class AppIdentityProxy implements IdentityApi { + private target?: IdentityApi; + + // This is called by the app manager once the sign-in page provides us with an implementation + setTarget(identityApi: IdentityApi) { + this.target = identityApi; + } + + getUserId(): string { + if (!this.target) { + throw mkError('getUserId'); + } + return this.target.getUserId(); + } + + getProfile(): ProfileInfo { + if (!this.target) { + throw mkError('getProfile'); + } + return this.target.getProfile(); + } + + async getProfileInfo(): Promise { + if (!this.target) { + throw mkError('getProfileInfo'); + } + return this.target.getProfileInfo(); + } + + async getBackstageIdentity(): Promise { + if (!this.target) { + throw mkError('getBackstageIdentity'); + } + return this.target.getBackstageIdentity(); + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + if (!this.target) { + throw mkError('getCredentials'); + } + return this.target.getCredentials(); + } + + async getIdToken(): Promise { + if (!this.target) { + throw mkError('getIdToken'); + } + return this.target.getIdToken(); + } + + async signOut(): Promise { + if (!this.target) { + throw mkError('signOut'); + } + await this.target.signOut(); + location.reload(); + } +} diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts new file mode 100644 index 0000000000..db4731704c --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts @@ -0,0 +1,60 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + BackstageUserIdentity, +} from '@backstage/core-plugin-api'; + +export class GuestUserIdentity implements IdentityApi { + getUserId(): string { + return 'guest'; + } + + async getIdToken(): Promise { + return undefined; + } + + getProfile(): ProfileInfo { + return { + email: 'guest@example.com', + displayName: 'Guest', + }; + } + + async getProfileInfo(): Promise { + return { + email: 'guest@example.com', + displayName: 'Guest', + }; + } + + async getBackstageIdentity(): Promise { + const userEntityRef = `user:default/guest`; + return { + type: 'user', + userEntityRef, + ownershipEntityRefs: [userEntityRef], + }; + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + return {}; + } + + async signOut(): Promise {} +} diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts new file mode 100644 index 0000000000..5c74472cdb --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts @@ -0,0 +1,76 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + BackstageUserIdentity, + SignInResult, +} from '@backstage/core-plugin-api'; + +function parseJwtPayload(token: string) { + const [_header, payload, _signature] = token.split('.'); + return JSON.parse(atob(payload)); +} + +export class LegacyUserIdentity implements IdentityApi { + constructor(private readonly result: SignInResult) {} + + getUserId(): string { + return this.result.userId; + } + + async getIdToken(): Promise { + return this.result.getIdToken?.(); + } + + getProfile(): ProfileInfo { + return this.result.profile; + } + + async getProfileInfo(): Promise { + return this.result.profile; + } + + async getBackstageIdentity(): Promise { + const token = await this.getIdToken(); + + if (!token) { + const userEntityRef = `user:default/${this.getUserId()}`; + return { + type: 'user', + userEntityRef, + ownershipEntityRefs: [userEntityRef], + }; + } + + const { sub, ent } = parseJwtPayload(token); + return { + type: 'user', + userEntityRef: sub, + ownershipEntityRefs: ent ?? [sub], + }; + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + const token = await this.result.getIdToken?.(); + return { token }; + } + + async signOut(): Promise { + return this.result.signOut?.(); + } +} diff --git a/packages/core-app-api/src/app/AppIdentity.ts b/packages/core-app-api/src/app/AppIdentity.ts deleted file mode 100644 index 64e698102f..0000000000 --- a/packages/core-app-api/src/app/AppIdentity.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 { IdentityApi, ProfileInfo } from '@backstage/core-plugin-api'; -import { SignInResult } from './types'; - -/** - * Implementation of the connection between the App-wide IdentityApi - * and sign-in page. - */ -export class AppIdentity implements IdentityApi { - private hasIdentity = false; - private userId?: string; - private profile?: ProfileInfo; - private idTokenFunc?: () => Promise; - private signOutFunc?: () => Promise; - - getUserId(): string { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi userId before app was loaded', - ); - } - return this.userId!; - } - - getProfile(): ProfileInfo { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi profile before app was loaded', - ); - } - return this.profile!; - } - - async getIdToken(): Promise { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi idToken before app was loaded', - ); - } - return this.idTokenFunc?.(); - } - - async signOut(): Promise { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi signOutFunc before app was loaded', - ); - } - await this.signOutFunc?.(); - location.reload(); - } - - // This is indirectly called by the sign-in page to continue into the app. - setSignInResult(result: SignInResult) { - if (this.hasIdentity) { - return; - } - if (!result.userId) { - throw new Error('Invalid sign-in result, userId not set'); - } - if (!result.profile) { - throw new Error('Invalid sign-in result, profile not set'); - } - this.hasIdentity = true; - this.userId = result.userId; - this.profile = result.profile; - this.idTokenFunc = result.getIdToken; - this.signOutFunc = result.signOut; - } -} diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 5903a8c874..13ac8b4177 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -43,12 +43,15 @@ import { AppThemeApi, ConfigApi, featureFlagsApiRef, + IdentityApi, identityApiRef, BackstagePlugin, RouteRef, SubRouteRef, ExternalRouteRef, } from '@backstage/core-plugin-api'; +import { GuestUserIdentity } from '../apis/implementations/IdentityApi/GuestUserIdentity'; +import { LegacyUserIdentity } from '../apis/implementations/IdentityApi/LegacyUserIdentity'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, @@ -66,7 +69,7 @@ import { RoutingProvider } from '../routing/RoutingProvider'; import { RouteTracker } from '../routing/RouteTracker'; import { validateRoutes } from '../routing/validation'; import { AppContextProvider } from './AppContext'; -import { AppIdentity } from './AppIdentity'; +import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy'; import { AppComponents, AppConfigLoader, @@ -189,7 +192,7 @@ export class AppManager implements BackstageApp { private readonly defaultApis: Iterable; private readonly bindRoutes: AppOptions['bindRoutes']; - private readonly identityApi = new AppIdentity(); + private readonly appIdentityProxy = new AppIdentityProxy(); private readonly apiFactoryRegistry: ApiFactoryRegistry; constructor(options: AppOptions) { @@ -344,14 +347,23 @@ export class AppManager implements BackstageApp { component: ComponentType; children: ReactElement; }) => { - const [result, setResult] = useState(); + const [identityApi, setIdentityApi] = useState(); - if (result) { - this.identityApi.setSignInResult(result); - return children; + const setLegacyResult = (result: SignInResult) => { + setIdentityApi(new LegacyUserIdentity(result)); + }; + + if (!identityApi) { + return ( + + ); } - return ; + this.appIdentityProxy.setTarget(identityApi); + return children; }; const AppRouter = ({ children }: PropsWithChildren<{}>) => { @@ -360,13 +372,7 @@ export class AppManager implements BackstageApp { // If the app hasn't configured a sign-in page, we just continue as guest. if (!SignInPageComponent) { - this.identityApi.setSignInResult({ - userId: 'guest', - profile: { - email: 'guest@example.com', - displayName: 'Guest', - }, - }); + this.appIdentityProxy.setTarget(new GuestUserIdentity()); return ( @@ -430,7 +436,7 @@ export class AppManager implements BackstageApp { this.apiFactoryRegistry.register('static', { api: identityApiRef, deps: {}, - factory: () => this.identityApi, + factory: () => this.appIdentityProxy, }); // It's possible to replace the feature flag API, but since we must have at least @@ -485,3 +491,56 @@ export class AppManager implements BackstageApp { } } } + +interface FooPropsV1 { + foo: () => undefined; +} + +interface FooPropsV2 { + foo: () => undefined; + bar: () => undefined; +} + +// type FooProps = { +// foo: () => undefined +// } | { +// foo: () => undefined +// bar: () => undefined +// } + +interface CreateDerpOptions { + components: { + Foo: (props: FooPropsV1 | FooPropsV2) => JSX.Element; + }; +} + +interface Derp { + getComponents(): { + Foo: (props: FooPropsV1) => JSX.Element; + }; +} + +function createDerp(options: CreateDerpOptions): Derp { + return { getComponents: () => options.components }; +} + +function CustomFoo(props: FooPropsV1) { + return
{props.foo()}
; +} + +function NewCustomFoo(props: FooPropsV2) { + return ( +
+ {props.foo()} {props.bar()} +
+ ); +} + +const derp = createDerp({ + components: { + Foo: NewCustomFoo, + }, +}); + +const { Foo } = derp.getComponents(); +const _foo = undefined} />; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index f16c8ac656..9e89bc0b56 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -25,6 +25,7 @@ import { SubRouteRef, ExternalRouteRef, PluginOutput, + IdentityApi, } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; @@ -42,6 +43,7 @@ export type BootErrorPageProps = { * The outcome of signing in on the sign-in page. * * @public + * @deprecated replaced by passing the {@link IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead. */ export type SignInResult = { /** @@ -70,8 +72,14 @@ export type SignInResult = { export type SignInPageProps = { /** * Set the sign-in result for the app. This should only be called once. + * @deprecated use {@link SignInPageProps.onSignInSuccess} instead. */ onResult(result: SignInResult): void; + + /** + * Set the IdentityApi on successful sign in. This should only be called once. + */ + onSignInSuccess(identityApi: IdentityApi): void; }; /** diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index bd4fe37480..be2287f705 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -15,11 +15,12 @@ */ import { - BackstageIdentity, + BackstageIdentityResponse, configApiRef, SignInPageProps, useApi, } from '@backstage/core-plugin-api'; +import { UserIdentity } from './UserIdentity'; import Button from '@material-ui/core/Button'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; @@ -87,7 +88,7 @@ export const MultiSignInPage = ({ }; export const SingleSignInPage = ({ - onResult, + onSignInSuccess, provider, auto, }: SingleSignInPageProps) => { @@ -105,54 +106,47 @@ export const SingleSignInPage = ({ type LoginOpts = { checkExisting?: boolean; showPopup?: boolean }; const login = async ({ checkExisting, showPopup }: LoginOpts) => { try { - let identity: BackstageIdentity | undefined; + let identityResponse: BackstageIdentityResponse | undefined; if (checkExisting) { // Do an initial check if any logged-in session exists - identity = await authApi.getBackstageIdentity({ + identityResponse = await authApi.getBackstageIdentity({ optional: true, }); } // If no session exists, show the sign-in page - if (!identity && (showPopup || auto)) { + if (!identityResponse && (showPopup || auto)) { // Unless auto is set to true, this step should not happen. // When user intentionally clicks the Sign In button, autoShowPopup is set to true setShowLoginPage(true); - identity = await authApi.getBackstageIdentity({ + identityResponse = await authApi.getBackstageIdentity({ instantPopup: true, }); - if (!identity) { + if (!identityResponse) { throw new Error( `The ${provider.title} provider is not configured to support sign-in`, ); } } - if (!identity) { + if (!identityResponse) { setShowLoginPage(true); return; } - const profile = await authApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => { - return authApi - .getBackstageIdentity() - .then(i => i!.token ?? i!.idToken); - }, - signOut: async () => { - await authApi.signOut(); - }, - }); + onSignInSuccess( + UserIdentity.from({ + identity: identityResponse.identity, + authApi, + profile, + }), + ); } catch (err: any) { // User closed the sign-in modal setError(err); setShowLoginPage(true); } }; - useMount(() => login({ checkExisting: true })); return showLoginPage ? ( diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts new file mode 100644 index 0000000000..daf9111d84 --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -0,0 +1,91 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + ProfileInfoApi, + BackstageUserIdentity, + BackstageIdentityApi, + SessionApi, +} from '@backstage/core-plugin-api'; + +export class UserIdentity implements IdentityApi { + static from(options: { + identity: BackstageUserIdentity; + authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; + /** + * Passing a profile synchronously allows the deprecated `getProfile` method to be + * called by consumers of the {@link IdentityApi}. If you do not have any consumers + * of that method than this is safe to leave out. + * + * @deprecated Only provide this if you have plugins that call the synchronous `getProfile` method, which is also deprecated. + */ + profile?: ProfileInfo; + }) { + return new UserIdentity(options.identity, options.authApi, options.profile); + } + + private constructor( + private readonly identity: BackstageUserIdentity, + private readonly authApi: ProfileInfoApi & + BackstageIdentityApi & + SessionApi, + private readonly profile?: ProfileInfo, + ) {} + + getUserId(): string { + const ref = this.identity.userEntityRef; + const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref); + if (!match) { + throw new TypeError(`Invalid user entity reference "${ref}"`); + } + + return match[3]; + } + + async getIdToken(): Promise { + const identity = await this.authApi.getBackstageIdentity(); + return identity!.token; + } + + getProfile(): ProfileInfo { + if (!this.profile) { + throw new Error( + 'The identity API does not implement synchronous profile fetching, use getProfileInfo() instead', + ); + } + return this.profile; + } + + async getProfileInfo(): Promise { + const profile = await this.authApi.getProfile(); + return profile!; + } + + async getBackstageIdentity(): Promise { + return this.identity; + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + const identity = await this.authApi.getBackstageIdentity(); + return { token: identity!.token }; + } + + async signOut(): Promise { + return this.authApi.signOut(); + } +} diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 9f68a8b2cc..38376491ca 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -16,6 +16,34 @@ import { ApiRef, createApiRef } from '../system'; import { ProfileInfo } from './auth'; +/* + +- [ ] IdentityApi getProfile, make async +- [ ] BackstageIdentity (settle or remove) +- [ ] Evolution plan for utility APIs + +*/ + +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; +}; + /** * The Identity API used to identify and get information about the signed in user. * @@ -26,25 +54,45 @@ export type IdentityApi = { * The ID of the signed in user. This ID is not meant to be presented to the user, but used * as an opaque string to pass on to backends or use in frontend logic. * - * TODO: The intention of the user ID is to be able to tie the user to an identity - * that is known by the catalog and/or identity backend. It should for example - * be possible to fetch all owned components using this ID. + * @deprecated use {@link IdentityApi.getIdentity} instead. */ getUserId(): string; - /** - * The profile of the signed in user. - */ - getProfile(): ProfileInfo; - /** * An OpenID Connect ID Token which proves the identity of the signed in user. * * The ID token will be undefined if the signed in user does not have a verified * identity, such as a demo user or mocked user for e2e tests. + * + * @deprecated use {@link IdentityApi.getCredentials} instead. */ getIdToken(): Promise; + /** + * The profile of the signed in user. + * + * @deprecated use {@link IdentityApi.getProfileInfo} instead. + */ + getProfile(): ProfileInfo; + + /** + * The profile of the signed in user. + */ + getProfileInfo(): Promise; + + /** + * User identity information within Backstage. + */ + getBackstageIdentity(): Promise; + + /** + * Provides credentials in the form of a token which proves the identity of the signed in user. + * + * The token will be undefined if the signed in user does not have a verified + * identity, such as a demo user or mocked user for e2e tests. + */ + getCredentials(): Promise<{ token?: string }>; + /** * Sign out the current user */ diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 8fe532f3a6..8da7fbe6fd 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -160,7 +160,31 @@ export type BackstageIdentityApi = { */ getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; +}; + +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + /** + * The type of identity that this structure represents. In the frontend app + * this will currently always be 'user'. + */ + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; }; /** @@ -168,23 +192,31 @@ export type BackstageIdentityApi = { * * @public */ -export type BackstageIdentity = { +export type BackstageIdentityResponse = { /** * The backstage user ID. + * + * @deprecated The identity is now provided via the `identity` field instead. */ id: string; - /** - * @deprecated This is deprecated, use `token` instead. - */ - idToken: string; - /** * The token used to authenticate the user within Backstage. */ token: string; + + /** + * Identity information derived from the token. + */ + identity: BackstageUserIdentity; }; +/** + * @public + * @deprecated use {@link BackstageIdentityResponse} instead. + */ +export type BackstageIdentity = BackstageIdentityResponse; + /** * Profile information of the user. * diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 089c82b293..04da05a314 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -140,32 +140,51 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentity; }; -export type BackstageIdentity = { +/** + * @public + */ +export type BackstageIdentityResponse = { /** * An opaque ID that uniquely identifies the user within Backstage. * * This is typically the same as the user entity `metadata.name`. + * + * @deprecated Use the `identity` field instead */ id: string; - /** - * This is deprecated, use `token` instead. - * @deprecated - */ - idToken?: string; - - /** - * The token used to authenticate the user within Backstage. - */ - token?: string; - /** * The entity that the user is represented by within Backstage. * * This entity may or may not exist within the Catalog, and it can be used * to read and store additional metadata about the user. + * + * @deprecated Use the `identity` field instead. */ entity?: Entity; + + /** + * The token used to authenticate the user within Backstage. + */ + token: string; + + /** + * A plaintext description of the identity that is encapsulated within the token. + */ + identity?: { + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; + }; }; /** @@ -173,6 +192,8 @@ export type BackstageIdentity = { * * It is also temporarily used as the profile of the signed-in user's Backstage * identity, but we want to replace that with data from identity and/org catalog service + * + * @public */ export type ProfileInfo = { /** From 32b04436608362c868a6fc9736f9124de5ec9b39 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 26 Nov 2021 16:05:32 +0100 Subject: [PATCH 02/23] core-plugin-api: Use LegacyUserIdentity helper Co-authored-by: blam Signed-off-by: Johan Haals --- .../IdentityApi/LegacyUserIdentity.ts | 6 +- packages/core-app-api/src/app/AppManager.tsx | 101 ++++++++---------- packages/core-app-api/src/app/types.ts | 6 -- .../src/layout/SignInPage/SignInPage.tsx | 8 +- .../src/layout/SignInPage/auth0Provider.tsx | 42 ++++---- .../src/layout/SignInPage/commonProvider.tsx | 45 ++++---- .../src/layout/SignInPage/customProvider.tsx | 19 ++-- .../src/layout/SignInPage/guestProvider.tsx | 15 +-- .../src/layout/SignInPage/providers.tsx | 27 +++-- .../src/layout/SignInPage/types.ts | 4 +- plugins/auth-backend/src/index.ts | 2 +- plugins/auth-backend/src/providers/types.ts | 4 +- 12 files changed, 129 insertions(+), 150 deletions(-) diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts index 5c74472cdb..ff6d38c860 100644 --- a/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts @@ -27,12 +27,16 @@ function parseJwtPayload(token: string) { } export class LegacyUserIdentity implements IdentityApi { - constructor(private readonly result: SignInResult) {} + private constructor(private readonly result: SignInResult) {} getUserId(): string { return this.result.userId; } + static fromResult(result: SignInResult): LegacyUserIdentity { + return new LegacyUserIdentity(result); + } + async getIdToken(): Promise { return this.result.getIdToken?.(); } diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 13ac8b4177..31e093a145 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -51,7 +51,6 @@ import { ExternalRouteRef, } from '@backstage/core-plugin-api'; import { GuestUserIdentity } from '../apis/implementations/IdentityApi/GuestUserIdentity'; -import { LegacyUserIdentity } from '../apis/implementations/IdentityApi/LegacyUserIdentity'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, @@ -78,7 +77,6 @@ import { AppRouteBinder, BackstageApp, SignInPageProps, - SignInResult, } from './types'; import { AppThemeProvider } from './AppThemeProvider'; import { defaultConfigLoader } from './defaultConfigLoader'; @@ -349,17 +347,8 @@ export class AppManager implements BackstageApp { }) => { const [identityApi, setIdentityApi] = useState(); - const setLegacyResult = (result: SignInResult) => { - setIdentityApi(new LegacyUserIdentity(result)); - }; - if (!identityApi) { - return ( - - ); + return ; } this.appIdentityProxy.setTarget(identityApi); @@ -492,55 +481,55 @@ export class AppManager implements BackstageApp { } } -interface FooPropsV1 { - foo: () => undefined; -} - -interface FooPropsV2 { - foo: () => undefined; - bar: () => undefined; -} - -// type FooProps = { -// foo: () => undefined -// } | { -// foo: () => undefined -// bar: () => undefined +// interface FooPropsV1 { +// foo: () => undefined; // } -interface CreateDerpOptions { - components: { - Foo: (props: FooPropsV1 | FooPropsV2) => JSX.Element; - }; -} +// interface FooPropsV2 { +// foo: () => undefined; +// bar: () => undefined; +// } -interface Derp { - getComponents(): { - Foo: (props: FooPropsV1) => JSX.Element; - }; -} +// // type FooProps = { +// // foo: () => undefined +// // } | { +// // foo: () => undefined +// // bar: () => undefined +// // } -function createDerp(options: CreateDerpOptions): Derp { - return { getComponents: () => options.components }; -} +// interface CreateDerpOptions { +// components: { +// Foo: (props: FooPropsV1 | FooPropsV2) => JSX.Element; +// }; +// } -function CustomFoo(props: FooPropsV1) { - return
{props.foo()}
; -} +// interface Derp { +// getComponents(): { +// Foo: (props: FooPropsV1) => JSX.Element; +// }; +// } -function NewCustomFoo(props: FooPropsV2) { - return ( -
- {props.foo()} {props.bar()} -
- ); -} +// function createDerp(options: CreateDerpOptions): Derp { +// return { getComponents: () => options.components }; +// } -const derp = createDerp({ - components: { - Foo: NewCustomFoo, - }, -}); +// function CustomFoo(props: FooPropsV1) { +// return
{props.foo()}
; +// } -const { Foo } = derp.getComponents(); -const _foo = undefined} />; +// function NewCustomFoo(props: FooPropsV2) { +// return ( +//
+// {props.foo()} {props.bar()} +//
+// ); +// } + +// const derp = createDerp({ +// components: { +// Foo: NewCustomFoo, +// }, +// }); + +// const { Foo } = derp.getComponents(); +// const _foo = undefined} />; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 9e89bc0b56..0921bd2e17 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -70,12 +70,6 @@ export type SignInResult = { * @public */ export type SignInPageProps = { - /** - * Set the sign-in result for the app. This should only be called once. - * @deprecated use {@link SignInPageProps.onSignInSuccess} instead. - */ - onResult(result: SignInResult): void; - /** * Set the IdentityApi on successful sign in. This should only be called once. */ diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index be2287f705..d0704c2b85 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -50,7 +50,7 @@ type SingleSignInPageProps = SignInPageProps & { export type Props = MultiSignInPageProps | SingleSignInPageProps; export const MultiSignInPage = ({ - onResult, + onSignInSuccess, providers = [], title, align = 'left', @@ -61,7 +61,7 @@ export const MultiSignInPage = ({ const signInProviders = getSignInProviders(providers); const [loading, providerElements] = useSignInProviders( signInProviders, - onResult, + onSignInSuccess, ); if (loading) { @@ -88,9 +88,9 @@ export const MultiSignInPage = ({ }; export const SingleSignInPage = ({ - onSignInSuccess, provider, auto, + onSignInSuccess, }: SingleSignInPageProps) => { const classes = useStyles(); const authApi = useApi(provider.apiRef); @@ -133,7 +133,9 @@ export const SingleSignInPage = ({ setShowLoginPage(true); return; } + const profile = await authApi.getProfile(); + onSignInSuccess( UserIdentity.from({ identity: identityResponse.identity, diff --git a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx index e1372a2362..8e2728a95e 100644 --- a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx +++ b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx @@ -26,17 +26,18 @@ import { errorApiRef, } from '@backstage/core-plugin-api'; import { ForwardedError } from '@backstage/errors'; +import { UserIdentity } from './UserIdentity'; -const Component: ProviderComponent = ({ onResult }) => { +const Component: ProviderComponent = ({ onSignInSuccess }) => { const auth0AuthApi = useApi(auth0AuthApiRef); const errorApi = useApi(errorApiRef); const handleLogin = async () => { try { - const identity = await auth0AuthApi.getBackstageIdentity({ + const identityResponse = await auth0AuthApi.getBackstageIdentity({ instantPopup: true, }); - if (!identity) { + if (!identityResponse) { throw new Error( 'The Auth0 provider is not configured to support sign-in', ); @@ -44,15 +45,13 @@ const Component: ProviderComponent = ({ onResult }) => { const profile = await auth0AuthApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => - auth0AuthApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken), - signOut: async () => { - await auth0AuthApi.signOut(); - }, - }); + onSignInSuccess( + UserIdentity.from({ + identity: identityResponse.identity, + authApi: auth0AuthApi, + profile, + }), + ); } catch (error) { errorApi.post(new ForwardedError('Auth0 login failed', error)); } @@ -77,25 +76,20 @@ const Component: ProviderComponent = ({ onResult }) => { const loader: ProviderLoader = async apis => { const auth0AuthApi = apis.get(auth0AuthApiRef)!; - const identity = await auth0AuthApi.getBackstageIdentity({ + const identityResponse = await auth0AuthApi.getBackstageIdentity({ optional: true, }); - if (!identity) { + if (!identityResponse) { return undefined; } const profile = await auth0AuthApi.getProfile(); - - return { - userId: identity.id, - profile: profile!, - getIdToken: () => - auth0AuthApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken), - signOut: async () => { - await auth0AuthApi.signOut(); - }, - }; + return UserIdentity.from({ + identity: identityResponse.identity, + authApi: auth0AuthApi, + profile, + }); }; export const auth0Provider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/commonProvider.tsx b/packages/core-components/src/layout/SignInPage/commonProvider.tsx index 515ae01e4d..48dd7df62c 100644 --- a/packages/core-components/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/commonProvider.tsx @@ -27,36 +27,33 @@ import { import { useApi, errorApiRef } from '@backstage/core-plugin-api'; import { GridItem } from './styles'; import { ForwardedError } from '@backstage/errors'; +import { UserIdentity } from './UserIdentity'; -const Component: ProviderComponent = ({ config, onResult }) => { +const Component: ProviderComponent = ({ config, onSignInSuccess }) => { const { apiRef, title, message } = config as SignInProviderConfig; const authApi = useApi(apiRef); const errorApi = useApi(errorApiRef); const handleLogin = async () => { try { - const identity = await authApi.getBackstageIdentity({ + const identityResponse = await authApi.getBackstageIdentity({ instantPopup: true, }); - if (!identity) { + if (!identityResponse) { throw new Error( `The ${title} provider is not configured to support sign-in`, ); } const profile = await authApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => { - return authApi - .getBackstageIdentity() - .then(i => i!.token ?? i!.idToken); - }, - signOut: async () => { - await authApi.signOut(); - }, - }); + + onSignInSuccess( + UserIdentity.from({ + identity: identityResponse.identity, + profile, + authApi, + }), + ); } catch (error) { errorApi.post(new ForwardedError('Login failed', error)); } @@ -82,25 +79,21 @@ const Component: ProviderComponent = ({ config, onResult }) => { const loader: ProviderLoader = async (apis, apiRef) => { const authApi = apis.get(apiRef)!; - const identity = await authApi.getBackstageIdentity({ + const identityResponse = await authApi.getBackstageIdentity({ optional: true, }); - if (!identity) { + if (!identityResponse) { return undefined; } const profile = await authApi.getProfile(); - return { - userId: identity.id, - profile: profile!, - getIdToken: () => - authApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken), - signOut: async () => { - await authApi.signOut(); - }, - }; + return UserIdentity.from({ + identity: identityResponse.identity, + profile, + authApi, + }); }; export const commonProvider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index da1848ac05..77bde5223c 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -26,6 +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 '@backstage/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity'; // 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; @@ -60,7 +61,7 @@ const asInputRef = (renderResult: UseFormRegisterReturn) => { }; }; -const Component: ProviderComponent = ({ onResult }) => { +const Component: ProviderComponent = ({ onSignInSuccess }) => { const classes = useFormStyles(); const { register, handleSubmit, formState } = useForm({ mode: 'onChange', @@ -69,13 +70,15 @@ const Component: ProviderComponent = ({ onResult }) => { const { errors } = formState; const handleResult = ({ userId, idToken }: Data) => { - onResult({ - userId, - profile: { - email: `${userId}@example.com`, - }, - getIdToken: idToken ? async () => idToken : undefined, - }); + onSignInSuccess( + LegacyUserIdentity.fromResult({ + userId, + profile: { + email: `${userId}@example.com`, + }, + getIdToken: idToken ? async () => idToken : undefined, + }), + ); }; return ( diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index e91d9cbc75..9e0326c7df 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -20,16 +20,9 @@ import Button from '@material-ui/core/Button'; import { InfoCard } from '../InfoCard/InfoCard'; import { GridItem } from './styles'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +import { GuestUserIdentity } from '@backstage/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity'; -const result = { - userId: 'guest', - profile: { - email: 'guest@example.com', - displayName: 'Guest', - }, -}; - -const Component: ProviderComponent = ({ onResult }) => ( +const Component: ProviderComponent = ({ onSignInSuccess }) => ( ( @@ -56,7 +49,7 @@ const Component: ProviderComponent = ({ onResult }) => ( ); const loader: ProviderLoader = async () => { - return result; + return new GuestUserIdentity(); }; export const guestProvider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx index 19915e6aa9..1ab7369dab 100644 --- a/packages/core-components/src/layout/SignInPage/providers.tsx +++ b/packages/core-components/src/layout/SignInPage/providers.tsx @@ -17,10 +17,10 @@ import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; import { SignInPageProps, - SignInResult, useApi, useApiHolder, errorApiRef, + IdentityApi, } from '@backstage/core-plugin-api'; import { IdentityProviders, @@ -81,7 +81,7 @@ export function getSignInProviders( export const useSignInProviders = ( providers: SignInProviderType, - onResult: SignInPageProps['onResult'], + onSignInSuccess: SignInPageProps['onSignInSuccess'], ) => { const errorApi = useApi(errorApiRef); const apiHolder = useApiHolder(); @@ -89,16 +89,16 @@ export const useSignInProviders = ( // This decorates the result with sign out logic from this hook const handleWrappedResult = useCallback( - (result: SignInResult) => { - onResult({ - ...result, + (identityApi: IdentityApi) => { + onSignInSuccess({ + ...identityApi, signOut: async () => { localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.signOut?.(); + await identityApi.signOut?.(); }, }); }, - [onResult], + [onSignInSuccess], ); // In this effect we check if the user has already selected an existing login @@ -151,7 +151,14 @@ export const useSignInProviders = ( return () => { didCancel = true; }; - }, [loading, errorApi, onResult, apiHolder, providers, handleWrappedResult]); + }, [ + loading, + errorApi, + onSignInSuccess, + apiHolder, + providers, + handleWrappedResult, + ]); // This renders all available sign-in providers const elements = useMemo( @@ -161,7 +168,7 @@ export const useSignInProviders = ( const { Component } = provider.components; - const handleResult = (result: SignInResult) => { + const handleSignInSuccess = (result: IdentityApi) => { localStorage.setItem(PROVIDER_STORAGE_KEY, provider.id); handleWrappedResult(result); @@ -171,7 +178,7 @@ export const useSignInProviders = ( ); }), diff --git a/packages/core-components/src/layout/SignInPage/types.ts b/packages/core-components/src/layout/SignInPage/types.ts index 1e4e4a2997..5b458448bf 100644 --- a/packages/core-components/src/layout/SignInPage/types.ts +++ b/packages/core-components/src/layout/SignInPage/types.ts @@ -17,12 +17,12 @@ import { ComponentType } from 'react'; import { SignInPageProps, - SignInResult, ApiHolder, ApiRef, ProfileInfoApi, BackstageIdentityApi, SessionApi, + IdentityApi, } from '@backstage/core-plugin-api'; export type SignInProviderConfig = { @@ -41,7 +41,7 @@ export type ProviderComponent = ComponentType< export type ProviderLoader = ( apis: ApiHolder, apiRef: ApiRef, -) => Promise; +) => Promise; export type SignInProvider = { Component: ProviderComponent; diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 894f8c1e55..66c1b42dd9 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -29,7 +29,7 @@ export * from './providers'; // ensuresXRequestedWith and postMessageResponse to safely handle CORS requests for login. The WebMessageResponse type in flow is used to type the response from the login-popup export * from './lib/flow'; -// OAuth wrapper over a passport or a custom `startegy`. +// OAuth wrapper over a passport or a custom `strategy`. export * from './lib/oauth'; export * from './lib/catalog'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 04da05a314..52cff0bdf9 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -137,7 +137,7 @@ export type AuthProviderFactory = ( export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; - backstageIdentity?: BackstageIdentity; + backstageIdentity?: BackstageIdentityResponse; }; /** @@ -230,7 +230,7 @@ export type SignInResolver = ( catalogIdentityClient: CatalogIdentityClient; logger: Logger; }, -) => Promise; +) => Promise; export type AuthHandlerResult = { profile: ProfileInfo }; From e9471d274c244c13daa34250e242fa7ac3d164fe Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 30 Nov 2021 13:14:48 +0100 Subject: [PATCH 03/23] Use BackstageUserIdentity, fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: blam Co-authored-by: Fredrik Adelöw Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- .../src/apis/definitions/IdentityApi.ts | 22 +---------- .../src/identity/IdentityClient.test.ts | 20 +++++++++- .../src/identity/IdentityClient.ts | 26 +++++++++---- .../src/lib/flow/authFlowHelpers.test.ts | 6 +-- .../src/lib/oauth/OAuthAdapter.test.ts | 1 + .../src/lib/oauth/OAuthAdapter.ts | 10 ++--- .../src/providers/auth0/provider.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 2 +- .../src/providers/oidc/provider.ts | 1 - .../src/providers/onelogin/provider.ts | 2 +- plugins/auth-backend/src/providers/types.ts | 39 ++++++++++++------- .../src/api/CatalogImportClient.test.ts | 3 ++ .../DefaultImportPage.test.tsx | 3 ++ .../components/ImportPage/ImportPage.test.tsx | 3 ++ .../catalog/src/CatalogClientWrapper.test.ts | 6 +++ .../cost-insights/src/testUtils/providers.tsx | 3 ++ plugins/fossa/src/api/FossaClient.test.ts | 13 +------ plugins/search/src/apis.test.ts | 3 ++ .../sonarqube/src/api/SonarQubeClient.test.ts | 6 +++ plugins/techdocs/src/client.test.ts | 3 ++ 20 files changed, 107 insertions(+), 67 deletions(-) diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 38376491ca..4b0672d745 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { ApiRef, createApiRef } from '../system'; -import { ProfileInfo } from './auth'; +import { BackstageUserIdentity, ProfileInfo } from './auth'; /* @@ -24,26 +24,6 @@ import { ProfileInfo } from './auth'; */ -/** - * User identity information within Backstage. - * - * @public - */ -export type BackstageUserIdentity = { - type: 'user'; - - /** - * The entityRef of the user in the catalog. - * For example User:default/sandra - */ - userEntityRef: string; - - /** - * The user and group entities that the user claims ownership through - */ - ownershipEntityRefs: string[]; -}; - /** * The Identity API used to identify and get information about the signed in user. * diff --git a/plugins/auth-backend/src/identity/IdentityClient.test.ts b/plugins/auth-backend/src/identity/IdentityClient.test.ts index 5eca6bf8cd..9f5e1ce489 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.test.ts +++ b/plugins/auth-backend/src/identity/IdentityClient.test.ts @@ -96,7 +96,15 @@ describe('IdentityClient', () => { it('should accept fresh token', async () => { const token = await factory.issueToken({ claims: { sub: 'foo' } }); const response = await client.authenticate(token); - expect(response).toEqual({ id: 'foo', idToken: token }); + expect(response).toEqual({ + id: 'foo', + token: token, + identity: { + ownershipEntityRefs: [], + type: 'user', + userEntityRef: 'foo', + }, + }); }); it('should throw on incorrect issuer', async () => { @@ -159,7 +167,15 @@ describe('IdentityClient', () => { jest.spyOn(Date, 'now').mockImplementation(() => fixedTime); const token = await factory.issueToken({ claims: { sub: 'foo' } }); const response = await client.authenticate(token); - expect(response).toEqual({ id: 'foo', idToken: token }); + expect(response).toEqual({ + id: 'foo', + token: token, + identity: { + ownershipEntityRefs: [], + type: 'user', + userEntityRef: 'foo', + }, + }); }); it('should not be fooled by the none algorithm', async () => { diff --git a/plugins/auth-backend/src/identity/IdentityClient.ts b/plugins/auth-backend/src/identity/IdentityClient.ts index d60829bf59..552d231e1f 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.ts +++ b/plugins/auth-backend/src/identity/IdentityClient.ts @@ -16,8 +16,9 @@ import fetch from 'node-fetch'; import { JWK, JWT, JWKS, JSONWebKey } from 'jose'; -import { BackstageIdentity } from '../providers'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { AuthenticationError } from '@backstage/errors'; +import { BackstageIdentityResponse } from '../providers/types'; const CLOCK_MARGIN_S = 10; @@ -45,15 +46,17 @@ export class IdentityClient { * Returns a BackstageIdentity (user) matching the token. * The method throws an error if verification fails. */ - async authenticate(token: string | undefined): Promise { + async authenticate( + token: string | undefined, + ): Promise { // Extract token from header if (!token) { - throw new Error('No token specified'); + throw new AuthenticationError('No token specified'); } // Get signing key matching token const key = await this.getKey(token); if (!key) { - throw new Error('No signing key matching token found'); + throw new AuthenticationError('No signing key matching token found'); } // Verify token claims and signature // Note: Claims must match those set by TokenFactory when issuing tokens @@ -62,12 +65,21 @@ export class IdentityClient { algorithms: ['ES256'], audience: 'backstage', issuer: this.issuer, - }) as { sub: string }; + }) as { sub: string; ent: string[] }; // Verified, return the matching user as BackstageIdentity // TODO: Settle internal user format/properties - const user: BackstageIdentity = { + if (!decoded.sub) { + throw new AuthenticationError('No user sub found in token'); + } + + const user: BackstageIdentityResponse = { id: decoded.sub, - idToken: token, + token, + identity: { + type: 'user', + userEntityRef: decoded.sub, + ownershipEntityRefs: decoded.ent ?? [], + }, }; return user; } diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts index 117f1b86e4..8b4b95d59b 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -51,7 +51,7 @@ describe('oauth helpers', () => { }, backstageIdentity: { id: 'a', - idToken: 'a.b.c', + token: 'a.b.c', }, }, }; @@ -106,7 +106,7 @@ describe('oauth helpers', () => { }, backstageIdentity: { id: 'a', - idToken: 'a.b.c', + token: 'a.b.c', }, }, }; @@ -148,7 +148,7 @@ describe('oauth helpers', () => { }, backstageIdentity: { id: 'a', - idToken: 'a.b.c', + token: 'a.b.c', }, }, }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index fa61d69d80..5e52e60f46 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -31,6 +31,7 @@ const mockResponseData = { }, backstageIdentity: { id: 'foo', + token: '', }, }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index e1a6fdf2f9..2df67462cf 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -19,8 +19,8 @@ import crypto from 'crypto'; import { URL } from 'url'; import { AuthProviderRouteHandlers, - BackstageIdentity, AuthProviderConfig, + BackstageIdentityResponse, } from '../../providers/types'; import { AuthenticationError, @@ -228,17 +228,17 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { * If the response from the OAuth provider includes a Backstage identity, we * make sure it's populated with all the information we can derive from the user ID. */ - private async populateIdentity(identity?: BackstageIdentity) { + private async populateIdentity(identity?: BackstageIdentityResponse) { if (!identity) { return; } - if (!(identity.token || identity.idToken)) { + if (!(identity.token || identity.token)) { identity.token = await this.options.tokenIssuer.issueToken({ claims: { sub: identity.id }, }); - } else if (!identity.token && identity.idToken) { - identity.token = identity.idToken; + } else if (!identity.token && identity.token) { + identity.token = identity.token; } } diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 4cc3f33499..7aa98c3e65 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -151,7 +151,7 @@ export class Auth0AuthProvider implements OAuthHandlers { const id = profile.email.split('@')[0]; - return { ...response, backstageIdentity: { id } }; + return { ...response, backstageIdentity: { id, token: '' } }; } } diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 88c6ecb6fe..5ad0b84074 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -38,4 +38,4 @@ export type { // These types are needed for a postMessage from the login pop-up // to the frontend -export type { AuthResponse, BackstageIdentity, ProfileInfo } from './types'; +export type { AuthResponse, BackstageUserIdentity, ProfileInfo } from './types'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index eba9c72e41..2e58111868 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -205,7 +205,6 @@ export class OidcAuthProvider implements OAuthHandlers { }, ); } - return response; } } diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 092d22189d..66e8b0bfc5 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -148,7 +148,7 @@ export class OneLoginProvider implements OAuthHandlers { const id = profile.email.split('@')[0]; - return { ...response, backstageIdentity: { id } }; + return { ...response, backstageIdentity: { id, token: '' } }; } } diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 52cff0bdf9..17f7002d41 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -140,6 +140,30 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentityResponse; }; +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + /** + * The type of identity that this structure represents. In the frontend app + * this will currently always be 'user'. + */ + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; +}; + /** * @public */ @@ -171,20 +195,7 @@ export type BackstageIdentityResponse = { /** * A plaintext description of the identity that is encapsulated within the token. */ - identity?: { - type: 'user'; - - /** - * The entityRef of the user in the catalog. - * For example User:default/sandra - */ - userEntityRef: string; - - /** - * The user and group entities that the user claims ownership through - */ - ownershipEntityRefs: string[]; - }; + identity?: BackstageUserIdentity; }; /** diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 276b6d351c..6154eec757 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -79,6 +79,9 @@ describe('CatalogImportClient', () => { signOut: () => { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const scmIntegrationsApi = ScmIntegrations.fromConfig( diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx index 784cbfbe9e..d6132b249e 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx @@ -37,6 +37,9 @@ describe('', () => { signOut: () => { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; let apis: TestApiRegistry; diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx index 1778af73dc..498e786eb3 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx @@ -43,6 +43,9 @@ describe('', () => { signOut: () => { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; let apis: TestApiRegistry; diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index 0e9da8d8c8..6917b5abed 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -40,6 +40,9 @@ const identityApi: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const guestIdentityApi: IdentityApi = { getUserId() { @@ -54,6 +57,9 @@ const guestIdentityApi: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; describe('CatalogClientWrapper', () => { diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 09d2feb71b..fb18c1a68c 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -187,6 +187,9 @@ export const MockCostInsightsApiProvider = ({ getIdToken: jest.fn(), getUserId: jest.fn(), signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const defaultCostInsightsApi: CostInsightsApi = { diff --git a/plugins/fossa/src/api/FossaClient.test.ts b/plugins/fossa/src/api/FossaClient.test.ts index 4debb37ad6..e723a4b00a 100644 --- a/plugins/fossa/src/api/FossaClient.test.ts +++ b/plugins/fossa/src/api/FossaClient.test.ts @@ -24,20 +24,11 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; const server = setupServer(); -const identityApi: IdentityApi = { - getUserId() { - return 'jane-fonda'; - }, - getProfile() { - return { email: 'jane-fonda@spotify.com' }; - }, +const identityApi = { async getIdToken() { return Promise.resolve('fake-id-token'); }, - async signOut() { - return Promise.resolve(); - }, -}; +} as IdentityApi; describe('FossaClient', () => { setupRequestMockHandlers(server); diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts index 91395d1f0b..5721812059 100644 --- a/plugins/search/src/apis.test.ts +++ b/plugins/search/src/apis.test.ts @@ -34,6 +34,9 @@ describe('apis', () => { getUserId: jest.fn(), getProfile: jest.fn(), signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }); const client = new SearchClient({ diff --git a/plugins/sonarqube/src/api/SonarQubeClient.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts index f8fd7c2264..a3a301d77a 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -37,6 +37,9 @@ const identityApiAuthenticated: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const identityApiGuest: IdentityApi = { getUserId() { @@ -51,6 +54,9 @@ const identityApiGuest: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; describe('SonarQubeClient', () => { diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 7624f250d9..bdb2f74772 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -44,6 +44,9 @@ describe('TechDocsStorageClient', () => { getProfile: jest.fn(), getUserId: jest.fn(), signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; beforeEach(() => { From 15b98232d089f3b8b88e73bb86c7786aff8a34dd Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 30 Nov 2021 13:19:21 +0100 Subject: [PATCH 04/23] chore: drop wip comments Signed-off-by: Johan Haals --- packages/core-app-api/src/app/AppManager.tsx | 53 -------------------- 1 file changed, 53 deletions(-) diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 31e093a145..a923b4131e 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -480,56 +480,3 @@ export class AppManager implements BackstageApp { } } } - -// interface FooPropsV1 { -// foo: () => undefined; -// } - -// interface FooPropsV2 { -// foo: () => undefined; -// bar: () => undefined; -// } - -// // type FooProps = { -// // foo: () => undefined -// // } | { -// // foo: () => undefined -// // bar: () => undefined -// // } - -// interface CreateDerpOptions { -// components: { -// Foo: (props: FooPropsV1 | FooPropsV2) => JSX.Element; -// }; -// } - -// interface Derp { -// getComponents(): { -// Foo: (props: FooPropsV1) => JSX.Element; -// }; -// } - -// function createDerp(options: CreateDerpOptions): Derp { -// return { getComponents: () => options.components }; -// } - -// function CustomFoo(props: FooPropsV1) { -// return
{props.foo()}
; -// } - -// function NewCustomFoo(props: FooPropsV2) { -// return ( -//
-// {props.foo()} {props.bar()} -//
-// ); -// } - -// const derp = createDerp({ -// components: { -// Foo: NewCustomFoo, -// }, -// }); - -// const { Foo } = derp.getComponents(); -// const _foo = undefined} />; From 48e1d3bfca17449f202799df64ffb6ce75ff17fb Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 30 Nov 2021 13:24:52 +0100 Subject: [PATCH 05/23] chore: remove wip comments Signed-off-by: Johan Haals --- .../core-plugin-api/src/apis/definitions/IdentityApi.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 4b0672d745..36d43f93ff 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -16,14 +16,6 @@ import { ApiRef, createApiRef } from '../system'; import { BackstageUserIdentity, ProfileInfo } from './auth'; -/* - -- [ ] IdentityApi getProfile, make async -- [ ] BackstageIdentity (settle or remove) -- [ ] Evolution plan for utility APIs - -*/ - /** * The Identity API used to identify and get information about the signed in user. * From 8c337a480f28b4eec6e991bb6c1d0575ef035655 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 30 Nov 2021 16:25:29 +0100 Subject: [PATCH 06/23] chore: Update types and API reports Signed-off-by: Johan Haals --- packages/core-app-api/api-report.md | 5 +-- packages/core-app-api/src/app/types.ts | 2 +- packages/core-plugin-api/api-report.md | 36 +++++++++++++------ .../src/apis/definitions/IdentityApi.ts | 2 +- plugins/auth-backend/api-report.md | 24 +++++++------ plugins/auth-backend/src/providers/index.ts | 7 +++- plugins/auth-backend/src/providers/types.ts | 1 + .../permission-backend/src/service/router.ts | 4 +-- plugins/permission-node/src/policy/types.ts | 4 +-- 9 files changed, 55 insertions(+), 30 deletions(-) diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index e69ee4f325..252531f7ad 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -39,6 +39,7 @@ import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api'; import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OAuthRequestApi } from '@backstage/core-plugin-api'; @@ -635,10 +636,10 @@ export type SamlSession = { // @public export type SignInPageProps = { - onResult(result: SignInResult): void; + onSignInSuccess(identityApi: IdentityApi): void; }; -// @public +// @public @deprecated export type SignInResult = { userId: string; profile: ProfileInfo; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 0921bd2e17..a538ccd366 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -43,7 +43,7 @@ export type BootErrorPageProps = { * The outcome of signing in on the sign-in page. * * @public - * @deprecated replaced by passing the {@link IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead. + * @deprecated replaced by passing the {@link @backstage/core-plugin-api#IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead. */ export type SignInResult = { /** diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 560071e1fa..9245f039df 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -10,6 +10,7 @@ import { BackstageTheme } from '@backstage/theme'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; +import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api'; import { Observable as Observable_2 } from '@backstage/types'; import { Observer as Observer_2 } from '@backstage/types'; import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api'; @@ -236,18 +237,21 @@ export type AuthRequestOptions = { instantPopup?: boolean; }; -// @public -export type BackstageIdentity = { - id: string; - idToken: string; - token: string; -}; +// @public @deprecated (undocumented) +export type BackstageIdentity = BackstageIdentityResponse; // @public export type BackstageIdentityApi = { getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; +}; + +// @public +export type BackstageIdentityResponse = { + id: string; + token: string; + identity: BackstageUserIdentity; }; // @public @@ -264,6 +268,13 @@ export type BackstagePlugin< externalRoutes: ExternalRoutes; }; +// @public +export type BackstageUserIdentity = { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; +}; + // @public export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -531,8 +542,13 @@ export type IconComponent = ComponentType<{ // @public export type IdentityApi = { getUserId(): string; - getProfile(): ProfileInfo; getIdToken(): Promise; + getProfile(): ProfileInfo; + getProfileInfo(): Promise; + getBackstageIdentity(): Promise; + getCredentials(): Promise<{ + token?: string; + }>; signOut(): Promise; }; @@ -745,10 +761,10 @@ export enum SessionState { // @public export type SignInPageProps = { - onResult(result: SignInResult): void; + onSignInSuccess(identityApi: IdentityApi_2): void; }; -// @public +// @public @deprecated export type SignInResult = { userId: string; profile: ProfileInfo_2; diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 36d43f93ff..fe4de89106 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -26,7 +26,7 @@ export type IdentityApi = { * The ID of the signed in user. This ID is not meant to be presented to the user, but used * as an opaque string to pass on to backends or use in frontend logic. * - * @deprecated use {@link IdentityApi.getIdentity} instead. + * @deprecated use {@link IdentityApi.getBackstageIdentity} instead. */ getUserId(): string; diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 46d8b03cbb..2004490261 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -103,7 +103,7 @@ export interface AuthProviderRouteHandlers { export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; - backstageIdentity?: BackstageIdentity; + backstageIdentity?: BackstageIdentityResponse; }; // Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -116,14 +116,19 @@ export type AwsAlbProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "BackstageIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type BackstageIdentity = { +// @public +export type BackstageIdentityResponse = { id: string; - idToken?: string; - token?: string; entity?: Entity; + token: string; + identity?: BackstageUserIdentity; +}; + +// @public +export type BackstageUserIdentity = { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; }; // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -361,7 +366,7 @@ export type GoogleProviderOptions = { // @public export class IdentityClient { constructor(options: { discovery: PluginEndpointDiscovery; issuer: string }); - authenticate(token: string | undefined): Promise; + authenticate(token: string | undefined): Promise; static getBearerToken( authorizationHeader: string | undefined, ): string | undefined; @@ -551,8 +556,6 @@ export const postMessageResponse: ( response: WebMessageResponse, ) => void; -// Warning: (ae-missing-release-tag) "ProfileInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type ProfileInfo = { email?: string; @@ -637,5 +640,4 @@ export type WebMessageResponse = // src/providers/github/provider.d.ts:71:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // src/providers/github/provider.d.ts:78:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:100:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:122:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative ``` diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 5ad0b84074..5b4fbc6338 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -38,4 +38,9 @@ export type { // These types are needed for a postMessage from the login pop-up // to the frontend -export type { AuthResponse, BackstageUserIdentity, ProfileInfo } from './types'; +export type { + AuthResponse, + BackstageUserIdentity, + BackstageIdentityResponse, + ProfileInfo, +} from './types'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 17f7002d41..1d2c6f5134 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -165,6 +165,7 @@ export type BackstageUserIdentity = { }; /** + * Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider. * @public */ export type BackstageIdentityResponse = { diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index b85e7feb85..41791b8fc8 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -23,7 +23,7 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { - BackstageIdentity, + BackstageIdentityResponse, IdentityClient, } from '@backstage/plugin-auth-backend'; import { @@ -71,7 +71,7 @@ export interface RouterOptions { const handleRequest = async ( { id, resourceRef, ...request }: Identified, - user: BackstageIdentity | undefined, + user: BackstageIdentityResponse | undefined, policy: PermissionPolicy, permissionIntegrationClient: PermissionIntegrationClient, authHeader?: string, diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index 3548d051f6..d122ad8d8d 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -20,7 +20,7 @@ import { PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; -import { BackstageIdentity } from '@backstage/plugin-auth-backend'; +import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend'; /** * An authorization request to be evaluated by the {@link PermissionPolicy}. @@ -83,6 +83,6 @@ export type PolicyDecision = export interface PermissionPolicy { handle( request: PolicyAuthorizeRequest, - user?: BackstageIdentity, + user?: BackstageIdentityResponse, ): Promise; } From 0bb10226b8f618e9a932a283aeedace421ba9ebd Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 10:33:55 +0100 Subject: [PATCH 07/23] chore: move some packges around so that we don't have bad imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johan Haals Co-authored-by: Fredrik Adelöw Signed-off-by: blam --- packages/core-app-api/src/app/AppManager.tsx | 2 +- .../src/layout/SignInPage}/GuestUserIdentity.ts | 0 .../src/layout/SignInPage}/LegacyUserIdentity.ts | 0 .../core-components/src/layout/SignInPage/customProvider.tsx | 2 +- .../core-components/src/layout/SignInPage/guestProvider.tsx | 2 +- packages/core-components/src/layout/SignInPage/index.ts | 2 ++ 6 files changed, 5 insertions(+), 3 deletions(-) rename packages/{core-app-api/src/apis/implementations/IdentityApi => core-components/src/layout/SignInPage}/GuestUserIdentity.ts (100%) rename packages/{core-app-api/src/apis/implementations/IdentityApi => core-components/src/layout/SignInPage}/LegacyUserIdentity.ts (100%) diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index a923b4131e..141de0aab2 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -50,7 +50,7 @@ import { SubRouteRef, ExternalRouteRef, } from '@backstage/core-plugin-api'; -import { GuestUserIdentity } from '../apis/implementations/IdentityApi/GuestUserIdentity'; +import { GuestUserIdentity } from '@backstage/core-components'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts b/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts similarity index 100% rename from packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts rename to packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts similarity index 100% rename from packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts rename to packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index 77bde5223c..a6e46aee6a 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 '@backstage/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity'; +import { LegacyUserIdentity } from './LegacyUserIdentity'; // 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; diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index 9e0326c7df..b2370a98d5 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -20,7 +20,7 @@ import Button from '@material-ui/core/Button'; import { InfoCard } from '../InfoCard/InfoCard'; import { GridItem } from './styles'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; -import { GuestUserIdentity } from '@backstage/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity'; +import { GuestUserIdentity } from './GuestUserIdentity'; const Component: ProviderComponent = ({ onSignInSuccess }) => ( diff --git a/packages/core-components/src/layout/SignInPage/index.ts b/packages/core-components/src/layout/SignInPage/index.ts index caa8506399..176cba96a6 100644 --- a/packages/core-components/src/layout/SignInPage/index.ts +++ b/packages/core-components/src/layout/SignInPage/index.ts @@ -18,3 +18,5 @@ export type { SignInProviderConfig } from './types'; export { SignInPage } from './SignInPage'; export type { SignInPageClassKey } from './styles'; export type { CustomProviderClassKey } from './customProvider'; +export { GuestUserIdentity } from './GuestUserIdentity'; +export { LegacyUserIdentity } from './LegacyUserIdentity'; From 64c3fc492e1956396019f5e6a67cd4d2fb3f639e Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 10:55:17 +0100 Subject: [PATCH 08/23] chore: fix up api-reports and fix the export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johan Haals Co-authored-by: Fredrik Adelöw Signed-off-by: blam --- packages/core-app-api/src/app/AppManager.tsx | 4 +- packages/core-components/api-report.md | 41 +++++++++++++++++++ .../src/layout/SignInPage/UserIdentity.ts | 14 ++++++- .../src/layout/SignInPage/index.ts | 3 +- .../src/lib/oauth/OAuthAdapter.ts | 2 +- plugins/permission-node/api-report.md | 4 +- 6 files changed, 60 insertions(+), 8 deletions(-) diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 141de0aab2..21940c08d5 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -50,7 +50,7 @@ import { SubRouteRef, ExternalRouteRef, } from '@backstage/core-plugin-api'; -import { GuestUserIdentity } from '@backstage/core-components'; +import { UserIdentity } from '@backstage/core-components'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, @@ -361,7 +361,7 @@ export class AppManager implements BackstageApp { // If the app hasn't configured a sign-in page, we just continue as guest. if (!SignInPageComponent) { - this.appIdentityProxy.setTarget(new GuestUserIdentity()); + this.appIdentityProxy.setTarget(UserIdentity.createGuest()); return ( diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index c1e021391e..f237932baa 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; import { BackstageTheme } from '@backstage/theme'; +import { BackstageUserIdentity } from '@backstage/core-plugin-api'; import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button'; import { CardHeaderProps } from '@material-ui/core/CardHeader'; import { Column } from '@material-table/core'; @@ -20,6 +21,7 @@ import { CSSProperties } from 'react'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; import { IconComponent } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { LinearProgressProps } from '@material-ui/core/LinearProgress'; import { LinkProps as LinkProps_2 } from '@material-ui/core/Link'; import { LinkProps as LinkProps_3 } from 'react-router-dom'; @@ -27,6 +29,7 @@ import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Overrides } from '@material-ui/core/styles/overrides'; +import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; @@ -35,6 +38,7 @@ import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { SessionApi } from '@backstage/core-plugin-api'; import { SignInPageProps } from '@backstage/core-plugin-api'; +import { SignInResult } from '@backstage/core-plugin-api'; import { SparklinesLineProps } from 'react-sparklines'; import { SparklinesProps } from 'react-sparklines'; import { StyledComponentProps } from '@material-ui/core/styles'; @@ -2317,6 +2321,42 @@ export function useQueryParamState( // @public (undocumented) export function UserIcon(props: IconComponentProps): JSX.Element; +// Warning: (ae-missing-release-tag) "UserIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class UserIdentity implements IdentityApi { + // Warning: (ae-forgotten-export) The symbol "GuestUserIdentity" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static createGuest(): GuestUserIdentity; + // (undocumented) + static from(options: { + identity: BackstageUserIdentity; + authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; + profile?: ProfileInfo; + }): UserIdentity; + // Warning: (ae-forgotten-export) The symbol "LegacyUserIdentity" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static fromLegacy({ result }: { result: SignInResult }): LegacyUserIdentity; + // (undocumented) + getBackstageIdentity(): Promise; + // (undocumented) + getCredentials(): Promise<{ + token?: string | undefined; + }>; + // (undocumented) + getIdToken(): Promise; + // (undocumented) + getProfile(): ProfileInfo; + // (undocumented) + getProfileInfo(): Promise; + // (undocumented) + getUserId(): string; + // (undocumented) + signOut(): Promise; +} + // Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2360,4 +2400,5 @@ export type WarningPanelClassKey = // src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts // src/components/Table/Table.d.ts:20:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts // src/layout/ErrorBoundary/ErrorBoundary.d.ts:8:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts +// src/layout/SignInPage/UserIdentity.d.ts:22:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "IdentityApi" ``` diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts index daf9111d84..088f76e60e 100644 --- a/packages/core-components/src/layout/SignInPage/UserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -21,16 +21,28 @@ import { BackstageUserIdentity, BackstageIdentityApi, SessionApi, + SignInResult, } from '@backstage/core-plugin-api'; +import { GuestUserIdentity } from './GuestUserIdentity'; +import { LegacyUserIdentity } from './LegacyUserIdentity'; + export class UserIdentity implements IdentityApi { + static createGuest() { + return new GuestUserIdentity(); + } + + static fromLegacy({ result }: { result: SignInResult }) { + return LegacyUserIdentity.fromResult(result); + } + static from(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; /** * Passing a profile synchronously allows the deprecated `getProfile` method to be * called by consumers of the {@link IdentityApi}. If you do not have any consumers - * of that method than this is safe to leave out. + * of that method then this is safe to leave out. * * @deprecated Only provide this if you have plugins that call the synchronous `getProfile` method, which is also deprecated. */ diff --git a/packages/core-components/src/layout/SignInPage/index.ts b/packages/core-components/src/layout/SignInPage/index.ts index 176cba96a6..b3c7618f50 100644 --- a/packages/core-components/src/layout/SignInPage/index.ts +++ b/packages/core-components/src/layout/SignInPage/index.ts @@ -18,5 +18,4 @@ export type { SignInProviderConfig } from './types'; export { SignInPage } from './SignInPage'; export type { SignInPageClassKey } from './styles'; export type { CustomProviderClassKey } from './customProvider'; -export { GuestUserIdentity } from './GuestUserIdentity'; -export { LegacyUserIdentity } from './LegacyUserIdentity'; +export { UserIdentity } from './UserIdentity'; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 2df67462cf..bbef4790fe 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.token)) { + if (!(identity.token || identity.id)) { identity.token = await this.options.tokenIssuer.issueToken({ claims: { sub: identity.id }, }); diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 75c45a9250..2bd9c5d6a8 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -5,7 +5,7 @@ ```ts import { AuthorizeRequest } from '@backstage/plugin-permission-common'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; -import { BackstageIdentity } from '@backstage/plugin-auth-backend'; +import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend'; import { PermissionCondition } from '@backstage/plugin-permission-common'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { Router } from 'express'; @@ -98,7 +98,7 @@ export interface PermissionPolicy { // (undocumented) handle( request: PolicyAuthorizeRequest, - user?: BackstageIdentity, + user?: BackstageIdentityResponse, ): Promise; } From 29d0b45c6a660f604d5bc2fcc66ff669dacfdf3b Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 14:13:07 +0100 Subject: [PATCH 09/23] chore: fixing issue with multi signin providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johan Haals Co-authored-by: Fredrik Adelöw Signed-off-by: blam --- .../SignInPage/IdentityApiSignOutProxy.ts | 65 +++++++++++++++++++ .../src/layout/SignInPage/SignInPage.tsx | 2 +- .../src/layout/SignInPage/customProvider.tsx | 15 +++-- .../src/layout/SignInPage/providers.tsx | 17 +++-- .../src/lib/oauth/OAuthAdapter.ts | 2 +- .../src/providers/decorateWithIdentity.ts | 39 +++++++++++ .../src/providers/github/provider.test.ts | 55 ++++++++++++++-- .../src/providers/github/provider.ts | 24 ++++++- .../src/providers/google/provider.ts | 4 +- plugins/auth-backend/src/providers/types.ts | 4 +- 10 files changed, 200 insertions(+), 27 deletions(-) create mode 100644 packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts create mode 100644 plugins/auth-backend/src/providers/decorateWithIdentity.ts 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 }; From 39645e56ac32fe9dfedf5dac7a8114cf186eb35a Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 16:02:09 +0100 Subject: [PATCH 10/23] chore: reworking the auth providers to decorate the identity from the token that is returned from the different providers Co-authored-by: Johan Haals Signed-off-by: blam --- .../src/layout/SignInPage/providers.tsx | 4 +-- .../src/lib/flow/authFlowHelpers.test.ts | 15 +++++++++ .../src/lib/oauth/OAuthAdapter.test.ts | 10 ++++-- .../src/lib/oauth/OAuthAdapter.ts | 33 ++++++++++++------- plugins/auth-backend/src/lib/oauth/types.ts | 18 +++++++--- .../src/providers/atlassian/provider.ts | 5 ++- .../src/providers/aws-alb/provider.test.ts | 14 ++++++-- .../src/providers/aws-alb/provider.ts | 3 +- .../src/providers/decorateWithIdentity.ts | 2 +- .../src/providers/github/provider.ts | 22 +------------ .../src/providers/saml/provider.ts | 11 ++++++- 11 files changed, 89 insertions(+), 48 deletions(-) diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx index 2abe6b3d51..23d6c95b3c 100644 --- a/packages/core-components/src/layout/SignInPage/providers.tsx +++ b/packages/core-components/src/layout/SignInPage/providers.tsx @@ -30,7 +30,7 @@ import { import { commonProvider } from './commonProvider'; import { guestProvider } from './guestProvider'; import { customProvider } from './customProvider'; -import { IdentityApiProxy } from './IdentityApiProxy'; +import { IdentityApiSignOutProxy } from './IdentityApiSignOutProxy'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; @@ -92,7 +92,7 @@ export const useSignInProviders = ( const handleWrappedResult = useCallback( (identityApi: IdentityApi) => { onSignInSuccess( - IdentityApiProxy.from({ + IdentityApiSignOutProxy.from({ identityApi, signOut: async () => { localStorage.removeItem(PROVIDER_STORAGE_KEY); diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts index 8b4b95d59b..07c8196dd2 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -52,6 +52,11 @@ describe('oauth helpers', () => { backstageIdentity: { id: 'a', token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, }, }, }; @@ -107,6 +112,11 @@ describe('oauth helpers', () => { backstageIdentity: { id: 'a', token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, }, }, }; @@ -149,6 +159,11 @@ describe('oauth helpers', () => { backstageIdentity: { id: 'a', token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, }, }, }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 5e52e60f46..91711f67ad 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -31,7 +31,8 @@ const mockResponseData = { }, backstageIdentity: { id: 'foo', - token: '', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', }, }; @@ -218,7 +219,12 @@ describe('OAuthAdapter', () => { ...mockResponseData, backstageIdentity: { id: mockResponseData.backstageIdentity.id, - token: 'my-id-token', + token: mockResponseData.backstageIdentity.token, + identity: { + ownershipEntityRefs: ['user:default/jimmymarkum'], + type: 'user', + userEntityRef: 'jimmymarkum', + }, }, }); }); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index d6a825f3fd..970fe95b55 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -37,6 +37,7 @@ import { OAuthRefreshRequest, OAuthState, } from './types'; +import { decorateWithIdentity } from '../../providers/decorateWithIdentity'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -150,12 +151,12 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, refreshToken); } - await this.populateIdentity(response.backstageIdentity); + const identity = await this.populateIdentity(response.backstageIdentity); // post message back to popup if successful return postMessageResponse(res, appOrigin, { type: 'authorization_response', - response, + response: { ...response, backstageIdentity: identity }, }); } catch (error) { const { name, message } = isError(error) @@ -209,7 +210,9 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { forwardReq as OAuthRefreshRequest, ); - await this.populateIdentity(response.backstageIdentity); + const backstageIdentity = await this.populateIdentity( + response.backstageIdentity, + ); if ( response.providerInfo.refreshToken && @@ -218,7 +221,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, response.providerInfo.refreshToken); } - res.status(200).json(response); + res.status(200).json({ ...response, backstageIdentity }); } catch (error) { throw new AuthenticationError('Refresh failed', error); } @@ -228,18 +231,24 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { * If the response from the OAuth provider includes a Backstage identity, we * make sure it's populated with all the information we can derive from the user ID. */ - private async populateIdentity(identity?: BackstageIdentityResponse) { + private async populateIdentity( + identity?: Omit, + ): Promise { if (!identity) { - return; + return undefined; } - if (!identity.token) { - identity.token = await this.options.tokenIssuer.issueToken({ - claims: { sub: identity.id }, - }); - } else if (!identity.token && identity.token) { - identity.token = identity.token; + if (identity.token) { + return decorateWithIdentity(identity); } + + const token = await this.options.tokenIssuer.issueToken({ + claims: { sub: identity.id }, + }); + + console.log(token); + + return decorateWithIdentity({ ...identity, token }); } private setNonceCookie = (res: express.Response, nonce: string) => { diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 7912dd16a5..f1ff9e763d 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -16,8 +16,11 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; -import { AuthResponse, RedirectInfo } from '../../providers/types'; - +import { + AuthResponse, + RedirectInfo, + BackstageIdentityResponse, +} from '../../providers/types'; /** * Common options for passport.js-based OAuth providers */ @@ -47,7 +50,12 @@ export type OAuthResult = { refreshToken?: string; }; -export type OAuthResponse = AuthResponse; +export type OAuthResponse = Omit< + AuthResponse, + 'backstageIdentity' +> & { + backstageIdentity?: Omit; +}; export type OAuthProviderInfo = { /** @@ -108,7 +116,7 @@ export interface OAuthHandlers { * @param {express.Request} req */ handler(req: express.Request): Promise<{ - response: AuthResponse; + response: OAuthResponse; refreshToken?: string; }>; @@ -117,7 +125,7 @@ export interface OAuthHandlers { * @param {string} refreshToken * @param {string} scope */ - refresh?(req: OAuthRefreshRequest): Promise>; + refresh?(req: OAuthRefreshRequest): Promise; /** * (Optional) Sign out of the auth provider. diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index e19f29a3a4..99e1ebd2d8 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -45,6 +45,7 @@ import express from 'express'; import { TokenIssuer } from '../../identity'; import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; +import { decorateWithIdentity } from '../decorateWithIdentity'; export type AtlassianAuthProviderOptions = OAuthProviderOptions & { scopes: string; @@ -136,7 +137,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( + const resolverResponse = await this.signInResolver( { result, profile, @@ -147,6 +148,8 @@ export class AtlassianAuthProvider implements OAuthHandlers { logger: this.logger, }, ); + + response.backstageIdentity = decorateWithIdentity(resolverResponse); } return response; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index 3f72004d48..9e4aed2030 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -122,7 +122,11 @@ describe('AwsAlbAuthProvider', () => { profile: makeProfileInfo(fullProfile), }), signInResolver: async () => { - return { id: 'user.name', token: 'TOKEN' }; + return { + id: 'user.name', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + }; }, }); @@ -133,7 +137,13 @@ describe('AwsAlbAuthProvider', () => { expect(mockResponse.json).toHaveBeenCalledWith({ backstageIdentity: { id: 'user.name', - token: 'TOKEN', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + identity: { + ownershipEntityRefs: ['user:default/jimmymarkum'], + type: 'user', + userEntityRef: 'jimmymarkum', + }, }, profile: { displayName: 'User Name', diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index b0b9070d50..027d5190de 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -32,6 +32,7 @@ import { CatalogIdentityClient } from '../../lib/catalog'; import { Profile as PassportProfile } from 'passport'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; +import { decorateWithIdentity } from '../decorateWithIdentity'; export const ALB_JWT_HEADER = 'x-amzn-oidc-data'; export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken'; @@ -198,7 +199,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { accessToken: result.accessToken, expiresInSeconds: result.expiresInSeconds, }, - backstageIdentity, + backstageIdentity: decorateWithIdentity(backstageIdentity), profile, }; } diff --git a/plugins/auth-backend/src/providers/decorateWithIdentity.ts b/plugins/auth-backend/src/providers/decorateWithIdentity.ts index dab1480db4..76fa97bd81 100644 --- a/plugins/auth-backend/src/providers/decorateWithIdentity.ts +++ b/plugins/auth-backend/src/providers/decorateWithIdentity.ts @@ -33,7 +33,7 @@ export function decorateWithIdentity( identity: { type: 'user', userEntityRef: sub, - ownershipEntityRefs: ent ?? [sub], + ownershipEntityRefs: ent ?? [], }, }; } diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index eb15afa4ab..e9d019f4ac 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -32,7 +32,6 @@ import { AuthHandler, SignInResolver, StateEncoder, - BackstageIdentityResponse, } from '../types'; import { OAuthAdapter, @@ -46,6 +45,7 @@ import { } from '../../lib/oauth'; import { CatalogIdentityClient } from '../../lib/catalog'; import { TokenIssuer } from '../../identity'; +import { decorateWithIdentity } from '../decorateWithIdentity'; type PrivateInfo = { refreshToken?: string; @@ -168,25 +168,6 @@ export class GithubAuthProvider implements OAuthHandlers { }; if (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, @@ -198,7 +179,6 @@ export class GithubAuthProvider implements OAuthHandlers { logger: this.logger, }, ); - response.backstageIdentity = decorateWithIdentity(signInResolverResult); } diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 3ab00ce401..f760c9e8ee 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -36,8 +36,13 @@ import { import { postMessageResponse } from '../../lib/flow'; import { TokenIssuer } from '../../identity/types'; import { isError } from '@backstage/errors'; +<<<<<<< HEAD import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; +import { decorateWithIdentity } from '../decorateWithIdentity'; +======= +import { decorateWithIdentity } from '../decorateWithIdentity'; +>>>>>>> chore: reworking the auth providers to decorate the identity from the token that is returned from the different providers /** @public */ export type SamlAuthResult = { @@ -105,7 +110,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { }; if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( + const signInResponse = await this.signInResolver( { result, profile, @@ -116,8 +121,12 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { logger: this.logger, }, ); + + response.backstageIdentity = decorateWithIdentity(signInResponse); } + + return postMessageResponse(res, this.appUrl, { type: 'authorization_response', response, From b3ac79d7c29f9eaecd3a2f78d9b22d57302af87d Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 16:19:24 +0100 Subject: [PATCH 11/23] chore: updated the api-report for auth backend. probably need to make this a bit better. Signed-off-by: blam --- plugins/auth-backend/api-report.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 2004490261..51d280988a 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -121,7 +121,7 @@ export type BackstageIdentityResponse = { id: string; entity?: Entity; token: string; - identity?: BackstageUserIdentity; + identity: BackstageUserIdentity; }; // @public @@ -453,7 +453,7 @@ export interface OAuthHandlers { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' handler(req: express.Request): Promise<{ - response: AuthResponse; + response: OAuthResponse; refreshToken?: string; }>; logout?(): Promise; @@ -461,7 +461,7 @@ export interface OAuthHandlers { // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' - refresh?(req: OAuthRefreshRequest): Promise>; + refresh?(req: OAuthRefreshRequest): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -499,7 +499,12 @@ export type OAuthRefreshRequest = express.Request<{}> & { // Warning: (ae-missing-release-tag) "OAuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type OAuthResponse = AuthResponse; +export type OAuthResponse = Omit< + AuthResponse, + 'backstageIdentity' +> & { + backstageIdentity?: Omit; +}; // Warning: (ae-missing-release-tag) "OAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // From 11a90d6a79e82ea2028eabc6f1ffc4d081fe1b72 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 16:29:14 +0100 Subject: [PATCH 12/23] chore: revert some of the handling Signed-off-by: blam --- plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts | 2 -- plugins/auth-backend/src/providers/google/provider.ts | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 970fe95b55..b6c6473031 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -246,8 +246,6 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { claims: { sub: identity.id }, }); - console.log(token); - return decorateWithIdentity({ ...identity, token }); } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 293f738dfd..d85fc2de2d 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) { - const signInResolverResult = await this.signInResolver( + response.backstageIdentity = await this.signInResolver( { result, profile, @@ -170,8 +170,6 @@ export class GoogleAuthProvider implements OAuthHandlers { logger: this.logger, }, ); - - console.log(signInResolverResult); } return response; From 3b39323f2631862c690d5c1235395f9d389227a2 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 2 Dec 2021 11:04:33 +0100 Subject: [PATCH 13/23] chore: revert some of the provider changes are they are handled in the OAuthAdapter now Signed-off-by: blam --- .../src/providers/atlassian/provider.ts | 5 +- .../src/providers/github/provider.test.ts | 55 +++---------------- .../src/providers/github/provider.ts | 4 +- 3 files changed, 9 insertions(+), 55 deletions(-) diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 99e1ebd2d8..e19f29a3a4 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -45,7 +45,6 @@ import express from 'express'; import { TokenIssuer } from '../../identity'; import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; -import { decorateWithIdentity } from '../decorateWithIdentity'; export type AtlassianAuthProviderOptions = OAuthProviderOptions & { scopes: string; @@ -137,7 +136,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - const resolverResponse = await this.signInResolver( + response.backstageIdentity = await this.signInResolver( { result, profile, @@ -148,8 +147,6 @@ export class AtlassianAuthProvider implements OAuthHandlers { logger: this.logger, }, ); - - response.backstageIdentity = decorateWithIdentity(resolverResponse); } return response; diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 02c3367914..e418ab22c2 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -41,12 +41,7 @@ describe('GithubAuthProvider', () => { const tokenIssuer: TokenIssuer = { listPublicKeys: jest.fn(), async issueToken(params) { - const tokenContents = { - sub: params.claims.sub, - ent: params.claims.ent ?? [], - }; - - return `eyblob.${btoa(JSON.stringify(tokenContents))}.eyblob`; + return `token-for-${params.claims.sub}`; }, }; const catalogIdentityClient = { @@ -98,13 +93,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'jimmymarkum', - token: - 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', - identity: { - ownershipEntityRefs: ['user:default/jimmymarkum'], - type: 'user', - userEntityRef: 'jimmymarkum', - }, + token: 'token-for-jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -149,13 +138,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'jimmymarkum', - token: - 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/jimmymarkum'], - userEntityRef: 'jimmymarkum', - }, + token: 'token-for-jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -198,13 +181,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'jimmymarkum', - token: - 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/jimmymarkum'], - userEntityRef: 'jimmymarkum', - }, + token: 'token-for-jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -247,13 +224,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'daveboyle', - token: - 'eyblob.eyJzdWIiOiJkYXZlYm95bGUiLCJlbnQiOlsidXNlcjpkZWZhdWx0L2RhdmVib3lsZSJdfQ==.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/daveboyle'], - userEntityRef: 'daveboyle', - }, + token: 'token-for-daveboyle', }, providerInfo: { accessToken: @@ -297,13 +268,7 @@ describe('GithubAuthProvider', () => { response: { backstageIdentity: { id: 'ipd12039', - token: - 'eyblob.eyJzdWIiOiJpcGQxMjAzOSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvaXBkMTIwMzkiXX0=.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/ipd12039'], - userEntityRef: 'ipd12039', - }, + token: 'token-for-ipd12039', }, providerInfo: { accessToken: 'a.b.c', @@ -356,13 +321,7 @@ describe('GithubAuthProvider', () => { expect(response).toEqual({ backstageIdentity: { id: 'mockuser', - token: - 'eyblob.eyJzdWIiOiJtb2NrdXNlciIsImVudCI6WyJ1c2VyOmRlZmF1bHQvbW9ja3VzZXIiXX0=.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/mockuser'], - userEntityRef: 'mockuser', - }, + token: 'token-for-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 e9d019f4ac..c7e82cc5ff 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -45,7 +45,6 @@ import { } from '../../lib/oauth'; import { CatalogIdentityClient } from '../../lib/catalog'; import { TokenIssuer } from '../../identity'; -import { decorateWithIdentity } from '../decorateWithIdentity'; type PrivateInfo = { refreshToken?: string; @@ -168,7 +167,7 @@ export class GithubAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - const signInResolverResult = await this.signInResolver( + response.backstageIdentity = await this.signInResolver( { result, profile, @@ -179,7 +178,6 @@ export class GithubAuthProvider implements OAuthHandlers { logger: this.logger, }, ); - response.backstageIdentity = decorateWithIdentity(signInResolverResult); } return response; From acbb4cedd4c9b40f7392f6403641eda9aac74f7e Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 10:08:04 +0100 Subject: [PATCH 14/23] chore: fix derpy merge conflicts Signed-off-by: blam --- plugins/auth-backend/src/providers/saml/provider.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index f760c9e8ee..d0f7791aaa 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -36,13 +36,9 @@ import { import { postMessageResponse } from '../../lib/flow'; import { TokenIssuer } from '../../identity/types'; import { isError } from '@backstage/errors'; -<<<<<<< HEAD import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; import { decorateWithIdentity } from '../decorateWithIdentity'; -======= -import { decorateWithIdentity } from '../decorateWithIdentity'; ->>>>>>> chore: reworking the auth providers to decorate the identity from the token that is returned from the different providers /** @public */ export type SamlAuthResult = { @@ -125,8 +121,6 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { response.backstageIdentity = decorateWithIdentity(signInResponse); } - - return postMessageResponse(res, this.appUrl, { type: 'authorization_response', response, From 388fd9bb7f1ec165ed938ab7abe314f97d404e32 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 10:21:01 +0100 Subject: [PATCH 15/23] chore: fixing some code review comments Signed-off-by: blam --- .../core-components/src/layout/SignInPage/SignInPage.tsx | 2 +- .../core-components/src/layout/SignInPage/UserIdentity.ts | 4 ++-- .../src/layout/SignInPage/auth0Provider.tsx | 4 ++-- .../src/layout/SignInPage/commonProvider.tsx | 4 ++-- .../src/layout/SignInPage/customProvider.tsx | 8 +++----- packages/core-plugin-api/src/apis/definitions/auth.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 2 ++ plugins/auth-backend/src/providers/oidc/provider.ts | 2 ++ 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index 3a068decd5..304e427f33 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -136,7 +136,7 @@ export const SingleSignInPage = ({ const profile = await authApi.getProfile(); onSignInSuccess( - UserIdentity.from({ + UserIdentity.create({ identity: identityResponse.identity, authApi, profile, diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts index 088f76e60e..1461c48a49 100644 --- a/packages/core-components/src/layout/SignInPage/UserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -32,11 +32,11 @@ export class UserIdentity implements IdentityApi { return new GuestUserIdentity(); } - static fromLegacy({ result }: { result: SignInResult }) { + static fromLegacy(result: SignInResult) { return LegacyUserIdentity.fromResult(result); } - static from(options: { + static create(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; /** diff --git a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx index 8e2728a95e..739c3709a9 100644 --- a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx +++ b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx @@ -46,7 +46,7 @@ const Component: ProviderComponent = ({ onSignInSuccess }) => { const profile = await auth0AuthApi.getProfile(); onSignInSuccess( - UserIdentity.from({ + UserIdentity.create({ identity: identityResponse.identity, authApi: auth0AuthApi, profile, @@ -85,7 +85,7 @@ const loader: ProviderLoader = async apis => { } const profile = await auth0AuthApi.getProfile(); - return UserIdentity.from({ + return UserIdentity.create({ identity: identityResponse.identity, authApi: auth0AuthApi, profile, diff --git a/packages/core-components/src/layout/SignInPage/commonProvider.tsx b/packages/core-components/src/layout/SignInPage/commonProvider.tsx index 48dd7df62c..8f426a3907 100644 --- a/packages/core-components/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/commonProvider.tsx @@ -48,7 +48,7 @@ const Component: ProviderComponent = ({ config, onSignInSuccess }) => { const profile = await authApi.getProfile(); onSignInSuccess( - UserIdentity.from({ + UserIdentity.create({ identity: identityResponse.identity, profile, authApi, @@ -89,7 +89,7 @@ const loader: ProviderLoader = async (apis, apiRef) => { const profile = await authApi.getProfile(); - return UserIdentity.from({ + return UserIdentity.create({ identity: identityResponse.identity, profile, authApi, diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index 9863234a06..13ba906079 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -72,11 +72,9 @@ const Component: ProviderComponent = ({ onSignInSuccess }) => { const handleResult = ({ userId }: Data) => { onSignInSuccess( UserIdentity.fromLegacy({ - result: { - userId, - profile: { - email: `${userId}@example.com`, - }, + userId, + profile: { + email: `${userId}@example.com`, }, }), ); diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 8da7fbe6fd..e093a6b29b 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -188,7 +188,7 @@ export type BackstageUserIdentity = { }; /** - * A (user id, token) pair. + * Token and Identity response, with the users claims in the Identity. * * @public */ diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 5b4fbc6338..34c2ff06c2 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -44,3 +44,5 @@ export type { BackstageIdentityResponse, ProfileInfo, } from './types'; + +export { decorateWithIdentity } from './decorateWithIdentity'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 2e58111868..158af6f830 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -205,6 +205,8 @@ export class OidcAuthProvider implements OAuthHandlers { }, ); } + + return response; } } From a76dacda24fe414f550e06d9b8efa84f1542e74a Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 10:24:01 +0100 Subject: [PATCH 16/23] chore: fix up the api reports Signed-off-by: blam --- packages/core-components/api-report.md | 14 +++++++------- plugins/auth-backend/api-report.md | 7 +++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index f237932baa..31008624db 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -2325,20 +2325,20 @@ export function UserIcon(props: IconComponentProps): JSX.Element; // // @public (undocumented) export class UserIdentity implements IdentityApi { - // Warning: (ae-forgotten-export) The symbol "GuestUserIdentity" needs to be exported by the entry point index.d.ts - // // (undocumented) - static createGuest(): GuestUserIdentity; - // (undocumented) - static from(options: { + static create(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; profile?: ProfileInfo; }): UserIdentity; + // Warning: (ae-forgotten-export) The symbol "GuestUserIdentity" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static createGuest(): GuestUserIdentity; // Warning: (ae-forgotten-export) The symbol "LegacyUserIdentity" needs to be exported by the entry point index.d.ts // // (undocumented) - static fromLegacy({ result }: { result: SignInResult }): LegacyUserIdentity; + static fromLegacy(result: SignInResult): LegacyUserIdentity; // (undocumented) getBackstageIdentity(): Promise; // (undocumented) @@ -2400,5 +2400,5 @@ export type WarningPanelClassKey = // src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts // src/components/Table/Table.d.ts:20:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts // src/layout/ErrorBoundary/ErrorBoundary.d.ts:8:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts -// src/layout/SignInPage/UserIdentity.d.ts:22:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "IdentityApi" +// src/layout/SignInPage/UserIdentity.d.ts:20:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "IdentityApi" ``` diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 51d280988a..dffb0414db 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -288,6 +288,13 @@ export const createSamlProvider: ( options?: SamlProviderOptions | undefined, ) => AuthProviderFactory; +// Warning: (ae-missing-release-tag) "decorateWithIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function decorateWithIdentity( + signInResolverResponse: Omit, +): BackstageIdentityResponse; + // Warning: (ae-missing-release-tag) "factories" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) From e0f5814037453d49e74a1804c7a7eae37cdf4345 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 10:33:05 +0100 Subject: [PATCH 17/23] chore: more code review comments Signed-off-by: blam --- .../core-components/src/layout/SignInPage/LegacyUserIdentity.ts | 2 +- plugins/auth-backend/src/providers/decorateWithIdentity.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts index ff6d38c860..e28f94072e 100644 --- a/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts @@ -65,7 +65,7 @@ export class LegacyUserIdentity implements IdentityApi { return { type: 'user', userEntityRef: sub, - ownershipEntityRefs: ent ?? [sub], + ownershipEntityRefs: ent ?? [], }; } diff --git a/plugins/auth-backend/src/providers/decorateWithIdentity.ts b/plugins/auth-backend/src/providers/decorateWithIdentity.ts index 76fa97bd81..0e0756f8a8 100644 --- a/plugins/auth-backend/src/providers/decorateWithIdentity.ts +++ b/plugins/auth-backend/src/providers/decorateWithIdentity.ts @@ -22,6 +22,8 @@ function parseJwtPayload(token: string) { } /** + * @public + * * Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token */ export function decorateWithIdentity( From e847694fbbd3e5d4180056133c1dc75ad61c9265 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 14:10:00 +0100 Subject: [PATCH 18/23] chore: rebuild api-report now I fixed some issues Signed-off-by: blam --- plugins/auth-backend/api-report.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index dffb0414db..7d776986a7 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -288,8 +288,6 @@ export const createSamlProvider: ( options?: SamlProviderOptions | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "decorateWithIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function decorateWithIdentity( signInResolverResponse: Omit, From 73c73b71d15f1a25ee07cdbbc7a6467cf1a7f445 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 11:55:42 +0100 Subject: [PATCH 19/23] auth: simplify types and reintroduce deprecated BackstageIdentity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- packages/core-plugin-api/api-report.md | 2 +- .../src/apis/definitions/auth.ts | 2 ++ plugins/auth-backend/api-report.md | 30 ++++++++++++------- .../src/lib/oauth/OAuthAdapter.ts | 3 +- plugins/auth-backend/src/lib/oauth/types.ts | 14 ++++----- plugins/auth-backend/src/providers/index.ts | 2 ++ plugins/auth-backend/src/providers/types.ts | 23 +++++++++----- 7 files changed, 50 insertions(+), 26 deletions(-) diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 9245f039df..ad7d0b80a9 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -237,7 +237,7 @@ export type AuthRequestOptions = { instantPopup?: boolean; }; -// @public @deprecated (undocumented) +// @public @deprecated export type BackstageIdentity = BackstageIdentityResponse; // @public diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index e093a6b29b..37308ec29b 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -212,6 +212,8 @@ export type BackstageIdentityResponse = { }; /** + * The old exported symbol for {@link BackstageIdentityResponse}. + * * @public * @deprecated use {@link BackstageIdentityResponse} instead. */ diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 7d776986a7..41c017ef6b 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -116,13 +116,24 @@ export type AwsAlbProviderOptions = { }; }; +// @public @deprecated +export type BackstageIdentity = BackstageSignInResult; + // @public -export type BackstageIdentityResponse = { - id: string; - entity?: Entity; - token: string; +export interface BackstageIdentityResponse extends BackstageSignInResult { identity: BackstageUserIdentity; -}; +} + +// Warning: (ae-missing-release-tag) "BackstageSignInResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface BackstageSignInResult { + // @deprecated + entity?: Entity; + // @deprecated + id: string; + token: string; +} // @public export type BackstageUserIdentity = { @@ -504,11 +515,10 @@ export type OAuthRefreshRequest = express.Request<{}> & { // Warning: (ae-missing-release-tag) "OAuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type OAuthResponse = Omit< - AuthResponse, - 'backstageIdentity' -> & { - backstageIdentity?: Omit; +export type OAuthResponse = { + profile: ProfileInfo; + providerInfo: OAuthProviderInfo; + backstageIdentity?: BackstageSignInResult; }; // Warning: (ae-missing-release-tag) "OAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index b6c6473031..bb0bbf9dbd 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -21,6 +21,7 @@ import { AuthProviderRouteHandlers, AuthProviderConfig, BackstageIdentityResponse, + BackstageSignInResult, } from '../../providers/types'; import { AuthenticationError, @@ -232,7 +233,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { * make sure it's populated with all the information we can derive from the user ID. */ private async populateIdentity( - identity?: Omit, + identity?: BackstageSignInResult, ): Promise { if (!identity) { return undefined; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index f1ff9e763d..48e3ffe6ea 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -17,10 +17,11 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; import { - AuthResponse, RedirectInfo, - BackstageIdentityResponse, + BackstageSignInResult, + ProfileInfo, } from '../../providers/types'; + /** * Common options for passport.js-based OAuth providers */ @@ -50,11 +51,10 @@ export type OAuthResult = { refreshToken?: string; }; -export type OAuthResponse = Omit< - AuthResponse, - 'backstageIdentity' -> & { - backstageIdentity?: Omit; +export type OAuthResponse = { + profile: ProfileInfo; + providerInfo: OAuthProviderInfo; + backstageIdentity?: BackstageSignInResult; }; export type OAuthProviderInfo = { diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 34c2ff06c2..9fe77fe593 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -40,8 +40,10 @@ export type { // to the frontend export type { AuthResponse, + BackstageIdentity, BackstageUserIdentity, BackstageIdentityResponse, + BackstageSignInResult, ProfileInfo, } from './types'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 919cbe42b4..9f93e6fa21 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -164,11 +164,7 @@ export type BackstageUserIdentity = { ownershipEntityRefs: string[]; }; -/** - * Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider. - * @public - */ -export type BackstageIdentityResponse = { +export interface BackstageSignInResult { /** * An opaque ID that uniquely identifies the user within Backstage. * @@ -192,12 +188,25 @@ export type BackstageIdentityResponse = { * The token used to authenticate the user within Backstage. */ token: string; +} +/** + * The old exported symbol for {@link BackstageSignInResult}. + * @public + * @deprecated Use the `BackstageSignInResult` type instead. + */ +export type BackstageIdentity = BackstageSignInResult; + +/** + * Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider. + * @public + */ +export interface BackstageIdentityResponse extends BackstageSignInResult { /** * A plaintext description of the identity that is encapsulated within the token. */ identity: BackstageUserIdentity; -}; +} /** * Used to display login information to user, i.e. sidebar popup. @@ -242,7 +251,7 @@ export type SignInResolver = ( catalogIdentityClient: CatalogIdentityClient; logger: Logger; }, -) => Promise>; +) => Promise; export type AuthHandlerResult = { profile: ProfileInfo }; From a036b65c2f1a16553e9413b5f97629d0a610bce9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 14:53:16 +0100 Subject: [PATCH 20/23] changesets: added changesets for sign-in and identity changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .changeset/chilly-files-greet.md | 9 ++++++++ .changeset/cold-ties-pay.md | 17 +++++++++++++++ .changeset/fast-trainers-unite.md | 35 +++++++++++++++++++++++++++++++ .changeset/lovely-goats-eat.md | 8 +++++++ .changeset/odd-ears-pump.md | 11 ++++++++++ 5 files changed, 80 insertions(+) create mode 100644 .changeset/chilly-files-greet.md create mode 100644 .changeset/cold-ties-pay.md create mode 100644 .changeset/fast-trainers-unite.md create mode 100644 .changeset/lovely-goats-eat.md create mode 100644 .changeset/odd-ears-pump.md diff --git a/.changeset/chilly-files-greet.md b/.changeset/chilly-files-greet.md new file mode 100644 index 0000000000..d8cd71f1be --- /dev/null +++ b/.changeset/chilly-files-greet.md @@ -0,0 +1,9 @@ +--- +'@backstage/core-components': minor +--- + +The `SignInPage` has been updated to use the new `onSignInSuccess` callback that was introduced in the same release. While existing code will usually continue to work, it is technically a breaking change because of the dependency on `SignInProps` from the `@backstage/core-plugin-api`. For more information on this change and instructions on how to migrate existing code, see the [`@backstage/core-app-api` CHANGELOG.md](https://github.com/backstage/backstage/blob/master/packages/core-app-api/CHANGELOG.md). + +Added a new `UserIdentity` class which helps create implementations of the `IdentityApi`. It provides a couple of static factory methods such as the most relevant `create`, and `createGuest` to create an `IdentityApi` for a guest user. + +Also provides a deprecated `fromLegacy` method to create an `IdentityApi` from the now deprecated `SignInResult`. This method will be removed in the future when `SignInResult` is also removed. diff --git a/.changeset/cold-ties-pay.md b/.changeset/cold-ties-pay.md new file mode 100644 index 0000000000..3062761444 --- /dev/null +++ b/.changeset/cold-ties-pay.md @@ -0,0 +1,17 @@ +--- +'@backstage/core-plugin-api': minor +--- + +The `IdentityApi` has received several updates. The `getUserId`, `getProfile`, and `getIdToken` have all been deprecated. + +The replacement for `getUserId` is the new `getBackstageIdentity` method, which provides both the `userEntityRef` as well as the `ownershipEntityRefs` that are used to resolve ownership. Existing usage of the user ID would typically be using a fixed entity kind and namespace, for example `` `user:default/${identityApi.getUserId()}` ``, this kind of usage should now instead use the `userEntityRef` directly. + +The replacement for `getProfile` is the new async `getProfileInfo`. + +The replacement for `getIdToken` is the new `getCredentials` method, which provides an optional token to the caller like before, but it is now wrapped in an object for forwards compatibility. + +The deprecated `idToken` field of the `BackstageIdentity` type has been removed, leaving only the new `token` field, which should be used instead. The `BackstageIdentity` also received a new `identity` field, which is a decoded version of the information within the token. Furthermore the `BackstageIdentity` has been renamed to `BackstageIdentityResponse`, with the old name being deprecated. + +We expect most of the breaking changes in this update to have low impact since the `IdentityApi` implementation is provided by the app, but it is likely that some tests need to be updated. + +Another breaking change is that the `SignInPage` props have been updated, and the `SignInResult` type is now deprecated. This is unlikely to have any impact on the usage of this package, but it is an important change that you can find more information about in the [`@backstage/core-app-api` CHANGELOG.md](https://github.com/backstage/backstage/blob/master/packages/core-app-api/CHANGELOG.md). diff --git a/.changeset/fast-trainers-unite.md b/.changeset/fast-trainers-unite.md new file mode 100644 index 0000000000..48ba0eca55 --- /dev/null +++ b/.changeset/fast-trainers-unite.md @@ -0,0 +1,35 @@ +--- +'@backstage/core-app-api': minor +--- + +**BREAKING CHANGE** + +The app `SignInPage` component has been updated to switch out the `onResult` callback for a new `onSignInSuccess` callback. This is an immediate breaking change without any deprecation period, as it was deemed to be the way of making this change that had the lowest impact. + +The new `onSignInSuccess` callback directly accepts an implementation of an `IdentityApi`, rather than a `SignInResult`. The `SignInPage` from `@backstage/core-component` has been updated to fit this new API, and as long as you pass on `props` directly you should not see any breakage. + +However, if you implement your own custom `SignInPage`, then this will be a breaking change and you need to migrate over to using the new callback. While doing so you can take advantage of the `UserIdentity.fromLegacy` helper from `@backstage/core-components` to make the migration simpler by still using the `SignInResult` type. This helper is also deprecated though and is only provided for immediate migration. Long-term it will be necessary to build the `IdentityApi` using for example `UserIdentity.create` instead. + +The following is an example of how you can migrate existing usage immediately using `UserIdentity.fromLegacy`: + +```ts +onResult(signInResult); +// becomes +onSignInSuccess(UserIdentity.fromLegacy(signInResult)); +``` + +The following is an example of how implement the new `onSignInSuccess` callback of the `SignInPage` using `UserIdentity.create`: + +```ts +const identityResponse = await authApi.getBackstageIdentity(); +// Profile is optional and will be removed, but allows the +// synchronous getProfile method of the IdentityApi to be used. +const profile = await authApi.getProfile(); +onSignInSuccess( + UserIdentity.create({ + identity: identityResponse.identity, + authApi, + profile, + }), +); +``` diff --git a/.changeset/lovely-goats-eat.md b/.changeset/lovely-goats-eat.md new file mode 100644 index 0000000000..0fea320358 --- /dev/null +++ b/.changeset/lovely-goats-eat.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-permission-node': patch +--- + +Updated to use the new `BackstageIdentityResponse` type from `@backstage/plugin-auth-backend`. + +The `BackstageIdentityResponse` type is backwards compatible with the `BackstageIdentity`, and provides an additional `identity` field with the claims of the user. diff --git a/.changeset/odd-ears-pump.md b/.changeset/odd-ears-pump.md new file mode 100644 index 0000000000..453b0d083d --- /dev/null +++ b/.changeset/odd-ears-pump.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +**BREAKING CHANGE** The `idToken` field of `BackstageIdentity` has been removed, with the `token` taking its place. This means you may need to update existing `signIn.resolver` implementations to return an `token` rather than an `idToken`. This also applies to custom auth providers. + +The `BackstageIdentity` type has been deprecated and will be removed in the future. Taking its place is the new `BackstageSignInResult` type with the same shape. + +This change also introduces the new `BackstageIdentityResponse` that mirrors the type with the same name from `@backstage/core-plugin-api`. The `BackstageIdentityResponse` type is different from the `BackstageSignInResult` in that it also has a `identity` field which is of type `BackstageUserIdentity` and is a decoded version of the information within the token. + +When implementing a custom auth provider that is not based on the `OAuthAdapter` you may need to convert `BackstageSignInResult` into a `BackstageIdentityResponse`, this can be done using the new `prepareBackstageIdentityResponse` function. From 1154ec0017643d24fd40b0e301872659681255e5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 17:13:26 +0100 Subject: [PATCH 21/23] auth-backend: decorateWithIdentity -> prepareBackstageIdentityResponse + API report fixes Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/api-report.md | 18 +++++++----------- .../auth-backend/src/lib/oauth/OAuthAdapter.ts | 6 +++--- plugins/auth-backend/src/lib/oauth/types.ts | 5 +++++ .../src/providers/aws-alb/provider.ts | 4 ++-- plugins/auth-backend/src/providers/index.ts | 2 +- ....ts => prepareBackstageIdentityResponse.ts} | 18 +++++++++++------- .../src/providers/saml/provider.ts | 5 +++-- plugins/auth-backend/src/providers/types.ts | 8 ++++++++ 8 files changed, 40 insertions(+), 26 deletions(-) rename plugins/auth-backend/src/providers/{decorateWithIdentity.ts => prepareBackstageIdentityResponse.ts} (74%) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 41c017ef6b..a4076208ae 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -124,9 +124,7 @@ export interface BackstageIdentityResponse extends BackstageSignInResult { identity: BackstageUserIdentity; } -// Warning: (ae-missing-release-tag) "BackstageSignInResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface BackstageSignInResult { // @deprecated entity?: Entity; @@ -299,11 +297,6 @@ export const createSamlProvider: ( options?: SamlProviderOptions | undefined, ) => AuthProviderFactory; -// @public -export function decorateWithIdentity( - signInResolverResponse: Omit, -): BackstageIdentityResponse; - // Warning: (ae-missing-release-tag) "factories" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -512,9 +505,7 @@ export type OAuthRefreshRequest = express.Request<{}> & { refreshToken: string; }; -// Warning: (ae-missing-release-tag) "OAuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type OAuthResponse = { profile: ProfileInfo; providerInfo: OAuthProviderInfo; @@ -576,6 +567,11 @@ export const postMessageResponse: ( response: WebMessageResponse, ) => void; +// @public +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult, +): BackstageIdentityResponse; + // @public export type ProfileInfo = { email?: string; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index bb0bbf9dbd..eb3f7efa42 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -38,7 +38,7 @@ import { OAuthRefreshRequest, OAuthState, } from './types'; -import { decorateWithIdentity } from '../../providers/decorateWithIdentity'; +import { prepareBackstageIdentityResponse } from '../../providers/prepareBackstageIdentityResponse'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -240,14 +240,14 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { } if (identity.token) { - return decorateWithIdentity(identity); + return prepareBackstageIdentityResponse(identity); } const token = await this.options.tokenIssuer.issueToken({ claims: { sub: identity.id }, }); - return decorateWithIdentity({ ...identity, token }); + return prepareBackstageIdentityResponse({ ...identity, token }); } private setNonceCookie = (res: express.Response, nonce: string) => { diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 48e3ffe6ea..cd1439b399 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -51,6 +51,11 @@ export type OAuthResult = { refreshToken?: string; }; +/** + * The expected response from an OAuth flow. + * + * @public + */ export type OAuthResponse = { profile: ProfileInfo; providerInfo: OAuthProviderInfo; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 027d5190de..114bc9a204 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -32,7 +32,7 @@ import { CatalogIdentityClient } from '../../lib/catalog'; import { Profile as PassportProfile } from 'passport'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; -import { decorateWithIdentity } from '../decorateWithIdentity'; +import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; export const ALB_JWT_HEADER = 'x-amzn-oidc-data'; export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken'; @@ -199,7 +199,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { accessToken: result.accessToken, expiresInSeconds: result.expiresInSeconds, }, - backstageIdentity: decorateWithIdentity(backstageIdentity), + backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity), profile, }; } diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 9fe77fe593..4ecbb98fd2 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -47,4 +47,4 @@ export type { ProfileInfo, } from './types'; -export { decorateWithIdentity } from './decorateWithIdentity'; +export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse'; diff --git a/plugins/auth-backend/src/providers/decorateWithIdentity.ts b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts similarity index 74% rename from plugins/auth-backend/src/providers/decorateWithIdentity.ts rename to plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts index 0e0756f8a8..31df7a4cef 100644 --- a/plugins/auth-backend/src/providers/decorateWithIdentity.ts +++ b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BackstageIdentityResponse } from './types'; +import { BackstageIdentityResponse, BackstageSignInResult } from './types'; function parseJwtPayload(token: string) { const [_header, payload, _signature] = token.split('.'); @@ -22,16 +22,20 @@ function parseJwtPayload(token: string) { } /** - * @public - * * Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token + * + * @public */ -export function decorateWithIdentity( - signInResolverResponse: Omit, +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult, ): BackstageIdentityResponse { - const { sub, ent } = parseJwtPayload(signInResolverResponse.token); + const { sub, ent } = parseJwtPayload(result.token); return { - ...signInResolverResponse, + ...{ + // TODO: idToken is for backwards compatibility and can be removed in the future + idToken: result.token, + ...result, + }, identity: { type: 'user', userEntityRef: sub, diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index d0f7791aaa..8a4afd1fa6 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -38,7 +38,7 @@ import { TokenIssuer } from '../../identity/types'; import { isError } from '@backstage/errors'; import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; -import { decorateWithIdentity } from '../decorateWithIdentity'; +import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; /** @public */ export type SamlAuthResult = { @@ -118,7 +118,8 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { }, ); - response.backstageIdentity = decorateWithIdentity(signInResponse); + response.backstageIdentity = + prepareBackstageIdentityResponse(signInResponse); } return postMessageResponse(res, this.appUrl, { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 9f93e6fa21..dafa52163c 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -164,6 +164,14 @@ export type BackstageUserIdentity = { ownershipEntityRefs: string[]; }; +/** + * A representation of a successful Backstage sign-in. + * + * Compared to the {@link BackstageIdentityResponse} this type omits + * the decoded identity information embedded in the token. + * + * @public + */ export interface BackstageSignInResult { /** * An opaque ID that uniquely identifies the user within Backstage. From 18b7b795c102a3026abf0e472e0949e3aa2fc787 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 17:14:18 +0100 Subject: [PATCH 22/23] core-components: docs + fix API report warnings Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- packages/core-components/api-report.md | 18 ++------- .../src/layout/SignInPage/UserIdentity.ts | 39 ++++++++++++++++--- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 31008624db..d57f618b57 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -2321,24 +2321,15 @@ export function useQueryParamState( // @public (undocumented) export function UserIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "UserIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class UserIdentity implements IdentityApi { - // (undocumented) static create(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; profile?: ProfileInfo; - }): UserIdentity; - // Warning: (ae-forgotten-export) The symbol "GuestUserIdentity" needs to be exported by the entry point index.d.ts - // - // (undocumented) - static createGuest(): GuestUserIdentity; - // Warning: (ae-forgotten-export) The symbol "LegacyUserIdentity" needs to be exported by the entry point index.d.ts - // - // (undocumented) - static fromLegacy(result: SignInResult): LegacyUserIdentity; + }): IdentityApi; + static createGuest(): IdentityApi; + static fromLegacy(result: SignInResult): IdentityApi; // (undocumented) getBackstageIdentity(): Promise; // (undocumented) @@ -2400,5 +2391,4 @@ export type WarningPanelClassKey = // src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts // src/components/Table/Table.d.ts:20:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts // src/layout/ErrorBoundary/ErrorBoundary.d.ts:8:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts -// src/layout/SignInPage/UserIdentity.d.ts:20:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "IdentityApi" ``` diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts index 1461c48a49..f749711d3c 100644 --- a/packages/core-components/src/layout/SignInPage/UserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -27,27 +27,49 @@ import { import { GuestUserIdentity } from './GuestUserIdentity'; import { LegacyUserIdentity } from './LegacyUserIdentity'; +/** + * An implementation of the IdentityApi that is constructed using + * various backstage user identity representations. + * + * @public + */ export class UserIdentity implements IdentityApi { - static createGuest() { + /** + * Creates a new IdentityApi that acts as a Guest User. + * + * @public + */ + static createGuest(): IdentityApi { return new GuestUserIdentity(); } - static fromLegacy(result: SignInResult) { + /** + * Creates a new IdentityApi using a legacy SignInResult object. + * + * @public + */ + static fromLegacy(result: SignInResult): IdentityApi { return LegacyUserIdentity.fromResult(result); } + /** + * Creates a new IdentityApi implementation using a user identity + * and an auth API that will be used to request backstage tokens. + * + * @public + */ static create(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; /** * Passing a profile synchronously allows the deprecated `getProfile` method to be - * called by consumers of the {@link IdentityApi}. If you do not have any consumers - * of that method then this is safe to leave out. + * called by consumers of the {@link @backstage/core-plugin-api#IdentityApi}. If you + * do not have any consumers of that method then this is safe to leave out. * * @deprecated Only provide this if you have plugins that call the synchronous `getProfile` method, which is also deprecated. */ profile?: ProfileInfo; - }) { + }): IdentityApi { return new UserIdentity(options.identity, options.authApi, options.profile); } @@ -59,6 +81,7 @@ export class UserIdentity implements IdentityApi { private readonly profile?: ProfileInfo, ) {} + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getUserId} */ getUserId(): string { const ref = this.identity.userEntityRef; const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref); @@ -69,11 +92,13 @@ export class UserIdentity implements IdentityApi { return match[3]; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getIdToken} */ async getIdToken(): Promise { const identity = await this.authApi.getBackstageIdentity(); return identity!.token; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfile} */ getProfile(): ProfileInfo { if (!this.profile) { throw new Error( @@ -83,20 +108,24 @@ export class UserIdentity implements IdentityApi { return this.profile; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfileInfo} */ async getProfileInfo(): Promise { const profile = await this.authApi.getProfile(); return profile!; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getBackstageIdentity} */ async getBackstageIdentity(): Promise { return this.identity; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getCredentials} */ async getCredentials(): Promise<{ token?: string | undefined }> { const identity = await this.authApi.getBackstageIdentity(); return { token: identity!.token }; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.signOut} */ async signOut(): Promise { return this.authApi.signOut(); } From 11b7c39fb6c19a76d0b8d663013aadca05fa0814 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 7 Dec 2021 10:52:27 +0100 Subject: [PATCH 23/23] chore: added caching for successful profile retrieval Signed-off-by: blam --- .../layout/SignInPage/UserIdentity.test.ts | 83 +++++++++++++++++++ .../src/layout/SignInPage/UserIdentity.ts | 14 +++- .../src/lib/oauth/OAuthAdapter.test.ts | 1 + .../src/providers/aws-alb/provider.test.ts | 2 + 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 packages/core-components/src/layout/SignInPage/UserIdentity.test.ts diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.test.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.test.ts new file mode 100644 index 0000000000..6b136ed65d --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.test.ts @@ -0,0 +1,83 @@ +/* + * 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, ProfileInfo } from '@backstage/core-plugin-api'; +import { UserIdentity } from './UserIdentity'; + +describe('UserIdentity', () => { + it('should cache a successful response from the AuthApi for getProfile', async () => { + const mockIdentity: BackstageUserIdentity = { + type: 'user', + userEntityRef: 'user:default/blam', + ownershipEntityRefs: [], + }; + + const mockProfileInfo: ProfileInfo = { + displayName: 'Blam', + email: 'blob@boop.com', + }; + + const mockAuthApi: any = { + getProfile: jest.fn().mockResolvedValue(mockProfileInfo), + }; + + const userIdentity = UserIdentity.create({ + authApi: mockAuthApi, + identity: mockIdentity, + }); + + await userIdentity.getProfileInfo(); + await userIdentity.getProfileInfo(); + + const response = await userIdentity.getProfileInfo(); + + expect(mockAuthApi.getProfile).toHaveBeenCalledTimes(1); + + expect(response).toEqual(mockProfileInfo); + }); + + it('should not cache failures for the AuthApi for getProfile', async () => { + const mockIdentity: BackstageUserIdentity = { + type: 'user', + userEntityRef: 'user:default/blam', + ownershipEntityRefs: [], + }; + + const mockProfileInfo: ProfileInfo = { + displayName: 'Blam', + email: 'blob@boop.com', + }; + + const mockAuthApi: any = { + getProfile: jest + .fn() + .mockRejectedValueOnce(new Error('boop')) + .mockResolvedValueOnce(mockProfileInfo), + }; + + const userIdentity = UserIdentity.create({ + authApi: mockAuthApi, + identity: mockIdentity, + }); + + await expect(() => userIdentity.getProfileInfo()).rejects.toThrow('boop'); + const response = await userIdentity.getProfileInfo(); + + expect(mockAuthApi.getProfile).toHaveBeenCalledTimes(2); + + expect(response).toEqual(mockProfileInfo); + }); +}); diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts index f749711d3c..7781c79154 100644 --- a/packages/core-components/src/layout/SignInPage/UserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -34,6 +34,7 @@ import { LegacyUserIdentity } from './LegacyUserIdentity'; * @public */ export class UserIdentity implements IdentityApi { + private profilePromise?: Promise; /** * Creates a new IdentityApi that acts as a Guest User. * @@ -110,8 +111,17 @@ export class UserIdentity implements IdentityApi { /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfileInfo} */ async getProfileInfo(): Promise { - const profile = await this.authApi.getProfile(); - return profile!; + if (this.profilePromise) { + return await this.profilePromise; + } + + try { + this.profilePromise = this.authApi.getProfile() as Promise; + return await this.profilePromise; + } catch (ex) { + this.profilePromise = undefined; + throw ex; + } } /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getBackstageIdentity} */ diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 91711f67ad..92c76b04b7 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -220,6 +220,7 @@ describe('OAuthAdapter', () => { backstageIdentity: { id: mockResponseData.backstageIdentity.id, token: mockResponseData.backstageIdentity.token, + idToken: mockResponseData.backstageIdentity.token, identity: { ownershipEntityRefs: ['user:default/jimmymarkum'], type: 'user', diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index 9e4aed2030..048128f942 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -139,6 +139,8 @@ describe('AwsAlbAuthProvider', () => { id: 'user.name', token: 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + idToken: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', identity: { ownershipEntityRefs: ['user:default/jimmymarkum'], type: 'user',