Merge pull request #25516 from jasondiazg/master

feat(cloudflare-auth-access-provider): Add support for cloudflare custom headers and cookie auth name
This commit is contained in:
Patrik Oldsberg
2024-08-13 15:18:18 +02:00
committed by GitHub
5 changed files with 118 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend-module-cloudflare-access-provider': minor
---
Support for Cloudflare Custom Headers and Custom Cookie Auth Name
+6
View File
@@ -32,6 +32,12 @@ auth:
serviceTokens:
- token: '1uh2fh19efvfh129f1f919u21f2f19jf2.access'
subject: 'bot-user@your-company.com'
# You can customize the header name that contains the jwt token, by default
# cf-access-jwt-assertion is used
jwtHeaderName: <my-header>
# You can customize the authorization cookie name, by default
# CF_Authorization is used
authorizationCookieName: <MY_CAUTHORIZATION_COOKIE_NAME>
# This picks what sign in resolver(s) you want to use.
signIn:
resolvers:
@@ -27,6 +27,8 @@ export interface Config {
token: string;
subject: string;
}>;
jwtHeaderName?: string;
authorizationCookieName?: string;
signIn?: {
resolvers: Array<
| { resolver: 'emailLocalPartMatchingUserEntityName' }
@@ -239,4 +239,89 @@ describe('helpers', () => {
token: token,
});
});
it('works for regular tokens, through jwtHeaderName header', async () => {
jest.useFakeTimers({
now: 1600000004000,
});
const helper = AuthHelper.fromConfig(
new ConfigReader({
teamName: 'mock-team',
jwtHeaderName: 'X-Auth-Token',
}),
{ cache },
);
const token = await tokenFactory.userToken();
const request = createRequest({
headers: { ['X-Auth-Token']: token },
});
const expected = {
cfIdentity: {
email: 'hello@example.com',
groups: [{ id: '123', email: 'foo@bar.com', name: 'foo' }],
id: '1234567890',
name: 'User Name',
},
claims: {
iss: `https://mock-team.cloudflareaccess.com`,
sub: '1234567890',
name: 'User Name',
iat: 1600000000,
exp: 1600000005,
},
expiresInSeconds: 5,
};
await expect(helper.authenticate(request)).resolves.toEqual({
...expected,
token: token,
});
expect(cache.set).toHaveBeenCalledTimes(1);
expect(cache.set.mock.calls[0][0]).toBe(
'providers/cloudflare-access/profile-v1/1234567890',
);
expect(JSON.parse(cache.set.mock.calls[0][1] as string)).toEqual(expected);
});
it('works for regular tokens, through authorizationCookieName cookie name', async () => {
jest.useFakeTimers({
now: 1600000004000,
});
const helper = AuthHelper.fromConfig(
new ConfigReader({
teamName: 'mock-team',
authorizationCookieName: 'CF_Auth',
}),
{ cache },
);
const token = await tokenFactory.userToken();
const request = createRequest({
cookies: { CF_Auth: token },
});
const expected = {
cfIdentity: {
email: 'hello@example.com',
groups: [{ id: '123', email: 'foo@bar.com', name: 'foo' }],
id: '1234567890',
name: 'User Name',
},
claims: {
iss: `https://mock-team.cloudflareaccess.com`,
sub: '1234567890',
name: 'User Name',
iat: 1600000000,
exp: 1600000005,
},
expiresInSeconds: 5,
};
await expect(helper.authenticate(request)).resolves.toEqual({
...expected,
token: token,
});
});
});
@@ -40,6 +40,10 @@ export class AuthHelper {
options?: { cache?: CacheService },
): AuthHelper {
const teamName = config.getString('teamName');
const jwtHeaderName =
config.getOptionalString('jwtHeaderName') ?? CF_JWT_HEADER;
const authorizationCookieName =
config.getOptionalString('authorizationCookieName') ?? COOKIE_AUTH_NAME;
const serviceTokens = (
config.getOptionalConfigArray('serviceTokens') ?? []
)?.map(cfg => {
@@ -53,12 +57,21 @@ export class AuthHelper {
new URL(`https://${teamName}.cloudflareaccess.com/cdn-cgi/access/certs`),
);
return new AuthHelper(teamName, serviceTokens, keySet, options?.cache);
return new AuthHelper(
teamName,
serviceTokens,
jwtHeaderName,
authorizationCookieName,
keySet,
options?.cache,
);
}
private constructor(
private readonly teamName: string,
private readonly serviceTokens: ServiceToken[],
private readonly jwtHeaderName: string,
private readonly authorizationCookieName: string,
private readonly keySet: ReturnType<typeof createRemoteJWKSet>,
private readonly cache?: CacheService,
) {}
@@ -66,15 +79,16 @@ export class AuthHelper {
async authenticate(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);
let jwt = req.header(this.jwtHeaderName);
if (!jwt) {
jwt = req.cookies.CF_Authorization;
jwt = req.cookies[this.authorizationCookieName];
}
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`,
`Missing ${this.jwtHeaderName} and
${this.authorizationCookieName} from Cloudflare Access`,
);
}
@@ -155,8 +169,8 @@ export class AuthHelper {
): 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}`);
headers.set(this.jwtHeaderName, jwt);
headers.set('cookie', `${this.authorizationCookieName}=${jwt}`);
try {
const res = await fetch(
`https://${this.teamName}.cloudflareaccess.com/cdn-cgi/access/get-identity`,