update the jwks external auth to use singular nouns
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Vendored
+10
-10
@@ -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;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
+67
-1
@@ -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) {
|
||||
|
||||
+8
-3
@@ -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',
|
||||
|
||||
+16
-18
@@ -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();
|
||||
|
||||
+4
-3
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user