update the jwks external auth to use singular nouns

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-05-22 16:40:53 +02:00
parent 0639b07aa1
commit c2ea75f473
6 changed files with 112 additions and 46 deletions
+10 -10
View File
@@ -226,30 +226,30 @@ export interface Config {
type: 'jwks';
options: {
/**
* Sets the algorithms that should be used to verify the JWT tokens.
* The full URL of the JWKS endpoint.
*/
url: string;
/**
* Sets the algorithm(s) that should be used to verify the JWT tokens.
* The passed JWTs must have been signed using one of the listed algorithms.
*/
algorithms?: string[];
algorithm?: string | string[];
/**
* Sets the issuers that should be used to verify the JWT tokens.
* Sets the issuer(s) that should be used to verify the JWT tokens.
* Passed JWTs must have an `iss` claim which matches one of the specified issuers.
*/
issuers?: string[];
issuer?: string | string[];
/**
* Sets the audiences that should be used to verify the JWT tokens.
* Sets the audience(s) that should be used to verify the JWT tokens.
* The passed JWTs must have an "aud" claim that matches one of the audiences specified,
* or have no audience specified.
*/
audiences?: string[];
audience?: string | string[];
/**
* Sets an optional subject prefix. Passes the subject to called plugins.
* Useful for debugging and tracking purposes.
*/
subjectPrefix?: string;
/**
* Sets the URL containing the JWKS endpoint.
*/
url: string;
};
}
>;
@@ -15,8 +15,74 @@
*/
import { ConfigReader } from '@backstage/config';
import { readAccessRestrictionsFromConfig } from './helpers';
import {
readAccessRestrictionsFromConfig,
readStringOrStringArrayFromConfig,
} from './helpers';
import { JsonObject } from '@backstage/types';
import { mockServices } from '@backstage/backend-test-utils';
describe('readStringOrStringArrayFromConfig', () => {
it('handles all cases correctly', () => {
const config = mockServices.rootConfig({
data: {
wrongType: 1,
wrongTypeInArray: [1],
singleString: 'a',
spaceSeparatedString: 'a b c',
commaSeparatedString: 'a,b,c',
mixedSeparatorsString: 'a b,c ,, d',
emptyString: '',
emptyArray: [],
simpleArray: ['a', 'b', 'c'],
arrayWithSeparators: ['a b', 'c,d', 'e'],
complexDuplicates: ['a', 'a b', 'a', 'b, a'],
},
});
expect(() =>
readStringOrStringArrayFromConfig(config, 'wrongType'),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid type in config for key 'wrongType' in 'mock-config', got number, wanted string"`,
);
expect(() =>
readStringOrStringArrayFromConfig(config, 'wrongTypeInArray'),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid type in config for key 'wrongTypeInArray[0]' in 'mock-config', got number, wanted string-array"`,
);
expect(readStringOrStringArrayFromConfig(config, 'singleString')).toEqual([
'a',
]);
expect(
readStringOrStringArrayFromConfig(config, 'spaceSeparatedString'),
).toEqual(['a', 'b', 'c']);
expect(
readStringOrStringArrayFromConfig(config, 'commaSeparatedString'),
).toEqual(['a', 'b', 'c']);
expect(
readStringOrStringArrayFromConfig(config, 'mixedSeparatorsString'),
).toEqual(['a', 'b', 'c', 'd']);
expect(() =>
readStringOrStringArrayFromConfig(config, 'emptyString'),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid type in config for key 'emptyString' in 'mock-config', got empty-string, wanted string"`,
);
expect(
readStringOrStringArrayFromConfig(config, 'emptyArray'),
).toBeUndefined();
expect(readStringOrStringArrayFromConfig(config, 'simpleArray')).toEqual([
'a',
'b',
'c',
]);
expect(
readStringOrStringArrayFromConfig(config, 'arrayWithSeparators'),
).toEqual(['a', 'b', 'c', 'd', 'e']);
expect(
readStringOrStringArrayFromConfig(config, 'complexDuplicates'),
).toEqual(['a', 'b']);
});
});
describe('readAccessRestrictionsFromConfig', () => {
function r(config: JsonObject) {
@@ -66,8 +66,10 @@ export function readAccessRestrictionsFromConfig(
* splits by comma/space into a string array. Can also validate against a known
* set of values. Returns undefined if the key didn't exist or if the array
* would have ended up being empty.
*
* @internal
*/
function stringOrStringArray<T extends string>(
export function readStringOrStringArrayFromConfig<T extends string>(
root: Config,
key: string,
validValues?: readonly T[],
@@ -108,7 +110,10 @@ function stringOrStringArray<T extends string>(
}
function readPermissionNames(externalAccessEntryConfig: Config) {
return stringOrStringArray(externalAccessEntryConfig, 'permission');
return readStringOrStringArrayFromConfig(
externalAccessEntryConfig,
'permission',
);
}
function readPermissionAttributes(externalAccessEntryConfig: Config) {
@@ -129,7 +134,7 @@ function readPermissionAttributes(externalAccessEntryConfig: Config) {
}
}
const action = stringOrStringArray(config, 'action', [
const action = readStringOrStringArrayFromConfig(config, 'action', [
'create',
'read',
'update',
@@ -101,9 +101,9 @@ describe('JWKSHandler', () => {
it('verifies token with valid entry', async () => {
const validEntry = {
url: `${mockBaseUrl}/.well-known/jwks.json`,
algorithms: ['RS256'],
issuers: [mockBaseUrl],
audiences: ['backstage'],
algorithm: 'RS256',
issuer: mockBaseUrl,
audience: 'backstage',
};
const jwksHandler = new JWKSHandler();
@@ -121,16 +121,16 @@ describe('JWKSHandler', () => {
it('skips invalid entry and continues verification', async () => {
const invalidEntry = {
url: `${mockBaseUrl}/.well-known/jwks.json`,
algorithms: ['RS256'],
issuers: ['fakeIssuer'],
audiences: ['fakeAud'],
algorithm: 'RS256',
issuer: ['fakeIssuer'],
audience: ['fakeAud'],
};
const validEntry = {
url: `${mockBaseUrl}/.well-known/jwks.json`,
algorithms: ['RS256'],
issuers: ['multiple-issuers', mockBaseUrl],
audiences: ['multiple-audiences', 'backstage'],
algorithm: 'RS256',
issuer: ['multiple-issuers', mockBaseUrl],
audience: ['multiple-audiences', 'backstage'],
};
const jwksHandler = new JWKSHandler();
@@ -149,16 +149,14 @@ describe('JWKSHandler', () => {
it('returns undefined if no valid entry found', async () => {
const invalidEntry1 = {
url: `${mockBaseUrl}/.well-known/jwks.json`,
algorithms: ['RS256'],
issuers: [mockBaseUrl],
audiences: [],
algorithm: 'RS256',
issuer: 'wrong',
};
const invalidEntry2 = {
url: `${mockBaseUrl}/.well-known/jwks.json`,
algorithms: ['HS256'],
issuers: [],
audiences: ['backstage'],
algorithm: ['HS256'],
audience: 'wrong',
};
const jwksHandler = new JWKSHandler();
@@ -201,9 +199,9 @@ describe('JWKSHandler', () => {
it('uses custom subject prefix if provided', async () => {
const validEntry = {
url: `${mockBaseUrl}/.well-known/jwks.json`,
algorithms: ['RS256'],
issuers: [mockBaseUrl],
audiences: ['backstage'],
algorithm: 'RS256',
issuer: mockBaseUrl,
audience: 'backstage',
subjectPrefix: 'custom-prefix',
};
const jwksHandler = new JWKSHandler();
@@ -16,6 +16,7 @@
import { jwtVerify, createRemoteJWKSet, JWTVerifyGetKey } from 'jose';
import { Config } from '@backstage/config';
import { readStringOrStringArrayFromConfig } from './helpers';
import { TokenHandler } from './types';
/**
@@ -34,9 +35,9 @@ export class JWKSHandler implements TokenHandler {
}> = [];
add(options: Config) {
const algorithms = options.getOptionalStringArray('algorithms');
const issuers = options.getOptionalStringArray('issuers');
const audiences = options.getOptionalStringArray('audiences');
const algorithms = readStringOrStringArrayFromConfig(options, 'algorithm');
const issuers = readStringOrStringArrayFromConfig(options, 'issuer');
const audiences = readStringOrStringArrayFromConfig(options, 'audience');
const subjectPrefix = options.getOptionalString('subjectPrefix');
const url = new URL(options.getString('url'));
const jwks = createRemoteJWKSet(url);