diff --git a/.changeset/dull-peas-double.md b/.changeset/dull-peas-double.md new file mode 100644 index 0000000000..3d1735e672 --- /dev/null +++ b/.changeset/dull-peas-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added Google Cloud Identity-Aware Proxy as an identity provider. 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 620c8b397a..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", @@ -46,6 +47,7 @@ "express-promise-router": "^4.1.0", "express-session": "^1.17.1", "fs-extra": "9.1.0", + "google-auth-library": "^7.6.1", "helmet": "^4.0.0", "jose": "^1.27.1", "jwt-decode": "^3.1.0", @@ -83,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..94fa9c0bd8 --- /dev/null +++ b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts @@ -0,0 +1,134 @@ +/* + * 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); + }); + + it('rejects empty payload', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => undefined, + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(validJwt)).rejects.toMatchObject({ + name: 'TypeError', + message: 'Token had no payload', + }); + }); + }); + + 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 () => ({ sub: 'a' } as any)), + ).rejects.toMatchObject({ + name: 'AuthenticationError', + message: 'Google IAP token payload is missing sub and/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..c7e81adc73 --- /dev/null +++ b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts @@ -0,0 +1,84 @@ +/* + * 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 { + const client = mockClient ?? new OAuth2Client(); + + return async function tokenValidator(token) { + // TODO(freben): Rate limit the public key reads. It may be sensible to + // cache these for some reasonable time rather than asking for the public + // keys on every single sign-in. But since the rate of events here is so + // slow, I decided to keep it simple for now. + 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('Token had 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.sub || !payload.email) { + throw new AuthenticationError( + 'Google IAP token payload is missing sub and/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 new file mode 100644 index 0000000000..fe5d178fcc --- /dev/null +++ b/plugins/auth-backend/src/providers/gcp-iap/index.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +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 new file mode 100644 index 0000000000..bb1ed5c79b --- /dev/null +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2020 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { GcpIapProvider } from './provider'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('GcpIapProvider', () => { + const authHandler = jest.fn(); + const signInResolver = jest.fn(); + const tokenValidator = jest.fn(); + const logger = getVoidLogger(); + + it('runs the happy path', async () => { + const provider = new GcpIapProvider({ + authHandler, + signInResolver, + tokenValidator, + tokenIssuer: {} as any, + catalogIdentityClient: {} as any, + logger, + }); + + // { "sub": "user:default/me", "ent": ["group:default/home"] } + const backstageToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc'; + const iapToken = { sub: 's', email: 'e@mail.com' }; + + authHandler.mockResolvedValueOnce({ email: 'e@mail.com' }); + signInResolver.mockResolvedValueOnce({ id: 'i', token: backstageToken }); + tokenValidator.mockResolvedValueOnce(iapToken); + + 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 new file mode 100644 index 0000000000..9007fa24fe --- /dev/null +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -0,0 +1,125 @@ +/* + * 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 express from 'express'; +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, + AuthProviderRouteHandlers, + SignInResolver, +} from '../types'; +import { + createTokenValidator, + defaultAuthHandler, + parseRequestToken, +} from './helpers'; +import { + GcpIapProviderOptions, + GcpIapResponse, + GcpIapResult, + IAP_JWT_HEADER, +} from './types'; + +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; + + 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; + } + + async start() {} + + async frameHandler() {} + + async refresh(req: express.Request, res: express.Response): Promise { + 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), + }; + + res.json(response); + } +} + +/** + * Creates an auth provider for Google Identity-Aware Proxy. + * + * @public + */ +export function createGcpIapProvider( + options: GcpIapProviderOptions, +): AuthProviderFactory { + return ({ config, tokenIssuer, catalogApi, logger }) => { + const audience = config.getString('audience'); + + 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 = (