add Cloudflare Access authentication provider
Signed-off-by: Renlord Yang <renlord@cloudflare.com> Signed-off-by: Renlord Yang <me@renlord.com>
This commit is contained in:
committed by
Renlord Yang
parent
d1c04da5d0
commit
3cedfd8365
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': minor
|
||||
---
|
||||
|
||||
add Cloudflare Access auth provider to auth-backend
|
||||
@@ -362,3 +362,5 @@ Zhou
|
||||
zoomable
|
||||
zsh
|
||||
Alef
|
||||
Cloudflare
|
||||
cloudflare
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
id: cfaccess
|
||||
title: Cloudflare Access Provider
|
||||
sidebar_label: cfaccess
|
||||
# prettier-ignore
|
||||
description: Adding Cloudflare Access as an authentication provider in Backstage
|
||||
---
|
||||
|
||||
Similar to GCP IAP Proxy Provider or AWS ALB provider, developers can offload authentication
|
||||
support to Cloudflare Access.
|
||||
|
||||
This tutorial shows how to use authentication on Cloudflare Access sitting in
|
||||
front of Backstage.
|
||||
|
||||
It is assumed a Cloudflare tunnel is already serving traffic in front of a
|
||||
Backstage instance configured to serve the frontend app from the backend and is
|
||||
already gated using Cloudflare Access.
|
||||
|
||||
## Configuration
|
||||
|
||||
Let's start by adding the following `auth` configuration in your
|
||||
`app-config.yaml` or `app-config.production.yaml` or similar:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
providers:
|
||||
cfaccess:
|
||||
teamName: <Team Name>
|
||||
```
|
||||
|
||||
You can find the team name in the Cloudflare Zero Trust dashboard.
|
||||
|
||||
This config section must be in place for the provider to load at all. Now let's
|
||||
add the provider itself.
|
||||
|
||||
## Backend Changes
|
||||
|
||||
Add a `providerFactories` entry to the router in
|
||||
`packages/backend/plugin/auth.ts`.
|
||||
|
||||
```ts
|
||||
import { providers } from '@backstage/plugin-auth-backend';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
database: env.database,
|
||||
discovery: env.discovery,
|
||||
providerFactories: {
|
||||
'cfaccess': providers.cfAccess.create({
|
||||
// Replace the auth handler if you want to customize the returned user
|
||||
// profile info (can be left out; the default implementation is shown
|
||||
// below which only returns the email). You may want to amend this code
|
||||
// with something that loads additional user profile data out.
|
||||
async authHandler({ accessToken }) {
|
||||
return { profile: { email: accessToken.email } };
|
||||
},
|
||||
signIn: {
|
||||
// You need to supply an identity resolver, that takes the profile
|
||||
// and the access token and produces the Backstage token with the
|
||||
// relevant user info.
|
||||
async resolver({ profile, result }, ctx) {
|
||||
// Somehow compute the Backstage token claims. Just some dummy code
|
||||
// shown here, but you may want to query your LDAP server, or
|
||||
// https://<teamName>.cloudflareaccess.com/cdn-cgi/access/get-identity
|
||||
// https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json/#groups-within-a-jwt
|
||||
const id = profile.email.split('@')[0];
|
||||
const sub = stringifyEntityRef({ kind: 'User', name: id });
|
||||
const ent = [sub, stringifyEntityRef({ kind: 'Group', name: 'team-name' });
|
||||
return ctx.issueToken({ claims: { sub, ent } });
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Now the backend is ready to serve auth requests on the
|
||||
`/api/auth/cfaccess/refresh` endpoint. All that's left is to update the
|
||||
frontend sign-in mechanism to poll that endpoint through Cloudflare Access, on
|
||||
the user's behalf.
|
||||
|
||||
## Frontend Changes
|
||||
|
||||
It is recommended to use the `ProxiedSignInPage` for this provider, which is
|
||||
installed in `packages/app/src/App.tsx` like this:
|
||||
|
||||
```diff
|
||||
+import { ProxiedSignInPage } from '@backstage/core-components';
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
+ SignInPage: props => <ProxiedSignInPage {...props} provider="cfaccess" />,
|
||||
```
|
||||
|
||||
See the [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) section for more information.
|
||||
+2
-1
@@ -18,6 +18,7 @@ Backstage comes with many common authentication providers in the core library:
|
||||
- [Auth0](auth0/provider.md)
|
||||
- [Azure](microsoft/provider.md)
|
||||
- [Bitbucket](bitbucket/provider.md)
|
||||
- [Cloudflare Access](cloudflare/access.md)
|
||||
- [GitHub](github/provider.md)
|
||||
- [GitLab](gitlab/provider.md)
|
||||
- [Google](google/provider.md)
|
||||
@@ -131,7 +132,7 @@ allows allowing guest access:
|
||||
|
||||
Some auth providers are so-called "proxy" providers, meaning they're meant to be used
|
||||
behind an authentication proxy. Examples of these are
|
||||
[AWS ALB](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md),
|
||||
[AWS ALB](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), [Cloudflare Access](./cloudflare/access.md),
|
||||
[GCP IAP](./google/gcp-iap-auth.md), and [OAuth2 Proxy](./oauth2-proxy/provider.md).
|
||||
|
||||
When using a proxy provider, you'll end up wanting to use a different sign-in page, as
|
||||
|
||||
@@ -170,6 +170,12 @@ export class CatalogIdentityClient {
|
||||
resolveCatalogMembership(query: MemberClaimQuery): Promise<string[]>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type CloudflareAccessResult = {
|
||||
fullProfile: Profile;
|
||||
expiresInSeconds?: number;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type CookieConfigurer = (ctx: {
|
||||
providerId: string;
|
||||
|
||||
Vendored
+3
@@ -116,6 +116,9 @@ export interface Config {
|
||||
issuer?: string;
|
||||
region: string;
|
||||
};
|
||||
cfaccess?: {
|
||||
teamName: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 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 { cfAccess } from './provider';
|
||||
export type { CloudflareAccessResult } from './provider';
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 2022 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 express from 'express';
|
||||
import { jwtVerify } from 'jose';
|
||||
import {
|
||||
CF_JWT_HEADER,
|
||||
CF_AUTH_IDENTITY,
|
||||
CloudflareAccessAuthProvider,
|
||||
} from './provider';
|
||||
import { makeProfileInfo } from '../../lib/passport';
|
||||
import { AuthResolverContext } from '../types';
|
||||
|
||||
const jwtMock = jwtVerify as jest.Mocked<any>;
|
||||
|
||||
const mockKey = async () => {
|
||||
return `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnuN4LlaJhaUpx+qZFTzYCrSBLk0I
|
||||
yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw==
|
||||
-----END PUBLIC KEY-----
|
||||
`;
|
||||
};
|
||||
const mockJwt =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlVzZXIgTmFtZSIsImlhdCI6MTUxNjIzOTAyMn0.uMCSBGhij1xn5pnot8XgD-huQuTIBOFGs6kkW_p_X94';
|
||||
const mockClaims = {
|
||||
sub: '1234567890',
|
||||
name: 'User Name',
|
||||
family_name: 'Name',
|
||||
given_name: 'User',
|
||||
email: 'user.name@email.test',
|
||||
iat: 1632833760,
|
||||
exp: 1632833763,
|
||||
iss: 'ISSUER_URL',
|
||||
};
|
||||
const mockAuthenticatedUserEmail = 'user.name@email.test';
|
||||
|
||||
jest.mock('jose');
|
||||
jest.mock('node-fetch', () => ({
|
||||
__esModule: true,
|
||||
default: async () => {
|
||||
return {
|
||||
text: async () => {
|
||||
return mockKey();
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('CloudflareAccessAuthProvider', () => {
|
||||
const mockRequest = {
|
||||
header: jest.fn(name => {
|
||||
if (name === CF_JWT_HEADER) {
|
||||
return mockJwt;
|
||||
} else if (name === CF_AUTH_IDENTITY) {
|
||||
return mockAuthenticatedUserEmail;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} 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 CloudflareAccessResponse', () => {
|
||||
it('when JWT is valid and identity is resolved successfully', async () => {
|
||||
const provider = new CloudflareAccessAuthProvider({
|
||||
teamName: 'foobar',
|
||||
resolverContext: {} as AuthResolverContext,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return {
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.mockReturnValueOnce(Promise.resolve({ payload: mockClaims }));
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
backstageIdentity: {
|
||||
token:
|
||||
'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob',
|
||||
identity: {
|
||||
ownershipEntityRefs: ['user:default/jimmymarkum'],
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/jimmymarkum',
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
displayName: 'User Name',
|
||||
email: 'user.name@email.test',
|
||||
},
|
||||
providerInfo: {
|
||||
expiresInSeconds: mockClaims.exp - mockClaims.iat,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should fail when', () => {
|
||||
it('JWT is missing', async () => {
|
||||
const provider = new CloudflareAccessAuthProvider({
|
||||
teamName: 'foobar',
|
||||
resolverContext: {} as AuthResolverContext,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
provider.refresh(mockRequestWithoutJwt, mockResponse),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('JWT is invalid', async () => {
|
||||
const provider = new CloudflareAccessAuthProvider({
|
||||
teamName: 'foobar',
|
||||
resolverContext: {} as AuthResolverContext,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.mockImplementationOnce(() => {
|
||||
throw new Error('bad JWT');
|
||||
});
|
||||
|
||||
await expect(
|
||||
provider.refresh(mockRequest, mockResponse),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('SignInResolver rejects', async () => {
|
||||
const provider = new CloudflareAccessAuthProvider({
|
||||
teamName: 'foobar',
|
||||
resolverContext: {} as AuthResolverContext,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
throw new Error();
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.mockReturnValueOnce(mockClaims);
|
||||
|
||||
await expect(
|
||||
provider.refresh(mockRequest, mockResponse),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('AuthHandler rejects', async () => {
|
||||
const provider = new CloudflareAccessAuthProvider({
|
||||
teamName: 'foobar',
|
||||
resolverContext: {} as AuthResolverContext,
|
||||
authHandler: async () => {
|
||||
throw new Error();
|
||||
},
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.mockReturnValueOnce(mockClaims);
|
||||
|
||||
await expect(
|
||||
provider.refresh(mockRequest, mockResponse),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
AuthHandler,
|
||||
AuthProviderRouteHandlers,
|
||||
AuthResolverContext,
|
||||
AuthResponse,
|
||||
SignInResolver,
|
||||
} from '../types';
|
||||
import express from 'express';
|
||||
import * as _ from 'lodash';
|
||||
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
||||
import { Profile as PassportProfile } from 'passport';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
|
||||
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
|
||||
import { makeProfileInfo } from '../../lib/passport';
|
||||
|
||||
// JWT Web Token definitions are in the URL below
|
||||
// https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json/
|
||||
export const CF_JWT_HEADER = 'cf-access-jwt-assertion';
|
||||
export const CF_AUTH_IDENTITY = 'cf-access-authenticated-user-email';
|
||||
|
||||
/** @public */
|
||||
export type Options = {
|
||||
/**
|
||||
* Access team name
|
||||
*
|
||||
* When you configure Access, the public certificates are available at this
|
||||
* URL, where your-team-name is your team name:
|
||||
* https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/certs
|
||||
*/
|
||||
teamName: string;
|
||||
authHandler: AuthHandler<CloudflareAccessResult>;
|
||||
signInResolver: SignInResolver<CloudflareAccessResult>;
|
||||
resolverContext: AuthResolverContext;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type CloudflareAccessClaims = {
|
||||
/**
|
||||
* `aud` identifies the application to which the JWT is issued.
|
||||
*/
|
||||
aud: string[];
|
||||
/**
|
||||
* `email` contains the email address of the authenticated user.
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* iat and exp are the issuance and expiration timestamps.
|
||||
*/
|
||||
exp: number;
|
||||
iat: number;
|
||||
/**
|
||||
* `nonce` is the session identifier.
|
||||
*/
|
||||
nonce: string;
|
||||
/**
|
||||
* `identity_nonce` is available in the Application Token and can be used to
|
||||
* query all group membership for a given user.
|
||||
*/
|
||||
identity_nonce: string;
|
||||
/**
|
||||
* `sub` contains the identifier of the authenticated user.
|
||||
*/
|
||||
sub: string;
|
||||
/**
|
||||
* `iss` the issuer is the application’s Cloudflare Access Domain URL.
|
||||
*/
|
||||
iss: string;
|
||||
/**
|
||||
* `custom` contains SAML attributes in the Application Token specified by an
|
||||
* administrator in the identity provider configuration.
|
||||
*/
|
||||
custom: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type CloudflareAccessResult = {
|
||||
fullProfile: PassportProfile;
|
||||
expiresInSeconds?: number;
|
||||
};
|
||||
|
||||
export type CloudflareAccessProviderInfo = {
|
||||
/**
|
||||
* Expiry of the access token in seconds.
|
||||
*/
|
||||
expiresInSeconds?: number;
|
||||
};
|
||||
|
||||
export type CloudflareAccessResponse =
|
||||
AuthResponse<CloudflareAccessProviderInfo>;
|
||||
|
||||
export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers {
|
||||
private readonly teamName: string;
|
||||
private readonly resolverContext: AuthResolverContext;
|
||||
private readonly authHandler: AuthHandler<CloudflareAccessResult>;
|
||||
private readonly signInResolver: SignInResolver<CloudflareAccessResult>;
|
||||
private readonly jwtKeySet: any;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.teamName = options.teamName;
|
||||
this.authHandler = options.authHandler;
|
||||
this.signInResolver = options.signInResolver;
|
||||
this.resolverContext = options.resolverContext;
|
||||
this.jwtKeySet = createRemoteJWKSet(
|
||||
new URL(
|
||||
`https://${this.teamName}.cloudflareaccess.com/cdn-cgi/access/certs`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
frameHandler(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async refresh(req: express.Request, res: express.Response): Promise<void> {
|
||||
// ProxiedSignInPage calls `/refresh` implicitly each time the backstage
|
||||
// app is refreshed on the browser.
|
||||
// User authentication is then checked here.
|
||||
const result = await this.getResult(req);
|
||||
const response = await this.handleResult(result);
|
||||
res.json(response);
|
||||
}
|
||||
|
||||
start(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private async getResult(
|
||||
req: express.Request,
|
||||
): Promise<CloudflareAccessResult> {
|
||||
// JWTs generated by Access are available in a request header as
|
||||
// Cf-Access-Jwt-Assertion and as cookies as CF_Authorization.
|
||||
let jwt = req.header(CF_JWT_HEADER);
|
||||
if (!jwt) {
|
||||
jwt = req.cookies.CF_Authorization;
|
||||
}
|
||||
if (!jwt) {
|
||||
// Only throw if both are not provided by Cloudflare Access since either
|
||||
// can be used.
|
||||
throw new AuthenticationError(
|
||||
`Missing ${CF_JWT_HEADER} from Cloudflare Access`,
|
||||
);
|
||||
}
|
||||
|
||||
// Cloudflare signs the JWT using the RSA Signature with SHA-256 (RS256).
|
||||
// RS256 follows an asymmetric algorithm; a private key signs the JWTs and
|
||||
// a separate public key verifies the signature.
|
||||
const verifyResult = await jwtVerify(jwt, this.jwtKeySet, {
|
||||
// Cloudflare signs the JWT using the RSA Signature with SHA-256 (RS256).
|
||||
algorithms: ['RS256'],
|
||||
issuer: `https://${this.teamName}.cloudflareaccess.com`,
|
||||
});
|
||||
const claims = verifyResult.payload as CloudflareAccessClaims;
|
||||
|
||||
const fullProfile: PassportProfile = {
|
||||
provider: 'cfAccess',
|
||||
id: claims.sub,
|
||||
displayName: _.startCase(
|
||||
claims.email.split('@')[0].toLowerCase().replace('.', ' '),
|
||||
),
|
||||
username: claims.email.split('@')[0].toLowerCase(),
|
||||
emails: [{ value: claims.email.toLowerCase() }],
|
||||
};
|
||||
|
||||
return {
|
||||
fullProfile,
|
||||
expiresInSeconds: claims.exp - claims.iat,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleResult(
|
||||
result: CloudflareAccessResult,
|
||||
): Promise<CloudflareAccessResponse> {
|
||||
const { profile } = await this.authHandler(result, this.resolverContext);
|
||||
const backstageIdentity = await this.signInResolver(
|
||||
{
|
||||
result,
|
||||
profile,
|
||||
},
|
||||
this.resolverContext,
|
||||
);
|
||||
|
||||
return {
|
||||
providerInfo: {
|
||||
expiresInSeconds: result.expiresInSeconds,
|
||||
},
|
||||
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity),
|
||||
profile,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth provider integration for Cloudflare Access auth
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const cfAccess = createAuthProviderIntegration({
|
||||
create(options?: {
|
||||
/**
|
||||
* The profile transformation function used to verify and convert the auth response
|
||||
* into the profile that will be presented to the user.
|
||||
*/
|
||||
authHandler?: AuthHandler<CloudflareAccessResult>;
|
||||
|
||||
/**
|
||||
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
|
||||
*/
|
||||
signIn: {
|
||||
/**
|
||||
* Maps an auth result to a Backstage identity for the user.
|
||||
*/
|
||||
resolver: SignInResolver<CloudflareAccessResult>;
|
||||
};
|
||||
}) {
|
||||
return ({ config, resolverContext }) => {
|
||||
const teamName = config.getString('teamName');
|
||||
|
||||
if (!options?.signIn.resolver) {
|
||||
throw new Error(
|
||||
'SignInResolver is required to use this authentication provider',
|
||||
);
|
||||
}
|
||||
|
||||
const authHandler: AuthHandler<CloudflareAccessResult> =
|
||||
options?.authHandler
|
||||
? options.authHandler
|
||||
: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
});
|
||||
|
||||
return new CloudflareAccessAuthProvider({
|
||||
teamName,
|
||||
signInResolver: options?.signIn.resolver,
|
||||
authHandler,
|
||||
resolverContext,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -20,6 +20,7 @@ export type {
|
||||
BitbucketOAuthResult,
|
||||
BitbucketPassportProfile,
|
||||
} from './bitbucket';
|
||||
export type { CloudflareAccessResult } from './cloudflare-access';
|
||||
export type { GithubOAuthResult } from './github';
|
||||
export type { OAuth2ProxyResult } from './oauth2-proxy';
|
||||
export type { OidcAuthResult } from './oidc';
|
||||
|
||||
@@ -18,6 +18,7 @@ import { atlassian } from './atlassian';
|
||||
import { auth0 } from './auth0';
|
||||
import { awsAlb } from './aws-alb';
|
||||
import { bitbucket } from './bitbucket';
|
||||
import { cfAccess } from './cloudflare-access';
|
||||
import { gcpIap } from './gcp-iap';
|
||||
import { github } from './github';
|
||||
import { gitlab } from './gitlab';
|
||||
@@ -75,4 +76,5 @@ export const defaultAuthProviderFactories: {
|
||||
awsalb: awsAlb.create(),
|
||||
bitbucket: bitbucket.create(),
|
||||
atlassian: atlassian.create(),
|
||||
cfaccess: cfAccess.create(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user