From 761ff980f1e750972a63240a0c56383ee19ef32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 26 Dec 2021 21:57:07 +0100 Subject: [PATCH] use only a sign-in resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/auth/google/gcp-iap-auth.md | 180 ++++++++---------- packages/core-app-api/api-report.md | 37 ++++ .../auth/gcp-iap/GcpIapIdentity.ts | 74 +++++++ .../implementations/auth/gcp-iap/index.ts | 18 ++ .../auth/gcp-iap/types.test.ts | 50 +++++ .../implementations/auth/gcp-iap/types.ts | 63 ++++++ .../src/apis/implementations/auth/index.ts | 1 + plugins/auth-backend/api-report.md | 38 +++- plugins/auth-backend/package.json | 9 +- .../src/providers/aws-alb/index.ts | 1 + .../src/providers/aws-alb/provider.test.ts | 6 +- .../src/providers/aws-alb/provider.ts | 7 +- .../src/providers/gcp-iap/helpers.test.ts | 124 ++++++++++++ .../src/providers/gcp-iap/helpers.ts | 82 ++++++++ .../src/providers/gcp-iap/index.ts | 8 +- .../src/providers/gcp-iap/provider.test.ts | 136 +++++-------- .../src/providers/gcp-iap/provider.ts | 171 ++++++++--------- .../src/providers/gcp-iap/types.ts | 96 ++++++++++ plugins/auth-backend/src/providers/index.ts | 1 + .../prepareBackstageIdentityResponse.ts | 4 +- plugins/auth-backend/src/providers/types.ts | 49 +++-- 21 files changed, 838 insertions(+), 317 deletions(-) create mode 100644 packages/core-app-api/src/apis/implementations/auth/gcp-iap/GcpIapIdentity.ts create mode 100644 packages/core-app-api/src/apis/implementations/auth/gcp-iap/index.ts create mode 100644 packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.test.ts create mode 100644 packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.ts create mode 100644 plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts create mode 100644 plugins/auth-backend/src/providers/gcp-iap/helpers.ts create mode 100644 plugins/auth-backend/src/providers/gcp-iap/types.ts diff --git a/docs/auth/google/gcp-iap-auth.md b/docs/auth/google/gcp-iap-auth.md index 836abd4623..3c2cf68872 100644 --- a/docs/auth/google/gcp-iap-auth.md +++ b/docs/auth/google/gcp-iap-auth.md @@ -1,6 +1,6 @@ --- id: provider -title: Google Identity Aware Proxy Provider +title: Google Identity-Aware Proxy Provider sidebar_label: Google IAP # prettier-ignore description: Adding Google Identity-Aware Proxy as an authentication provider in Backstage @@ -12,103 +12,76 @@ Backstage allows offloading the responsibility of authenticating users to the Google HTTPS Load Balancer & [IAP](https://cloud.google.com/iap), leveraging the authentication support on the latter. -This tutorial shows how to use authentication on an ALB sitting in front of +This tutorial shows how to use authentication on an IAP sitting in front of Backstage. It is assumed an IAP is already serving traffic in front of a Backstage instance configured to serve the frontend app from the backend. -## Infrastructure setup +## Frontend Changes -## Backstage changes - -### Frontend - -The Backstage App needs a `SignInPage` to be configured. When using IAP Proxy +The Backstage app needs a `SignInPage` to be configured. When using IAP Proxy authentication Backstage will only be loaded once the user has successfully authenticated; we won't need to display a sign-in page as such, however we will -need to create a dummy `SignInPage` component that can decode and refresh the -token. +need to create a dummy `SignInPage` component that can fetch the token and other +information from the backend. -- edit `packages/app/src/App.tsx` -- import the following two additional definitions from `@backstage/core`: - `useApi`, `configApiRef`; these will be used to check whether Backstage is - running locally or behind an ALB -- add the following definition just before the app is created - (`const app = createApp`): +Create a sign-in page component in your app, for example in +`packages/app/src/components/SignInPage.tsx`. -```ts -const refreshToken = async ({ props, discoveryApiConfig, config }) => { - const baseUrl = await discoveryApiConfig.getBaseUrl('auth'); - const shouldAuth = !!config.getOptionalConfig('auth.providers.gcp-iap'); +```tsx +import { GcpIapIdentity } from '@backstage/core-app-api'; +import { ErrorPanel, Progress } from '@backstage/core-components'; +import { + discoveryApiRef, + fetchApiRef, + SignInPageProps, + useApi, +} from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import React from 'react'; +import { useAsync } from 'react-use'; - if (!shouldAuth) { - props.onResult({ - userId: 'guest', - profile: { - email: 'guest@example.com', - displayName: 'Guest', - picture: '', - }, - }); - return; - } +export const GcpIapSignInPage = (props: SignInPageProps) => { + const discovery = useApi(discoveryApiRef); + const { fetch } = useApi(fetchApiRef); - try { - const request = await fetch(`${baseUrl}/gcp-iap/refresh`, { - headers: { - 'x-requested-with': 'XMLHttpRequest', - }, + const { loading, error } = useAsync(async () => { + const base = await discovery.getBaseUrl('auth'); + const response = await fetch(`${base}/gcp-iap/refresh`, { + headers: { 'x-requested-with': 'XMLHttpRequest' }, credentials: 'include', }); - const data = await request.json(); + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + props.onSignInSuccess(GcpIapIdentity.fromResponse(await response.json())); + }, []); - props.onResult({ - userId: data.backstageIdentity.id ?? 'nouser@ms.at', - profile: data.profile ?? 'nouser@ms.at', - }); - } catch (e) { - props.onResult({ - userId: 'guest', - profile: { - email: 'guest@example.com', - displayName: 'Guest', - picture: '', - }, - }); - } -}; - -const DummySignInComponent: any = (props: any) => { - try { - const config = useApi(configApiRef); - const discoveryApiConfig = useApi(discoveryApiRef); - refreshToken({ props, discoveryApiConfig, config }); - return
; - } catch (err) { - return
{err.message}
; - } + if (loading) return ; + if (error) return ; + return null; }; ``` -### Backend +Pass this sign in page to your `createApp` function, in +`packages/app/src/App.tsx`. -When using ALB auth it is not possible to leverage the built-in auth config -discovery mechanism implemented in the app created by default; bespoke logic -needs to be implemented. +```diff ++import { GcpIapSignInPage } from './components/SignInPage'; -- Replace the content of `packages/backend/plugin/auth.ts` with the below + const app = createApp({ + components: { ++ SignInPage: GcpIapSignInPage +``` + +## Backend Changes + +- Add a `providerFactories` entry to the router in + `packages/backend/plugin/auth.ts`. ```ts -// imports are relative - as this was tested out in repo directly -import { createGcpIAPProvider } from './../providers/gcp-iap/provider'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; -import { - createRouter, - AuthResponse, - AuthProviderFactoryOptions, -} from '@backstage/plugin-auth-backend'; +import { createGcpIapProvider } from '@backstage/plugin-auth-backend'; export default async function createPlugin({ logger, @@ -116,48 +89,49 @@ export default async function createPlugin({ config, discovery, }: PluginEnvironment): Promise { - const identityResolver = (payload: any): Promise> => { - return Promise.resolve({ - providerInfo: {}, - profile: { - email: payload.email, - displayName: payload.name, - picture: payload.picture, - }, - backstageIdentity: { - id: payload.email, - }, - }); - }; return await createRouter({ logger, config, database, discovery, providerFactories: { - 'gcp-iap': (options: AuthProviderFactoryOptions) => { - return createGcpIAPProvider({ ...options, identityResolver })({ - ...options, - identityResolver, - }); - }, + 'gcp-iap': createGcpIapProvider({ + // Replace the auth handler if you want to customize the returned user + // profile info (can be left out; default implementation shown below + // which only returns the email. + async authHandler({ iapToken }) { + return { profile: { email: iapToken.email } }; + }, + signIn: { + // You need to supply an identity resolver, that takes the profile + // and the IAP token and produces the Backstage token with the + // relevant user info. + async resolver({ profile, result: { iapToken } }, ctx) { + // Somehow compute the Backstage token claims, just some dummy code + // shown here, but you may want to query your LDAP server, or + // GSuite or similar, based on the IAP token sub/email claims + const id = `user:default/${iapToken.email.split('@')[0]}`; + const fullEnt = ['group:default/team-name']; + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id, ent: fullEnt }, + }); + return { id, token }; + }, + }, + }), }, }); } ``` -### Configuration +## Configuration -Use the following `auth` configuration when running Backstage on AWS: +Use the following `auth` configuration: ```yaml auth: providers: gcp-iap: - audience: '/projects/0123456/global/backendServices/1242345678765434567' + audience: + '/projects//global/backendServices/' ``` - -## Conclusion - -Once it's deployed, after going through the AAD authentication flow, Backstage -should display the AAD user details. diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index f94979f975..ece34959de 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -21,7 +21,9 @@ import { AuthProviderInfo } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; import { BackstageIdentity } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; +import { BackstageIdentityResponse } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { BackstageUserIdentity } from '@backstage/core-plugin-api'; import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; @@ -388,6 +390,41 @@ export type FlatRoutesProps = { children: ReactNode; }; +// @public +export class GcpIapIdentity implements IdentityApi { + constructor(session: GcpIapSession); + static fromResponse(data: unknown): GcpIapIdentity; + // (undocumented) + getBackstageIdentity(): Promise; + // (undocumented) + getCredentials(): Promise<{ + token?: string | undefined; + }>; + // (undocumented) + getIdToken(): Promise; + // (undocumented) + getProfile(): ProfileInfo; + // (undocumented) + getProfileInfo(): Promise; + // (undocumented) + getUserId(): string; + // (undocumented) + signOut(): Promise; +} + +// @public +export type GcpIapSession = { + providerInfo: { + iapToken: { + sub: string; + email: string; + [key: string]: unknown; + }; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; + // @public export class GithubAuth implements OAuthApi, SessionApi { // (undocumented) diff --git a/packages/core-app-api/src/apis/implementations/auth/gcp-iap/GcpIapIdentity.ts b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/GcpIapIdentity.ts new file mode 100644 index 0000000000..0df14e1d99 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/GcpIapIdentity.ts @@ -0,0 +1,74 @@ +/* + * 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'; +import { GcpIapSession, gcpIapSessionSchema } from './types'; + +/** + * An identity API based on a Google Identity-Aware Proxy auth response. + * + * @public + */ +export class GcpIapIdentity implements IdentityApi { + /** + * Creates an identity instance based on the auth-backend API response. + * + * @param data - The raw JSON response from the auth-backend. + */ + static fromResponse(data: unknown): GcpIapIdentity { + const parsed = gcpIapSessionSchema.parse(data); + return new GcpIapIdentity(parsed); + } + + constructor(private readonly session: GcpIapSession) {} + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getUserId} */ + getUserId(): string { + return this.session.backstageIdentity.id; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getIdToken} */ + async getIdToken(): Promise { + return this.session.backstageIdentity.token; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfile} */ + getProfile(): ProfileInfo { + return this.session.profile; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfileInfo} */ + async getProfileInfo(): Promise { + return this.session.profile; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getBackstageIdentity} */ + async getBackstageIdentity(): Promise { + return this.session.backstageIdentity.identity; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getCredentials} */ + async getCredentials(): Promise<{ token?: string | undefined }> { + return { token: this.session.backstageIdentity.token }; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.signOut} */ + async signOut(): Promise {} +} diff --git a/packages/core-app-api/src/apis/implementations/auth/gcp-iap/index.ts b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/index.ts new file mode 100644 index 0000000000..8133061f20 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { GcpIapIdentity } from './GcpIapIdentity'; +export type { GcpIapSession } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.test.ts b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.test.ts new file mode 100644 index 0000000000..cb185103e2 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { TypeOf } from 'zod'; +import { GcpIapSession, gcpIapSessionSchema } from './types'; + +describe('types', () => { + const responseData: GcpIapSession = { + providerInfo: { + iapToken: { + sub: 's', + email: 'e', + other: 7, + }, + }, + profile: { + email: 'e', + displayName: 'd', + picture: 'p', + }, + backstageIdentity: { + id: 'i', + token: 't', + identity: { + type: 'user', + userEntityRef: 'ue', + ownershipEntityRefs: ['oe'], + }, + }, + }; + + it('has a compatible schema type', () => { + function f(_b: TypeOf) {} + f(responseData); // no tsc errors + expect(gcpIapSessionSchema.parse(responseData)).toEqual(responseData); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.ts b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.ts new file mode 100644 index 0000000000..98c728785d --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/gcp-iap/types.ts @@ -0,0 +1,63 @@ +/* + * 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 { + BackstageIdentityResponse, + ProfileInfo, +} from '@backstage/core-plugin-api'; +import { z } from 'zod'; + +export const gcpIapSessionSchema = z.object({ + providerInfo: z.object({ + iapToken: z + .object({ + sub: z.string(), + email: z.string(), + }) + .catchall(z.unknown()), + }), + profile: z.object({ + email: z.string().optional(), + displayName: z.string().optional(), + picture: z.string().optional(), + }), + backstageIdentity: z.object({ + id: z.string(), + token: z.string(), + identity: z.object({ + type: z.literal('user'), + userEntityRef: z.string(), + ownershipEntityRefs: z.array(z.string()), + }), + }), +}); + +/** + * Session information for Google Identity-Aware Proxy auth. + * + * @public + */ +export type GcpIapSession = { + providerInfo: { + iapToken: { + sub: string; + email: string; + [key: string]: unknown; + }; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index 50333f07a0..c53c9b7c76 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -25,4 +25,5 @@ export * from './microsoft'; export * from './onelogin'; export * from './bitbucket'; export * from './atlassian'; +export * from './gcp-iap'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types'; diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index af46d874df..ab8bd39a94 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -9,6 +9,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; +import { JsonValue } from '@backstage/types'; import { JSONWebKey } from 'jose'; import { Logger as Logger_2 } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -59,8 +60,8 @@ export type Auth0ProviderOptions = { }; // @public -export type AuthHandler = ( - input: AuthResult, +export type AuthHandler = ( + input: TAuthResult, ) => Promise; // @public @@ -249,6 +250,11 @@ export const createBitbucketProvider: ( options?: BitbucketProviderOptions | undefined, ) => AuthProviderFactory; +// @public +export function createGcpIapProvider( + options: GcpIapProviderOptions, +): AuthProviderFactory; + // Warning: (ae-missing-release-tag) "createGithubProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -335,6 +341,26 @@ export const encodeState: (state: OAuthState) => string; // @public (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; +// @public +export type GcpIapProviderOptions = { + authHandler?: AuthHandler; + signIn: { + resolver: SignInResolver; + }; +}; + +// @public +export type GcpIapResult = { + iapToken: GcpIapTokenInfo; +}; + +// @public +export type GcpIapTokenInfo = { + sub: string; + email: string; + [key: string]: JsonValue; +}; + // Warning: (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getEntityClaims" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -661,14 +687,14 @@ export type SamlProviderOptions = { }; // @public -export type SignInInfo = { +export type SignInInfo = { profile: ProfileInfo; - result: AuthResult; + result: TAuthResult; }; // @public -export type SignInResolver = ( - info: SignInInfo, +export type SignInResolver = ( + info: SignInInfo, context: { tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index bdc75f40e5..fa1fcd69cb 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -36,6 +36,7 @@ "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", "@backstage/test-utils": "^0.2.1", + "@backstage/types": "^0.1.1", "@google-cloud/firestore": "^4.15.1", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", @@ -45,11 +46,8 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-session": "^1.17.1", -<<<<<<< HEAD -======= "fs-extra": "9.1.0", ->>>>>>> 4d5fd11228 (first minor cleanups) - "google-auth-library": "^7.2.0", + "google-auth-library": "^7.6.1", "helmet": "^4.0.0", "jose": "^1.27.1", "jwt-decode": "^3.1.0", @@ -87,7 +85,8 @@ "@types/passport-saml": "^1.1.3", "@types/passport-strategy": "^0.2.35", "@types/xml2js": "^0.4.7", - "msw": "^0.35.0" + "msw": "^0.35.0", + "supertest": "^6.1.3" }, "files": [ "dist", diff --git a/plugins/auth-backend/src/providers/aws-alb/index.ts b/plugins/auth-backend/src/providers/aws-alb/index.ts index 5f63c514a4..a8130b3575 100644 --- a/plugins/auth-backend/src/providers/aws-alb/index.ts +++ b/plugins/auth-backend/src/providers/aws-alb/index.ts @@ -13,5 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { createAwsAlbProvider } from './provider'; export type { AwsAlbProviderOptions } from './provider'; 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 048128f942..fd5fe28505 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -18,7 +18,7 @@ import express from 'express'; import { JWT } from 'jose'; import { - ALB_ACCESSTOKEN_HEADER, + ALB_ACCESS_TOKEN_HEADER, ALB_JWT_HEADER, AwsAlbAuthProvider, } from './provider'; @@ -80,7 +80,7 @@ describe('AwsAlbAuthProvider', () => { header: jest.fn(name => { if (name === ALB_JWT_HEADER) { return mockJwt; - } else if (name === ALB_ACCESSTOKEN_HEADER) { + } else if (name === ALB_ACCESS_TOKEN_HEADER) { return mockAccessToken; } return undefined; @@ -88,7 +88,7 @@ describe('AwsAlbAuthProvider', () => { } as unknown as express.Request; const mockRequestWithoutJwt = { header: jest.fn(name => { - if (name === ALB_ACCESSTOKEN_HEADER) { + if (name === ALB_ACCESS_TOKEN_HEADER) { return mockAccessToken; } return undefined; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 114bc9a204..4f05cd7827 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { AuthHandler, AuthProviderFactory, @@ -35,7 +36,7 @@ import { AuthenticationError } from '@backstage/errors'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; export const ALB_JWT_HEADER = 'x-amzn-oidc-data'; -export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken'; +export const ALB_ACCESS_TOKEN_HEADER = 'x-amzn-oidc-accesstoken'; type Options = { region: string; @@ -134,7 +135,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { private async getResult(req: express.Request): Promise { const jwt = req.header(ALB_JWT_HEADER); - const accessToken = req.header(ALB_ACCESSTOKEN_HEADER); + const accessToken = req.header(ALB_ACCESS_TOKEN_HEADER); if (jwt === undefined) { throw new AuthenticationError( @@ -144,7 +145,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { if (accessToken === undefined) { throw new AuthenticationError( - `Missing ALB OIDC header: ${ALB_ACCESSTOKEN_HEADER}`, + `Missing ALB OIDC header: ${ALB_ACCESS_TOKEN_HEADER}`, ); } diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts new file mode 100644 index 0000000000..5063090442 --- /dev/null +++ b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts @@ -0,0 +1,124 @@ +/* + * 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 { ConflictError } from '@backstage/errors'; +import { OAuth2Client } from 'google-auth-library'; +import { createTokenValidator, parseRequestToken } from './helpers'; + +const validJwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('helpers', () => { + describe('createTokenValidator', () => { + it('runs the happy path', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => ({ sub: 's', email: 'e@mail.com' }), + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(validJwt)).resolves.toMatchObject({ + sub: 's', + email: 'e@mail.com', + }); + }); + + it('throws if the client throws', async () => { + const mockClient = { + getIapPublicKeys: async () => { + throw new TypeError('bam'); + }, + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(validJwt)).rejects.toThrowError(TypeError); + }); + }); + + describe('parseRequestToken', () => { + it('runs the happy path', async () => { + await expect( + parseRequestToken( + validJwt, + async () => ({ sub: 's', email: 'e@mail.com' } as any), + ), + ).resolves.toMatchObject({ + iapToken: { + sub: 's', + email: 'e@mail.com', + }, + }); + }); + + it('rejects bad tokens', async () => { + await expect( + parseRequestToken(7, undefined as any), + ).rejects.toMatchObject({ + name: 'AuthenticationError', + message: 'Missing Google IAP header: x-goog-iap-jwt-assertion', + }); + await expect( + parseRequestToken(undefined, undefined as any), + ).rejects.toMatchObject({ + name: 'AuthenticationError', + message: 'Missing Google IAP header: x-goog-iap-jwt-assertion', + }); + await expect( + parseRequestToken('', undefined as any), + ).rejects.toMatchObject({ + name: 'AuthenticationError', + message: 'Missing Google IAP header: x-goog-iap-jwt-assertion', + }); + }); + + it('translates validator errors', async () => { + await expect( + parseRequestToken(validJwt, async () => { + throw new ConflictError('Ouch'); + }), + ).rejects.toMatchObject({ + name: 'AuthenticationError', + message: 'Google IAP token verification failed, ConflictError: Ouch', + }); + }); + + it('rejects bad token payloads', async () => { + await expect( + parseRequestToken(validJwt, async () => undefined as any), + ).rejects.toMatchObject({ + name: 'AuthenticationError', + message: 'Google IAP token had no payload', + }); + + await expect( + parseRequestToken(validJwt, async () => ({ sub: 'a' } as any)), + ).rejects.toMatchObject({ + name: 'AuthenticationError', + message: 'Google IAP token payload had no sub or email claim', + }); + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts new file mode 100644 index 0000000000..0bc4c34ecc --- /dev/null +++ b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts @@ -0,0 +1,82 @@ +/* + * 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 { AuthenticationError } from '@backstage/errors'; +import { OAuth2Client, TokenPayload } from 'google-auth-library'; +import { AuthHandler } from '../types'; +import { GcpIapResult, IAP_JWT_HEADER } from './types'; + +export function createTokenValidator( + audience: string, + mockClient?: OAuth2Client, +): (token: string) => Promise { + return async function tokenValidator(token) { + const client = mockClient ?? new OAuth2Client(); + + const response = await client.getIapPublicKeys(); + const ticket = await client.verifySignedJwtWithCertsAsync( + token, + response.pubkeys, + audience, + ['https://cloud.google.com/iap'], + ); + + const payload = ticket.getPayload(); + if (!payload) { + throw new TypeError('No payload'); + } + + return payload; + }; +} + +export async function parseRequestToken( + jwtToken: unknown, + tokenValidator: (token: string) => Promise, +): Promise { + if (typeof jwtToken !== 'string' || !jwtToken) { + throw new AuthenticationError( + `Missing Google IAP header: ${IAP_JWT_HEADER}`, + ); + } + + let payload: TokenPayload; + try { + payload = await tokenValidator(jwtToken); + } catch (e) { + throw new AuthenticationError(`Google IAP token verification failed, ${e}`); + } + + if (!payload) { + throw new AuthenticationError('Google IAP token had no payload'); + } else if (!payload.sub || !payload.email) { + throw new AuthenticationError( + 'Google IAP token payload had no sub or email claim', + ); + } + + return { + iapToken: { + ...payload, + sub: payload.sub, + email: payload.email, + }, + }; +} + +export const defaultAuthHandler: AuthHandler = async ({ + iapToken, +}) => ({ profile: { email: iapToken.email } }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/index.ts b/plugins/auth-backend/src/providers/gcp-iap/index.ts index 634eb37a4a..fe5d178fcc 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/index.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/index.ts @@ -14,5 +14,9 @@ * limitations under the License. */ -export { createGcpIAPProvider } from './provider'; -export type { GcpIAPProviderOptions } from './provider'; +export { createGcpIapProvider } from './provider'; +export type { + GcpIapProviderOptions, + GcpIapResult, + GcpIapTokenInfo, +} from './types'; diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts index d2a70c043c..bb1ed5c79b 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts @@ -16,107 +16,61 @@ import { getVoidLogger } from '@backstage/backend-common'; import express from 'express'; -import { AuthResponse } from '../types'; -import { GcpIAPProvider } from './provider'; - -jest.mock('google-auth-library'); - -const identityResolutionCallbackMock = async (): Promise> => { - return { - backstageIdentity: { - id: 'foo', - idToken: '', - }, - profile: { - displayName: 'Foo Bar', - }, - providerInfo: {}, - }; -}; - -const identityResolutionCallbackRejectedMock = async (): Promise< - AuthResponse -> => { - throw new Error('failed'); -}; +import request from 'supertest'; +import { GcpIapProvider } from './provider'; beforeEach(() => { jest.clearAllMocks(); }); -describe('GcpIAPProvider', () => { - const catalogApi = { - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - addLocation: jest.fn(), - removeLocationById: jest.fn(), - getEntities: jest.fn(), - getOriginLocationByEntity: jest.fn(), - getLocationByEntity: jest.fn(), - getLocationById: jest.fn(), - removeEntityByUid: jest.fn(), - getEntityByName: jest.fn(), - }; +describe('GcpIapProvider', () => { + const authHandler = jest.fn(); + const signInResolver = jest.fn(); + const tokenValidator = jest.fn(); + const logger = getVoidLogger(); - const mockRequest = { - header: jest.fn(() => { - return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo'; - }), - } as unknown as express.Request; - const mockRequestWithoutJwt = { - header: jest.fn(() => { - return undefined; - }), - } as unknown as express.Request; - const mockResponse = { - end: jest.fn(), - header: () => jest.fn(), - json: jest.fn().mockReturnThis(), - status: jest.fn(), - } as unknown as express.Response; - - describe('should transform to type OAuthResponse', () => { - it('when JWT is valid and identity is resolved successfully', async () => { - const provider = new GcpIAPProvider(getVoidLogger(), catalogApi, { - identityResolutionCallback: identityResolutionCallbackMock, - audience: 'foo', - }); - - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.json).toHaveBeenCalledWith({ - backstageIdentity: { - id: 'foo', - idToken: '', - }, - profile: { - displayName: 'Foo Bar', - }, - providerInfo: {}, - }); - }); - }); - describe('should fail when', () => { - it('JWT is missing', async () => { - const provider = new GcpIAPProvider(getVoidLogger(), catalogApi, { - identityResolutionCallback: identityResolutionCallbackMock, - audience: 'foo', - }); - - await provider.refresh(mockRequestWithoutJwt, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + it('runs the happy path', async () => { + const provider = new GcpIapProvider({ + authHandler, + signInResolver, + tokenValidator, + tokenIssuer: {} as any, + catalogIdentityClient: {} as any, + logger, }); - it('identity resolution callback rejects', async () => { - const provider = new GcpIAPProvider(getVoidLogger(), catalogApi, { - identityResolutionCallback: identityResolutionCallbackRejectedMock, - audience: 'foo', - }); + // { "sub": "user:default/me", "ent": ["group:default/home"] } + const backstageToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc'; + const iapToken = { sub: 's', email: 'e@mail.com' }; - await provider.refresh(mockRequest, mockResponse); + authHandler.mockResolvedValueOnce({ email: 'e@mail.com' }); + signInResolver.mockResolvedValueOnce({ id: 'i', token: backstageToken }); + tokenValidator.mockResolvedValueOnce(iapToken); - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.end).toHaveBeenCalledTimes(1); + const app = express(); + app.use('/refresh', provider.refresh.bind(provider)); + + const response = await request(app) + .get('/refresh') + .set('x-goog-iap-jwt-assertion', 'token'); + + expect(response.status).toBe(200); + expect(response.get('content-type')).toBe( + 'application/json; charset=utf-8', + ); + expect(response.body).toEqual({ + backstageIdentity: { + id: 'i', + idToken: backstageToken, + token: backstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/me', + ownershipEntityRefs: ['group:default/home'], + }, + }, + providerInfo: { iapToken }, }); }); }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index 33235083ca..9007fa24fe 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -14,111 +14,112 @@ * limitations under the License. */ -import { CatalogApi } from '@backstage/catalog-client'; import express from 'express'; -import { OAuth2Client } from 'google-auth-library'; +import { TokenPayload } from 'google-auth-library'; import { Logger } from 'winston'; +import { TokenIssuer } from '../../identity/types'; +import { CatalogIdentityClient } from '../../lib/catalog'; +import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; import { + AuthHandler, AuthProviderFactory, - AuthProviderFactoryOptions, AuthProviderRouteHandlers, - ExperimentalIdentityResolver, + SignInResolver, } from '../types'; +import { + createTokenValidator, + defaultAuthHandler, + parseRequestToken, +} from './helpers'; +import { + GcpIapProviderOptions, + GcpIapResponse, + GcpIapResult, + IAP_JWT_HEADER, +} from './types'; -const IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; +export class GcpIapProvider implements AuthProviderRouteHandlers { + private readonly authHandler: AuthHandler; + private readonly signInResolver: SignInResolver; + private readonly tokenValidator: (token: string) => Promise; + private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; -export type GcpIAPProviderOptions = { - audience: string; - identityResolutionCallback: ExperimentalIdentityResolver; -}; -export class GcpIAPProvider implements AuthProviderRouteHandlers { - private logger: Logger; - private options: GcpIAPProviderOptions; - private readonly catalogClient: CatalogApi; - - constructor( - logger: Logger, - catalogClient: CatalogApi, - options: GcpIAPProviderOptions, - ) { - this.logger = logger; - this.catalogClient = catalogClient; - this.options = options; + constructor(options: { + authHandler: AuthHandler; + signInResolver: SignInResolver; + tokenValidator: (token: string) => Promise; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; + }) { + this.authHandler = options.authHandler; + this.signInResolver = options.signInResolver; + this.tokenValidator = options.tokenValidator; + this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; } - frameHandler(): Promise { - return Promise.resolve(undefined); - } + async start() {} + + async frameHandler() {} async refresh(req: express.Request, res: express.Response): Promise { - const expectedAudience = this.options.audience; - const jwtToken = req.header(IAP_JWT_HEADER); - if (jwtToken === undefined) { - res.status(401); - res.end(); - return; - } - const oAuth2Client = new OAuth2Client(); - const verify = async () => { - const response = await oAuth2Client.getIapPublicKeys(); - const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync( - jwtToken, - response.pubkeys, - expectedAudience, - ['https://cloud.google.com/iap'], - ); - return ticket.getPayload(); + const result = await parseRequestToken( + req.header(IAP_JWT_HEADER), + this.tokenValidator, + ); + + const { profile } = await this.authHandler(result); + + const backstageIdentity = await this.signInResolver( + { profile, result }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); + + const response: GcpIapResponse = { + providerInfo: { iapToken: result.iapToken }, + profile, + backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity), }; - try { - const user = await verify(); - if (user === undefined) { - this.logger.error('gcp iap proxy user returned undefined'); - res.status(401); - res.end(); - return; - } - const resolvedEntity = await this.options.identityResolutionCallback( - { - email: user.email, - }, - this.catalogClient, - ); - res.json(resolvedEntity); - } catch (e) { - this.logger.error('Verification failed with', e); - - res.status(401); - res.end(); - return; - } - res.status(200); - res.end(); - } - - start(): Promise { - return Promise.resolve(undefined); + res.json(response); } } +/** + * Creates an auth provider for Google Identity-Aware Proxy. + * + * @public + */ export function createGcpIapProvider( - _options?: GcpIAPProviderOptions, + options: GcpIapProviderOptions, ): AuthProviderFactory { - return ({ - logger, - catalogApi, - config, - identityResolver, - }: AuthProviderFactoryOptions) => { + return ({ config, tokenIssuer, catalogApi, logger }) => { const audience = config.getString('audience'); - if (identityResolver !== undefined) { - throw new Error( - 'Identity resolver is required to use this authentication provider', - ); - } - return new GcpIAPProvider(logger, catalogApi, { - audience, - identityResolutionCallback: identityResolver, + + const authHandler = options.authHandler ?? defaultAuthHandler; + const signInResolver = options.signIn.resolver; + const tokenValidator = createTokenValidator(audience); + + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + return new GcpIapProvider({ + authHandler, + signInResolver, + tokenValidator, + tokenIssuer, + catalogIdentityClient, + logger, }); }; } diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts new file mode 100644 index 0000000000..5a8c3491fe --- /dev/null +++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { JsonValue } from '@backstage/types'; +import { AuthHandler, AuthResponse, SignInResolver } from '../types'; + +/** + * The header name used by the IAP. + */ +export const IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; + +/** + * The data extracted from an IAP token. + * + * @public + */ +export type GcpIapTokenInfo = { + /** + * The unique, stable identifier for the user. + */ + sub: string; + /** + * User email address. + */ + email: string; + /** + * Other fields. + */ + [key: string]: JsonValue; +}; + +/** + * The result of the initial auth challenge. This is the input to the auth + * callbacks. + * + * @public + */ +export type GcpIapResult = { + /** + * The data extracted from the IAP token header. + */ + iapToken: GcpIapTokenInfo; +}; + +/** + * The provider info to return to the frontend. + */ +export type GcpIapProviderInfo = { + /** + * The data extracted from the IAP token header. + */ + iapToken: GcpIapTokenInfo; +}; + +/** + * The shape of the response to return to callers. + */ +export type GcpIapResponse = AuthResponse; + +/** + * Options for {@link createGcpIapProvider}. + * + * @public + */ +export type GcpIapProviderOptions = { + /** + * The profile transformation function used to verify and convert the auth + * response into the profile that will be presented to the user. The default + * implementation just provides the authenticated email that the IAP + * presented. + */ + authHandler?: AuthHandler; + + /** + * Configures sign-in for this provider. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; +}; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 3f71dd2c26..a25d2ce8bc 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -27,6 +27,7 @@ export * from './oidc'; export * from './okta'; export * from './onelogin'; export * from './saml'; +export * from './gcp-iap'; export { factories as defaultAuthProviderFactories } from './factories'; diff --git a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts index 31df7a4cef..1de8ebb9b2 100644 --- a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts +++ b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts @@ -22,7 +22,9 @@ function parseJwtPayload(token: string) { } /** - * Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token + * Parses a Backstage-issued token and decorates the + * {@link BackstageIdentityResponse} with identity information sourced from the + * token. * * @public */ diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index ecb466c768..0915bccc6c 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -200,13 +200,16 @@ export interface BackstageSignInResult { /** * The old exported symbol for {@link BackstageSignInResult}. + * * @public - * @deprecated Use the `BackstageSignInResult` type instead. + * @deprecated Use the {@link BackstageSignInResult} instead. */ export type BackstageIdentity = BackstageSignInResult; /** - * Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider. + * Response object containing the {@link BackstageUserIdentity} and the token + * from the authentication provider. + * * @public */ export interface BackstageIdentityResponse extends BackstageSignInResult { @@ -220,7 +223,8 @@ export interface BackstageIdentityResponse extends BackstageSignInResult { * Used to display login information to user, i.e. sidebar popup. * * 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 + * identity, but we want to replace that with data from identity and/org catalog + * service * * @public */ @@ -241,28 +245,32 @@ export type ProfileInfo = { }; /** - * type of sign in information context, includes the profile information and authentication result which contains auth. related information + * Type of sign in information context. Includes the profile information and + * authentication result which contains auth related information. + * * @public */ -export type SignInInfo = { +export type SignInInfo = { /** * The simple profile passed down for use in the frontend. */ profile: ProfileInfo; /** - * The authentication result that was received from the authentication provider. + * The authentication result that was received from the authentication + * provider. */ - result: AuthResult; + result: TAuthResult; }; /** - * Sign in resolver type describes the function which handles the result of a successful authentication - * and it must return a valid {@link BackstageSignInResult} + * Describes the function which handles the result of a successful + * authentication. Must return a valid {@link BackstageSignInResult}. + * * @public */ -export type SignInResolver = ( - info: SignInInfo, +export type SignInResolver = ( + info: SignInInfo, context: { tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; @@ -271,23 +279,28 @@ export type SignInResolver = ( ) => Promise; /** - * The return type of authentication handler which must contain a valid profile information + * The return type of an authentication handler. Must contain valid profile + * information. + * * @public */ export type AuthHandlerResult = { profile: ProfileInfo }; /** - * The AuthHandler function is called every time the user authenticates using the provider. + * The AuthHandler function is called every time the user authenticates using + * the provider. * - * The handler should return a profile that represents the session for the user in the frontend. + * The handler should return a profile that represents the session for the user + * in the frontend. * - * Throwing an error in the function will cause the authentication to fail, making it - * possible to use this function as a way to limit access to a certain group of users. + * Throwing an error in the function will cause the authentication to fail, + * making it possible to use this function as a way to limit access to a certain + * group of users. * * @public */ -export type AuthHandler = ( - input: AuthResult, +export type AuthHandler = ( + input: TAuthResult, ) => Promise; export type StateEncoder = (