diff --git a/contrib/docs/tutorials/gcp-iap-auth.md b/contrib/docs/tutorials/gcp-iap-auth.md
new file mode 100644
index 0000000000..c2ec031467
--- /dev/null
+++ b/contrib/docs/tutorials/gcp-iap-auth.md
@@ -0,0 +1,139 @@
+# Using AWS Application Load Balancer with Azure Active Directory to authenticate requests
+
+Backstage allows offloading the responsibility of authenticating users to an AWS Application Load Balancer (**ALB**), leveraging the authentication support on ALB.
+This tutorial shows how to use authentication on an ALB sitting in front of Backstage.
+Azure Active Directory (**AAD**) is used as identity provider but any identity provider supporting OpenID Connect (OIDC) can be used.
+
+It is assumed an ALB is already serving traffic in front of a Backstage instance configured to serve the frontend app from the backend.
+
+## Infrastructure setup
+
+## Backstage changes
+
+### 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.
+
+- 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`):
+
+```ts
+const refreshToken = async ({ props, discoveryApiConfig, config }) => {
+ const baseUrl = await discoveryApiConfig.getBaseUrl("auth");
+ const shouldAuth = !!config.getOptionalConfig('auth.providers.gcp-iap');
+
+ if (!shouldAuth) {
+ props.onResult({
+ userId: 'guest',
+ profile: {
+ email: 'guest@example.com',
+ displayName: 'Guest',
+ picture: '',
+ },
+ });
+ return;
+ }
+ try {
+
+ const request = await fetch(`${baseUrl}/gcp-iap/refresh`, {
+ headers: {
+ "x-requested-with": "XMLHttpRequest"
+ },
+ credentials: "include"
+ });
+ const data = await request.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}
;
+ }
+};
+```
+
+### 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.
+
+- replace the content of `packages/backend/plugin/auth.ts` with the below
+
+```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';
+
+export default async function createPlugin({
+ logger,
+ database,
+ 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 })
+ }
+ },
+ });
+}
+```
+
+### Configuration
+
+Use the following `auth` configuration when running Backstage on AWS:
+
+```yaml
+auth:
+ providers:
+ gcp-iap:
+ 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.
+
+
+
+[monorepo-app-setup-with-auth-ms]: https://backstage.io/docs/auth/microsoft/provider
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..1c81aa919d
--- /dev/null
+++ b/plugins/auth-backend/src/providers/gcp-iap/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 } from './provider';
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..3fa168665a
--- /dev/null
+++ b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts
@@ -0,0 +1,182 @@
+/*
+ * 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 { JWT } from 'jose';
+
+import { AwsAlbAuthProvider } from './provider';
+import { AuthResponse } from '../types';
+
+const jwtMock = JWT as jest.Mocked;
+
+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 identityResolutionCallbackMock = async (): Promise> => {
+ return {
+ backstageIdentity: {
+ id: 'foo',
+ idToken: '',
+ },
+ profile: {
+ displayName: 'Foo Bar',
+ },
+ providerInfo: {},
+ };
+};
+
+const identityResolutionCallbackRejectedMock = async (): Promise<
+ AuthResponse
+> => {
+ throw new Error('failed');
+};
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe('AwsALBAuthProvider', () => {
+ 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(),
+ };
+
+ 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 AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
+ region: 'us-west-2',
+ identityResolutionCallback: identityResolutionCallbackMock,
+ issuer: 'foo',
+ });
+
+ jwtMock.verify.mockImplementationOnce(() => ({
+ sub: '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 AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
+ region: 'us-west-2',
+ identityResolutionCallback: identityResolutionCallbackMock,
+ issuer: 'foo',
+ });
+
+ await provider.refresh(mockRequestWithoutJwt, mockResponse);
+
+ 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',
+ identityResolutionCallback: identityResolutionCallbackRejectedMock,
+ issuer: 'foo',
+ });
+
+ jwtMock.verify.mockReturnValueOnce({});
+
+ await provider.refresh(mockRequest, mockResponse);
+
+ expect(mockResponse.status).toHaveBeenCalledWith(401);
+ expect(mockResponse.end).toHaveBeenCalledTimes(1);
+ });
+ });
+});
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..1d6f5c3d5e
--- /dev/null
+++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts
@@ -0,0 +1,123 @@
+/*
+ * 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 {
+ AuthProviderFactoryOptions,
+ AuthProviderRouteHandlers,
+ AuthResponse
+} from '@backstage/plugin-auth-backend';
+import express from 'express';
+import { Logger } from 'winston';
+import { CatalogApi } from '@backstage/catalog-client';
+
+const { OAuth2Client } = require('google-auth-library');
+
+const IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion';
+
+export type ExperimentalIdentityResolver = (
+ /**
+ * An object containing information specific to the auth provider.
+ */
+ payload: object,
+ catalogApi: CatalogApi,
+) => Promise>;
+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;
+ }
+ frameHandler(): Promise {
+ return Promise.resolve(undefined);
+ }
+
+ async refresh(req: express.Request, res: express.Response): Promise {
+ const expectedAudience = this.options.audience;
+
+ const jwtToken = req.header(IAP_JWT_HEADER);
+
+ 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.payload;
+ }
+
+ try {
+ const user = await verify();
+ const resolvedEntity = await this.options.identityResolutionCallback(
+ {
+ 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();
+ }
+ res.status(200);
+ res.end();
+ }
+
+ start(): Promise {
+ return Promise.resolve(undefined);
+ }
+}
+
+
+export const createGcpIAPProvider = (_options?: GcpIAPProviderOptions) => {
+ return ({
+ logger,
+ catalogApi,
+ config,
+ identityResolver,
+ }: AuthProviderFactoryOptions) => {
+ const audience = config.getString('audience');
+ if (identityResolver !== undefined) {
+ return new GcpIAPProvider(logger, catalogApi, {
+ audience,
+ identityResolutionCallback: identityResolver,
+ });
+ }
+ throw new Error(
+ 'Identity resolver is required to use this authentication provider',
+ );
+ };
+};