Merge pull request #12234 from renlord/cloudflare-access

add cloudflare access authentication provider
This commit is contained in:
Ben Lambert
2022-07-18 18:04:29 +02:00
committed by GitHub
11 changed files with 848 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
add Cloudflare Access auth provider to auth-backend
+1
View File
@@ -362,3 +362,4 @@ Zhou
zoomable
zsh
Alef
Cloudflare
+113
View File
@@ -0,0 +1,113 @@
---
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" />,
```
## Adding the provider to the Backstage frontend
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 [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page to also work smoothly for local development.
+2 -1
View File
@@ -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
+48
View File
@@ -7,6 +7,7 @@
import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import { CacheClient } from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
@@ -170,6 +171,41 @@ export class CatalogIdentityClient {
resolveCatalogMembership(query: MemberClaimQuery): Promise<string[]>;
}
// @public
export type CloudflareAccessClaims = {
aud: string[];
email: string;
exp: number;
iat: number;
nonce: string;
identity_nonce: string;
sub: string;
iss: string;
custom: string;
};
// @public
export type CloudflareAccessGroup = {
id: string;
name: string;
email: string;
};
// @public
export type CloudflareAccessIdentityProfile = {
id: string;
name: string;
email: string;
groups: CloudflareAccessGroup[];
};
// @public (undocumented)
export type CloudflareAccessResult = {
claims: CloudflareAccessClaims;
cfIdentity: CloudflareAccessIdentityProfile;
expiresInSeconds?: number;
};
// @public
export type CookieConfigurer = (ctx: {
providerId: string;
@@ -472,6 +508,18 @@ export const providers: Readonly<{
userIdMatchingUserEntityAnnotation(): SignInResolver<OAuthResult>;
}>;
}>;
cfAccess: Readonly<{
create: (options: {
authHandler?: AuthHandler<CloudflareAccessResult> | undefined;
signIn: {
resolver: SignInResolver<CloudflareAccessResult>;
};
cache?: CacheClient | undefined;
}) => AuthProviderFactory;
resolvers: Readonly<{
emailMatchingUserEntityProfileEmail: () => SignInResolver<unknown>;
}>;
}>;
gcpIap: Readonly<{
create: (options: {
authHandler?: AuthHandler<GcpIapResult> | undefined;
+3
View File
@@ -116,6 +116,9 @@ export interface Config {
issuer?: string;
region: string;
};
cfaccess?: {
teamName: string;
};
};
};
}
@@ -0,0 +1,22 @@
/*
* 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 {
CloudflareAccessClaims,
CloudflareAccessGroup,
CloudflareAccessResult,
CloudflareAccessIdentityProfile,
} from './provider';
@@ -0,0 +1,267 @@
/*
* 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 { AuthResolverContext } from '../types';
import fetch from 'node-fetch';
const jwtMock = jwtVerify as jest.Mocked<any>;
const mockJwt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlVzZXIgTmFtZSIsImlhdCI6MTUxNjIzOTAyMn0.uMCSBGhij1xn5pnot8XgD-huQuTIBOFGs6kkW_p_X94';
const mockClaims = {
sub: '1234567890',
email: 'user.name@email.test',
iat: 1632833760,
exp: 1632833763,
iss: 'ISSUER_URL',
};
const mockCfIdentity = {
name: 'foo',
id: '123',
email: 'foo@bar.com',
groups: [
{
id: '123',
email: 'foo@bar.com',
name: 'foo',
},
],
};
const identityOkResponse = {
backstageIdentity: {
identity: {
ownershipEntityRefs: ['user:default/jimmymarkum'],
type: 'user',
userEntityRef: 'user:default/jimmymarkum',
},
token:
'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob',
},
profile: {
email: 'user.name@email.test',
},
providerInfo: {
cfAccessIdentityProfile: {
email: 'foo@bar.com',
groups: [
{
email: 'foo@bar.com',
id: '123',
name: 'foo',
},
],
id: '123',
name: 'foo',
},
claims: mockClaims,
expiresInSeconds: 3,
},
};
const mockAuthenticatedUserEmail = 'user.name@email.test';
const mockCacheClient = {
get: jest.fn(),
set: jest.fn(),
delete: jest.fn(),
};
jest.mock('jose');
jest.mock('node-fetch', () => {
const original = jest.requireActual('node-fetch');
return {
__esModule: true,
default: jest.fn(),
Headers: original.Headers,
};
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('CloudflareAccessAuthProvider', () => {
// Cloudflare access provides jwt in two ways.
const mockRequestWithJwtHeader = {
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 mockRequestWithJwtCookie = {
header: jest.fn(_ => {
return undefined;
}),
cookies: {
CF_Authorization: `${mockJwt}`,
},
} 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(),
status: jest.fn(),
} as unknown as express.Response;
const mockFetch = fetch as unknown as jest.Mocked<any>;
const provider = new CloudflareAccessAuthProvider({
teamName: 'foobar',
resolverContext: {} as AuthResolverContext,
authHandler: async ({ claims }) => ({
profile: {
email: claims.email,
},
}),
signInResolver: async () => {
return {
token:
'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob',
};
},
cache: mockCacheClient,
});
describe('when JWT is valid', () => {
it('returns cfidentity also when get-identity succeeds', async () => {
jwtMock.mockReturnValue(Promise.resolve({ payload: mockClaims }));
mockFetch.mockReturnValueOnce(
Promise.resolve({
ok: true,
status: 200,
json: () => {
return mockCfIdentity;
},
}),
);
await provider.refresh(mockRequestWithJwtHeader, mockResponse);
expect(mockResponse.json).toHaveBeenCalledWith(identityOkResponse);
});
it('should resolve when passed in cookie', async () => {
jwtMock.mockReturnValue(Promise.resolve({ payload: mockClaims }));
// when mockFetch resolves and there nothing gets returned from /get-identity
mockFetch.mockReturnValueOnce(
Promise.resolve({
ok: true,
status: 200,
json: () => {
return mockCfIdentity;
},
}),
);
await provider.refresh(mockRequestWithJwtCookie, mockResponse);
expect(mockResponse.json).toHaveBeenCalledWith(identityOkResponse);
});
it('should resolve an identity and populate access groups when there are groups', async () => {
// when get-identity api responds and responds with status 200
mockFetch.mockResolvedValueOnce(
Promise.resolve({
ok: () => {
return true;
},
status: 200,
json: () => {
return Promise.resolve({
name: 'foo',
id: '123',
email: 'foo@bar.com',
groups: [
{
id: '123',
email: 'foo@bar.com',
name: 'foo',
},
],
});
},
}),
);
jwtMock.mockReturnValueOnce(Promise.resolve({ payload: mockClaims }));
await provider.refresh(mockRequestWithJwtCookie, mockResponse);
expect(mockResponse.json).toHaveBeenCalledWith(identityOkResponse);
});
it('should throw an error when get-identity fails', async () => {
mockFetch.mockReturnValue(Promise.reject());
await expect(
provider.refresh(mockRequestWithJwtCookie, mockResponse),
).rejects.toThrowError();
});
});
describe('should fail when', () => {
it('JWT is missing', async () => {
await expect(
provider.refresh(mockRequestWithoutJwt, mockResponse),
).rejects.toThrow();
});
it('JWT is invalid', async () => {
jwtMock.mockImplementation(() => {
throw new Error('bad JWT');
});
await expect(
provider.refresh(mockRequestWithJwtCookie, mockResponse),
).rejects.toThrow();
await expect(
provider.refresh(mockRequestWithJwtHeader, mockResponse),
).rejects.toThrow();
jwtMock.mockReset();
});
it('SignInResolver rejects', async () => {
jwtMock.mockReturnValue(mockClaims);
await expect(
provider.refresh(mockRequestWithJwtCookie, mockResponse),
).rejects.toThrow();
await expect(
provider.refresh(mockRequestWithJwtHeader, mockResponse),
).rejects.toThrow();
jwtMock.mockReset();
});
it('AuthHandler rejects', async () => {
jwtMock.mockReturnValue(mockClaims);
await expect(
provider.refresh(mockRequestWithJwtCookie, mockResponse),
).rejects.toThrow();
await expect(
provider.refresh(mockRequestWithJwtHeader, mockResponse),
).rejects.toThrow();
jwtMock.mockReset();
});
});
});
@@ -0,0 +1,379 @@
/*
* 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 fetch, { Headers } from 'node-fetch';
import express from 'express';
import * as _ from 'lodash';
import { jwtVerify, createRemoteJWKSet } from 'jose';
import {
AuthenticationError,
ResponseError,
ForwardedError,
} from '@backstage/errors';
import { CacheClient } from '@backstage/backend-common';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
import { commonByEmailResolver } from '../resolvers';
// 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';
const COOKIE_AUTH_NAME = 'CF_Authorization';
const CACHE_PREFIX = 'providers/cloudflare-access/profile-v1';
/**
* Default cache TTL
*
* @public
*/
export const CF_DEFAULT_CACHE_TTL = 3600;
/** @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;
cache?: CacheClient;
};
/**
* CloudflareAccessClaims
*
* Can be used in externally provided auth handler or sign in resolver to
* enrich user profile for sign-in user entity
*
* @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 applications 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;
};
/**
* CloudflareAccessGroup
*
* @public
*/
export type CloudflareAccessGroup = {
/**
* Group id
*/
id: string;
/**
* Name of group as defined in Cloudflare zero trust dashboard
*/
name: string;
/**
* Access group email address
*/
email: string;
};
/**
* CloudflareAccessIdentityProfile
*
* Can be used in externally provided auth handler or sign in resolver to
* enrich user profile for sign-in user entity
*
* @public
*/
export type CloudflareAccessIdentityProfile = {
id: string;
name: string;
email: string;
groups: CloudflareAccessGroup[];
};
/**
*
* @public
*/
export type CloudflareAccessResult = {
claims: CloudflareAccessClaims;
cfIdentity: CloudflareAccessIdentityProfile;
expiresInSeconds?: number;
};
/**
* @public
*/
export type CloudflareAccessProviderInfo = {
/**
* Expiry of the access token in seconds.
*/
expiresInSeconds?: number;
/**
* Cloudflare access identity profile with cloudflare access groups
*/
cfAccessIdentityProfile?: CloudflareAccessIdentityProfile;
/**
* Cloudflare access claims
*/
claims: CloudflareAccessClaims;
};
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;
private readonly cache?: CacheClient;
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`,
),
);
this.cache = options.cache;
}
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 getIdentityProfile(
jwt: string,
): Promise<CloudflareAccessIdentityProfile> {
const headers = new Headers();
// set both headers just the way inbound responses are set
headers.set(CF_JWT_HEADER, jwt);
headers.set('cookie', `${COOKIE_AUTH_NAME}=${jwt}`);
try {
const res = await fetch(
`https://${this.teamName}.cloudflareaccess.com/cdn-cgi/access/get-identity`,
{ headers },
);
if (!res.ok) {
throw ResponseError.fromResponse(res);
}
const cfIdentity = await res.json();
return cfIdentity as unknown as CloudflareAccessIdentityProfile;
} catch (err) {
throw new ForwardedError('getIdentityProfile failed', err);
}
}
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, {
issuer: `https://${this.teamName}.cloudflareaccess.com`,
});
const sub = verifyResult.payload.sub;
const cfAccessResultStr = await this.cache?.get(`${CACHE_PREFIX}/${sub}`);
if (typeof cfAccessResultStr === 'string') {
return JSON.parse(cfAccessResultStr) as CloudflareAccessResult;
}
const claims = verifyResult.payload as CloudflareAccessClaims;
// Builds a passport profile from JWT claims first
try {
// If we successfully fetch the get-identity endpoint,
// We supplement the passport profile with richer user identity
// information here.
const cfIdentity = await this.getIdentityProfile(jwt);
// Stores a stringified JSON object in cfaccess provider cache only when
// we complete all steps
const cfAccessResult: CloudflareAccessResult = {
claims,
cfIdentity,
expiresInSeconds: claims.exp - claims.iat,
};
this.cache?.set(`${CACHE_PREFIX}/${sub}`, JSON.stringify(cfAccessResult));
return cfAccessResult;
} catch (err) {
throw new ForwardedError(
'Failed to populate access identity information',
err,
);
}
}
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,
claims: result.claims,
cfAccessIdentityProfile: result.cfIdentity,
},
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>;
};
/**
* CacheClient object that was configured for the Backstage backend,
* should be provided via the backend auth plugin.
*/
cache?: CacheClient;
}) {
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 ({ claims, cfIdentity }) => {
return {
profile: {
email: claims.email,
displayName: cfIdentity.name,
},
};
};
return new CloudflareAccessAuthProvider({
teamName,
signInResolver: options?.signIn.resolver,
authHandler,
resolverContext,
...(options.cache && { cache: options.cache }),
});
};
},
resolvers: {
/**
* Looks up the user by matching their email to the entity email.
*/
emailMatchingUserEntityProfileEmail: () => commonByEmailResolver,
},
});
@@ -20,6 +20,12 @@ export type {
BitbucketOAuthResult,
BitbucketPassportProfile,
} from './bitbucket';
export type {
CloudflareAccessClaims,
CloudflareAccessGroup,
CloudflareAccessResult,
CloudflareAccessIdentityProfile,
} 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';
@@ -41,6 +42,7 @@ export const providers = Object.freeze({
auth0,
awsAlb,
bitbucket,
cfAccess,
gcpIap,
github,
gitlab,