diff --git a/docs/auth/google/gcp-iap-auth.md b/docs/auth/google/gcp-iap-auth.md
index 2188ca5465..bafc573d18 100644
--- a/docs/auth/google/gcp-iap-auth.md
+++ b/docs/auth/google/gcp-iap-auth.md
@@ -2,16 +2,21 @@
id: provider
title: Google Identity Aware Proxy Provider
sidebar_label: Google IAP
-description: Adding Google Identity Aware Proxy as an authentication provider in Backstage
+description:
+ Adding Google Identity Aware Proxy as an authentication provider in Backstage
---
# Using Google Identity Aware Proxy to authenticate requests
-Backstage allows offloading the responsibility of authenticating users to an Google HTTPS Load Balancer & [IAP](https://cloud.google.com/iap), leveraging the authentication support on IAP.
+Backstage allows offloading the responsibility of authenticating users to an
+Google HTTPS Load Balancer & [IAP](https://cloud.google.com/iap), leveraging the
+authentication support on IAP.
-This tutorial shows how to use authentication on an ALB sitting in front of Backstage.
+This tutorial shows how to use authentication on an ALB 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.
+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
@@ -19,16 +24,21 @@ It is assumed an IAP is already serving traffic in front of a Backstage instance
### Frontend
-The Backstage App needs a SignInPage when authentication is required.
-When using ALB authentication Backstage will only be loaded once the user has successfully authenticated; we won't need to display a SignIn page, however we will need to create a dummy SignIn component that can refresh the token.
+The Backstage App needs a SignInPage when authentication is required. When using
+IAP Proxy authentication Backstage will only be loaded once the user has
+successfully authenticated; we won't need to display a SignIn page, however we
+will need to create a dummy SignIn component that can refresh the token.
- 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`):
+- 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`):
```ts
const refreshToken = async ({ props, discoveryApiConfig, config }) => {
- const baseUrl = await discoveryApiConfig.getBaseUrl("auth");
+ const baseUrl = await discoveryApiConfig.getBaseUrl('auth');
const shouldAuth = !!config.getOptionalConfig('auth.providers.gcp-iap');
if (!shouldAuth) {
@@ -43,18 +53,17 @@ const refreshToken = async ({ props, discoveryApiConfig, config }) => {
return;
}
try {
-
const request = await fetch(`${baseUrl}/gcp-iap/refresh`, {
headers: {
- "x-requested-with": "XMLHttpRequest"
+ 'x-requested-with': 'XMLHttpRequest',
},
- credentials: "include"
+ credentials: 'include',
});
- const data = await request.json()
+ const data = await request.json();
props.onResult({
- userId: data.backstageIdentity.id ?? "nouser@ms.at",
- profile: data.profile ?? "nouser@ms.at",
+ userId: data.backstageIdentity.id ?? 'nouser@ms.at',
+ profile: data.profile ?? 'nouser@ms.at',
});
} catch (e) {
props.onResult({
@@ -71,7 +80,7 @@ const DummySignInComponent: any = (props: any) => {
try {
const config = useApi(configApiRef);
const discoveryApiConfig = useApi(discoveryApiRef);
- refreshToken({ props, discoveryApiConfig, config })
+ refreshToken({ props, discoveryApiConfig, config });
return
;
} catch (err) {
return {err.message}
;
@@ -81,7 +90,9 @@ const DummySignInComponent: any = (props: any) => {
### Backend
-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.
+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.
- replace the content of `packages/backend/plugin/auth.ts` with the below
@@ -116,10 +127,17 @@ export default async function createPlugin({
});
};
return await createRouter({
- logger, config, database, discovery, providerFactories: {
- "gcp-iap": (options: AuthProviderFactoryOptions) => {
- return createGcpIAPProvider({ ...options, identityResolver })({ ...options, identityResolver })
- }
+ logger,
+ config,
+ database,
+ discovery,
+ providerFactories: {
+ 'gcp-iap': (options: AuthProviderFactoryOptions) => {
+ return createGcpIAPProvider({ ...options, identityResolver })({
+ ...options,
+ identityResolver,
+ });
+ },
},
});
}
@@ -133,10 +151,10 @@ Use the following `auth` configuration when running Backstage on AWS:
auth:
providers:
gcp-iap:
- audience: "/projects/0123456/global/backendServices/1242345678765434567"
+ audience: '/projects/0123456/global/backendServices/1242345678765434567'
```
-
## Conclusion
-Once it's deployed, after going through the AAD authentication flow, Backstage should display the AAD user details.
+Once it's deployed, after going through the AAD authentication flow, Backstage
+should display the AAD user details.
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index 620c8b397a..25f3a7fde8 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -45,7 +45,7 @@
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-session": "^1.17.1",
- "fs-extra": "9.1.0",
+ "google-auth-library": "^7.2.0",
"helmet": "^4.0.0",
"jose": "^1.27.1",
"jwt-decode": "^3.1.0",
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 3fa168665a..0d39987c93 100644
--- a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts
+++ b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts
@@ -15,33 +15,14 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
-import { JWT } from 'jose';
-import { AwsAlbAuthProvider } from './provider';
+import { GcpIAPProvider } from './provider';
import { AuthResponse } from '../types';
-const jwtMock = JWT as jest.Mocked;
+jest.mock('google-auth-library');
-const mockKey = async () => {
- return `-----BEGIN PUBLIC KEY-----
-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnuN4LlaJhaUpx+qZFTzYCrSBLk0I
-yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw==
------END PUBLIC KEY-----
-`;
-};
-
-jest.mock('jose');
-
-jest.mock('cross-fetch', () => ({
- __esModule: true,
- default: async () => {
- return {
- text: async () => {
- return mockKey();
- },
- };
- },
-}));
+const setCredentialsMock = jest.fn();
+const getAccessTokenMock = jest.fn();
const identityResolutionCallbackMock = async (): Promise> => {
return {
@@ -66,7 +47,7 @@ beforeEach(() => {
jest.clearAllMocks();
});
-describe('AwsALBAuthProvider', () => {
+describe('GcpIAPProvider', () => {
const catalogApi = {
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
addLocation: jest.fn(),
@@ -98,16 +79,11 @@ describe('AwsALBAuthProvider', () => {
describe('should transform to type OAuthResponse', () => {
it('when JWT is valid and identity is resolved successfully', async () => {
- const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
- region: 'us-west-2',
+ const provider = new GcpIAPProvider(getVoidLogger(), catalogApi, {
identityResolutionCallback: identityResolutionCallbackMock,
- issuer: 'foo',
+ audience: 'foo',
});
- jwtMock.verify.mockImplementationOnce(() => ({
- sub: 'foo',
- }));
-
await provider.refresh(mockRequest, mockResponse);
expect(mockResponse.json).toHaveBeenCalledWith({
@@ -124,10 +100,9 @@ describe('AwsALBAuthProvider', () => {
});
describe('should fail when', () => {
it('JWT is missing', async () => {
- const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
- region: 'us-west-2',
+ const provider = new GcpIAPProvider(getVoidLogger(), catalogApi, {
identityResolutionCallback: identityResolutionCallbackMock,
- issuer: 'foo',
+ audience: 'foo',
});
await provider.refresh(mockRequestWithoutJwt, mockResponse);
@@ -135,44 +110,12 @@ describe('AwsALBAuthProvider', () => {
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
- it('JWT is invalid', async () => {
- const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
- region: 'us-west-2',
- identityResolutionCallback: identityResolutionCallbackMock,
- issuer: 'foo',
- });
-
- jwtMock.verify.mockImplementationOnce(() => {
- throw new Error('bad JWT');
- });
-
- await provider.refresh(mockRequest, mockResponse);
-
- expect(mockResponse.status).toHaveBeenCalledWith(401);
- });
-
- it('issuer is invalid', async () => {
- const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
- region: 'us-west-2',
- identityResolutionCallback: identityResolutionCallbackMock,
- issuer: 'foobar',
- });
-
- jwtMock.verify.mockReturnValueOnce({});
-
- await provider.refresh(mockRequest, mockResponse);
- expect(mockResponse.status).toHaveBeenCalledWith(401);
- });
-
it('identity resolution callback rejects', async () => {
- const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
- region: 'us-west-2',
+ const provider = new GcpIAPProvider(getVoidLogger(), catalogApi, {
identityResolutionCallback: identityResolutionCallbackRejectedMock,
- issuer: 'foo',
+ audience: 'foo',
});
- jwtMock.verify.mockReturnValueOnce({});
-
await provider.refresh(mockRequest, mockResponse);
expect(mockResponse.status).toHaveBeenCalledWith(401);
diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts
index 0226855a88..c437dadda0 100644
--- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts
+++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts
@@ -13,16 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {
- AuthProviderFactoryOptions,
- AuthProviderRouteHandlers,
- AuthResponse
-} from '@backstage/plugin-auth-backend';
import {
+ AuthProviderRouteHandlers,
+ AuthProviderFactoryOptions,
ExperimentalIdentityResolver,
} from '../types';
+
import express from 'express';
+
import { Logger } from 'winston';
import { CatalogApi } from '@backstage/catalog-client';
@@ -54,9 +53,12 @@ export class GcpIAPProvider implements AuthProviderRouteHandlers {
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();
@@ -64,30 +66,32 @@ export class GcpIAPProvider implements AuthProviderRouteHandlers {
jwtToken,
response.pubkeys,
expectedAudience,
- ['https://cloud.google.com/iap']
+ ['https://cloud.google.com/iap'],
);
- return ticket.payload;
- }
+ return ticket.getPayload();
+ };
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
+ email: user.email,
},
this.catalogClient,
);
res.json(resolvedEntity);
} catch (e) {
- const resolvedEntity = await this.options.identityResolutionCallback(
- {},
- this.catalogClient,
- );
- res.json(resolvedEntity);
this.logger.error('Verification failed with', e);
res.status(401);
res.end();
+ return;
}
res.status(200);
res.end();
@@ -98,7 +102,6 @@ export class GcpIAPProvider implements AuthProviderRouteHandlers {
}
}
-
export const createGcpIAPProvider = (_options?: GcpIAPProviderOptions) => {
return ({
logger,