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 = (