implement external token access

Co-authored-by: Vincenzo Scamporlino <vincenzos@spotify.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-04-12 16:59:26 +02:00
parent 99305a09da
commit 00fca28b41
21 changed files with 1080 additions and 184 deletions
+98
View File
@@ -32,6 +32,104 @@ export interface Config {
* unless you configure credentials for service calls.
*/
dangerouslyDisableDefaultAuthPolicy?: boolean;
/**
* Configures methods of external access, ie ways for callers outside of
* the Backstage ecosystem to get authorized for access to APIs that do
* not permit unauthorized access.
*
* @deepVisibility secret
*/
externalAccess: Array<
| {
/**
* This is the legacy service-to-service access method, where a set
* of static keys were shared among plugins and used for symmetric
* signing and verification. These correspond to the old
* `backend.auth.keys` set and retain their behavior for backwards
* compatibility. Please migrate to other access methods when
* possible.
*
* Callers generate JWT tokens with the following payload:
*
* ```json
* {
* "sub": "backstage-plugin",
* "exp": <epoch seconds one hour in the future>
* }
* ```
*
* And sign them with HS256, using the base64 decoded secret. The
* tokens are then passed along with requests in the Authorization
* header:
*
* ```
* Authorization: Bearer eyJhbGciOiJIUzI...
* ```
*/
type: 'legacy';
options: {
/**
* Any set of base64 encoded random bytes to be used as both the
* signing and verification key. Should be sufficiently long so as
* not to be easy to guess by brute force.
*
* Can be generated eg using
*
* ```sh
* node -p 'require("crypto").randomBytes(24).toString("base64")'
* ```
*/
secret: string;
/**
* Sets the subject of the principal, when matching this token.
* Useful for debugging and tracking purposes.
*/
subject: string;
};
}
| {
/**
* This access method consists of random static tokens that can be
* handed out to callers.
*
* The tokens are then passed along verbatim with requests in the
* Authorization header:
*
* ```
* Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW
* ```
*/
type: 'static';
options: {
/**
* A raw token that can be any string, but for security reasons
* should be sufficiently long so as not to be easy to guess by
* brute force.
*
* Can be generated eg using
*
* ```sh
* node -p 'require("crypto").randomBytes(24).toString("base64")'
* ```
*
* Since the tokens can be any string, you are free to add
* additional identifying data to them if you like. For example,
* adding a `freben-local-dev-` prefix for debugging purposes to a
* token that you know will be handed out for use as a personal
* access token during development.
*/
token: string;
/**
* Sets the subject of the principal, when matching this token.
* Useful for debugging and tracking purposes.
*/
subject: string;
};
}
>;
};
};
@@ -26,8 +26,9 @@ import {
import { AuthenticationError } from '@backstage/errors';
import { JsonObject } from '@backstage/types';
import { decodeJwt } from 'jose';
import { PluginTokenHandler } from './PluginTokenHandler';
import { UserTokenHandler } from './UserTokenHandler';
import { ExternalTokenHandler } from './external/ExternalTokenHandler';
import { PluginTokenHandler } from './plugin/PluginTokenHandler';
import { UserTokenHandler } from './user/UserTokenHandler';
import {
createCredentialsWithNonePrincipal,
createCredentialsWithServicePrincipal,
@@ -39,12 +40,13 @@ import { KeyStore } from './types';
/** @internal */
export class DefaultAuthService implements AuthService {
constructor(
private readonly tokenManager: TokenManager,
private readonly userTokenHandler: UserTokenHandler,
private readonly pluginTokenHandler: PluginTokenHandler,
private readonly externalTokenHandler: ExternalTokenHandler,
private readonly tokenManager: TokenManager,
private readonly pluginId: string,
private readonly disableDefaultAuthPolicy: boolean,
private readonly publicKeyStore: KeyStore,
private readonly pluginTokenHandler: PluginTokenHandler,
) {}
// allowLimitedAccess is currently ignored, since we currently always use the full user tokens
@@ -78,14 +80,15 @@ export class DefaultAuthService implements AuthService {
);
}
// Legacy service-to-service token
const { sub, aud } = decodeJwt(token);
if (sub === 'backstage-server' && !aud) {
await this.tokenManager.authenticate(token);
return createCredentialsWithServicePrincipal('external:backstage-plugin');
const externalResult = await this.externalTokenHandler.verifyToken(token);
if (externalResult) {
return createCredentialsWithServicePrincipal(
externalResult.subject,
externalResult.token,
);
}
throw new AuthenticationError('Unknown token');
throw new AuthenticationError('Illegal token');
}
isPrincipal<TType extends keyof BackstagePrincipalTypes>(
@@ -20,8 +20,9 @@ import {
} from '@backstage/backend-plugin-api';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { DefaultAuthService } from './DefaultAuthService';
import { PluginTokenHandler } from './PluginTokenHandler';
import { UserTokenHandler } from './UserTokenHandler';
import { PluginTokenHandler } from './plugin/PluginTokenHandler';
import { UserTokenHandler } from './user/UserTokenHandler';
import { ExternalTokenHandler } from './external/ExternalTokenHandler';
/** @public */
export const authServiceFactory = createServiceFactory({
@@ -46,21 +47,34 @@ export const authServiceFactory = createServiceFactory({
),
);
const publicKeyStore = await DatabaseKeyStore.create({ database, logger });
const publicKeyStore = await DatabaseKeyStore.create({
database,
logger,
});
const userTokens = UserTokenHandler.create({
discovery,
});
const pluginTokens = PluginTokenHandler.create({
ownPluginId: plugin.getId(),
keyDurationSeconds: 60 * 60,
logger,
publicKeyStore,
discovery,
});
const externalTokens = ExternalTokenHandler.create({
config,
logger,
});
return new DefaultAuthService(
userTokens,
pluginTokens,
externalTokens,
tokenManager,
new UserTokenHandler({ discovery }),
plugin.getId(),
disableDefaultAuthPolicy,
publicKeyStore,
PluginTokenHandler.create({
ownPluginId: plugin.getId(),
keyDurationSeconds: 60 * 60,
logger,
publicKeyStore,
discovery,
}),
);
},
});
@@ -0,0 +1,91 @@
/*
* Copyright 2024 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 {
LoggerService,
RootConfigService,
} from '@backstage/backend-plugin-api';
import { LegacyTokenHandler } from './legacy';
import { StaticTokenHandler } from './static';
import { TokenHandler } from './types';
const NEW_CONFIG_KEY = 'backend.auth.externalAccess';
const OLD_CONFIG_KEY = 'backend.auth.keys';
/**
* Handles all types of external caller token types (i.e. not Backstage user
* tokens, nor Backstage backend plugin tokens).
*
* @internal
*/
export class ExternalTokenHandler {
static create(options: {
config: RootConfigService;
logger: LoggerService;
}): ExternalTokenHandler {
const { config, logger } = options;
const staticHandler = new StaticTokenHandler();
const legacyHandler = new LegacyTokenHandler();
const handlers: Record<string, TokenHandler> = {
static: staticHandler,
legacy: legacyHandler,
};
// Load the new-style handlers
const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? [];
for (const handlerConfig of handlerConfigs) {
const type = handlerConfig.getString('type');
const handler = handlers[type];
if (!handler) {
const valid = Object.keys(handlers)
.map(k => `'${k}'`)
.join(', ');
throw new Error(
`Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`,
);
}
handler.add(handlerConfig.getConfig('options'));
}
// Load the old keys too
const legacyConfigs = config.getOptionalConfigArray(OLD_CONFIG_KEY) ?? [];
if (legacyConfigs.length) {
logger.warn(
`DEPRECATION WARNING: The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`,
);
}
for (const handlerConfig of legacyConfigs) {
legacyHandler.addOld(handlerConfig);
}
return new ExternalTokenHandler(Object.values(handlers));
}
constructor(private readonly handlers: TokenHandler[]) {}
async verifyToken(
token: string,
): Promise<{ subject: string; token?: string } | undefined> {
for (const handler of this.handlers) {
const result = await handler.verifyToken(token);
if (result) {
return result;
}
}
return undefined;
}
}
@@ -0,0 +1,207 @@
/*
* Copyright 2024 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 { ConfigReader } from '@backstage/config';
import { randomBytes } from 'crypto';
import { SignJWT, importJWK } from 'jose';
import { DateTime } from 'luxon';
import { LegacyTokenHandler } from './legacy';
describe('LegacyTokenHandler', () => {
const tokenHandler = new LegacyTokenHandler();
const key1 = randomBytes(24);
const key2 = randomBytes(24);
const key3 = randomBytes(24);
tokenHandler.add(
new ConfigReader({
secret: key1.toString('base64'),
subject: 'key1',
}),
);
tokenHandler.add(
new ConfigReader({
secret: key2.toString('base64'),
subject: 'key2',
}),
);
tokenHandler.addOld(
new ConfigReader({
secret: key3.toString('base64'),
}),
);
it('should verify valid tokens', async () => {
const token1 = await new SignJWT({
sub: 'backstage-server',
exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(),
})
.setProtectedHeader({ alg: 'HS256' })
.sign(key1);
await expect(tokenHandler.verifyToken(token1)).resolves.toEqual({
subject: 'key1',
token: token1,
});
const token2 = await new SignJWT({
sub: 'backstage-server',
exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(),
})
.setProtectedHeader({ alg: 'HS256' })
.sign(key2);
await expect(tokenHandler.verifyToken(token2)).resolves.toEqual({
subject: 'key2',
token: token2,
});
const token3 = await new SignJWT({
sub: 'backstage-server',
exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(),
})
.setProtectedHeader({ alg: 'HS256' })
.sign(key3);
await expect(tokenHandler.verifyToken(token3)).resolves.toEqual({
subject: 'external:backstage-plugin',
token: token3,
});
});
it('should return undefined if the token is not a valid legacy token', async () => {
const validToken = await new SignJWT({
sub: 'backstage-serverrr',
exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(),
})
.setProtectedHeader({ alg: 'HS256' })
.sign(key1);
await expect(tokenHandler.verifyToken(validToken)).resolves.toBeUndefined();
await expect(
tokenHandler.verifyToken('statickeyblaaa'),
).resolves.toBeUndefined();
const randomToken = await new SignJWT({
sub: 'backstage-server',
exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(),
})
.setProtectedHeader({ alg: 'HS256' })
.sign(randomBytes(24));
await expect(
tokenHandler.verifyToken(randomToken),
).resolves.toBeUndefined();
const mockPublicKey = {
kty: 'EC',
x: 'GHlwg744e8JekzukPTdtix6R868D6fcWy0ooOx-NEZI',
y: 'Lyujcm0M6X9_yQi3l1eH09z0brU8K9cwrLml_fRFKro',
crv: 'P-256',
kid: 'mock',
alg: 'ES256',
};
const mockPrivateKey = {
...mockPublicKey,
d: 'KEn_mDqXYbZdRHb-JnCrW53LDOv5x4NL1FnlKcqBsFI',
};
const keyWithWrongAlg = await new SignJWT({
sub: 'backstage-server',
exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(),
})
.setProtectedHeader({ alg: 'ES256' })
.sign(await importJWK(mockPrivateKey));
await expect(
tokenHandler.verifyToken(keyWithWrongAlg),
).resolves.toBeUndefined();
});
it('should throw in case key uses a different payload', async () => {
const keyWithWrongExp = await new SignJWT({
sub: 'backstage-server',
// @ts-expect-error
exp: 'blaaah',
})
.setProtectedHeader({ alg: 'HS256' })
.sign(key1);
await expect(tokenHandler.verifyToken(keyWithWrongExp)).rejects.toThrow(
/\"exp\" claim must be a number/,
);
});
it('rejects bad config', () => {
const handler = new LegacyTokenHandler();
// new style add, bad secrets
expect(() =>
handler.add(new ConfigReader({ _missingsecret: true, subject: 'ok' })),
).toThrow(/secret/);
expect(() =>
handler.add(new ConfigReader({ secret: '', subject: 'ok' })),
).toThrow(/secret/);
expect(() =>
handler.add(new ConfigReader({ secret: 'has spaces', subject: 'ok' })),
).toThrow(/secret/);
expect(() =>
handler.add(new ConfigReader({ secret: 'hasnewline\n', subject: 'ok' })),
).toThrow(/secret/);
expect(() =>
handler.add(new ConfigReader({ secret: 3, subject: 'ok' })),
).toThrow(/secret/);
// new style add, bad subjects
expect(() =>
handler.add(new ConfigReader({ secret: 'b2s=', _missingsubject: true })),
).toThrow(/subject/);
expect(() =>
handler.add(new ConfigReader({ secret: 'b2s=', subject: '' })),
).toThrow(/subject/);
expect(() =>
handler.add(new ConfigReader({ secret: 'b2s=', subject: 'has spaces' })),
).toThrow(/subject/);
expect(() =>
handler.add(
new ConfigReader({ secret: 'b2s=', subject: 'hasnewline\n' }),
),
).toThrow(/subject/);
expect(() =>
handler.add(new ConfigReader({ secret: 'b2s=', subject: 3 })),
).toThrow(/subject/);
// old style add
expect(() =>
handler.addOld(new ConfigReader({ secret: 'b2s=' })),
).not.toThrow();
expect(() =>
handler.addOld(new ConfigReader({ _missingsecret: true })),
).toThrow(/secret/);
expect(() => handler.addOld(new ConfigReader({ secret: '' }))).toThrow(
/secret/,
);
expect(() =>
handler.addOld(new ConfigReader({ secret: 'has spaces' })),
).toThrow(/secret/);
expect(() =>
handler.addOld(new ConfigReader({ secret: 'hasnewline\n' })),
).toThrow(/secret/);
expect(() => handler.addOld(new ConfigReader({ secret: 3 }))).toThrow(
/secret/,
);
});
});
@@ -0,0 +1,99 @@
/*
* Copyright 2024 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 { Config } from '@backstage/config';
import { base64url, decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose';
import { TokenHandler } from './types';
/**
* Handles `type: legacy` access.
*
* @internal
*/
export class LegacyTokenHandler implements TokenHandler {
#entries: Array<{ key: Uint8Array; subject: string }> = [];
add(options: Config) {
this.#doAdd(options.getString('secret'), options.getString('subject'));
}
// used only for the old backend.auth.keys array
addOld(options: Config) {
// This choice of subject is for compatibility reasons
this.#doAdd(options.getString('secret'), 'external:backstage-plugin');
}
#doAdd(secret: string, subject: string) {
if (!secret.match(/^\S+$/)) {
throw new Error('Illegal secret, must be a valid base64 string');
}
let key: Uint8Array;
try {
key = base64url.decode(secret);
} catch {
throw new Error('Illegal secret, must be a valid base64 string');
}
if (!subject.match(/^\S+$/)) {
throw new Error('Illegal subject, must be a set of non-space characters');
}
this.#entries.push({ key, subject });
}
async verifyToken(
token: string,
): Promise<{ subject: string; token?: string } | undefined> {
// First do a duck typing check to see if it remotely looks like a legacy token
try {
// We do a fair amount of checking upfront here. Since we aren't certain
// that it's even the right type of key that we're looking at, we can't
// defer eg the alg check to jwtVerify, because it won't be possible to
// discern different reasons for key verification failures from each other
// easily
const { alg } = decodeProtectedHeader(token);
if (alg !== 'HS256') {
return undefined;
}
const { sub, aud } = decodeJwt(token);
if (sub !== 'backstage-server' || aud) {
return undefined;
}
} catch (e) {
// Doesn't look like a jwt at all
return undefined;
}
for (const entry of this.#entries) {
try {
await jwtVerify(token, entry.key);
return {
subject: entry.subject,
token: token,
};
} catch (e) {
if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') {
throw e;
}
// Otherwise continue to try the next key
}
}
// None of the signing keys matched
return undefined;
}
}
@@ -0,0 +1,77 @@
/*
* Copyright 2024 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 { ConfigReader } from '@backstage/config';
import { StaticTokenHandler } from './static';
describe('StaticTokenHandler', () => {
it('accepts any of the added list of tokens', async () => {
const handler = new StaticTokenHandler();
handler.add(new ConfigReader({ token: 'abc', subject: 'one' }));
handler.add(new ConfigReader({ token: 'def', subject: 'two' }));
await expect(handler.verifyToken('abc')).resolves.toEqual({
subject: 'one',
token: 'abc',
});
await expect(handler.verifyToken('def')).resolves.toEqual({
subject: 'two',
token: 'def',
});
await expect(handler.verifyToken('ghi')).resolves.toBeUndefined();
});
it('gracefully handles no added tokens', async () => {
const handler = new StaticTokenHandler();
await expect(handler.verifyToken('ghi')).resolves.toBeUndefined();
});
it('rejects bad config', () => {
const handler = new StaticTokenHandler();
expect(() =>
handler.add(new ConfigReader({ _missingtoken: true, subject: 'ok' })),
).toThrow(/token/);
expect(() =>
handler.add(new ConfigReader({ token: '', subject: 'ok' })),
).toThrow(/token/);
expect(() =>
handler.add(new ConfigReader({ token: 'has spaces', subject: 'ok' })),
).toThrow(/token/);
expect(() =>
handler.add(new ConfigReader({ token: 'hasnewline\n', subject: 'ok' })),
).toThrow(/token/);
expect(() =>
handler.add(new ConfigReader({ token: 3, subject: 'ok' })),
).toThrow(/token/);
expect(() =>
handler.add(new ConfigReader({ token: 'ok', _missingsubject: true })),
).toThrow(/subject/);
expect(() =>
handler.add(new ConfigReader({ token: 'ok', subject: '' })),
).toThrow(/subject/);
expect(() =>
handler.add(new ConfigReader({ token: 'ok', subject: 'has spaces' })),
).toThrow(/subject/);
expect(() =>
handler.add(new ConfigReader({ token: 'ok', subject: 'hasnewline\n' })),
).toThrow(/subject/);
expect(() =>
handler.add(new ConfigReader({ token: 'ok', subject: 3 })),
).toThrow(/subject/);
});
});
@@ -0,0 +1,55 @@
/*
* Copyright 2024 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 { Config } from '@backstage/config';
import { TokenHandler } from './types';
/**
* Handles `type: static` access.
*
* @internal
*/
export class StaticTokenHandler implements TokenHandler {
#entries: Array<{ token: string; subject: string }> = [];
add(options: Config) {
const token = options.getString('token');
if (!token.match(/^\S+$/)) {
throw new Error('Illegal token, must be a set of non-space characters');
}
const subject = options.getString('subject');
if (!subject.match(/^\S+$/)) {
throw new Error('Illegal subject, must be a set of non-space characters');
}
this.#entries.push({ token, subject });
}
async verifyToken(
token: string,
): Promise<{ subject: string; token: string } | undefined> {
const entry = this.#entries.find(e => e.token === token);
if (!entry) {
return undefined;
}
return {
subject: entry.subject,
token: token,
};
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2024 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 { Config } from '@backstage/config';
export interface TokenHandler {
add(options: Config): void;
verifyToken(
token: string,
): Promise<{ subject: string; token?: string } | undefined>;
}
@@ -25,11 +25,11 @@ import {
decodeProtectedHeader,
} from 'jose';
import { v4 as uuid } from 'uuid';
import { InternalKey, KeyStore } from './types';
import { InternalKey, KeyStore } from '../types';
import { AuthenticationError } from '@backstage/errors';
import { jwtVerify } from 'jose';
import { tokenTypes } from '@backstage/plugin-auth-node';
import { JwksClient } from './JwksClient';
import { JwksClient } from '../JwksClient';
/**
* The margin for how many times longer we make the public key available
@@ -69,7 +69,7 @@ describe('UserTokenHandler', () => {
beforeEach(() => {
jest.useRealTimers();
userTokenHandler = new UserTokenHandler({
userTokenHandler = UserTokenHandler.create({
discovery: mockServices.discovery(),
});
@@ -24,7 +24,7 @@ import {
jwtVerify,
JWTVerifyOptions,
} from 'jose';
import { JwksClient } from './JwksClient';
import { JwksClient } from '../JwksClient';
/**
* An identity client to interact with auth-backend and authenticate Backstage
@@ -33,29 +33,32 @@ import { JwksClient } from './JwksClient';
* @internal
*/
export class UserTokenHandler {
readonly #jwksClient: JwksClient;
readonly #algorithms?: string[];
constructor(options: { discovery: DiscoveryService }) {
this.#algorithms = ['ES256']; // TODO: configurable?
this.#jwksClient = new JwksClient(async () => {
static create(options: { discovery: DiscoveryService }): UserTokenHandler {
const algorithms = ['ES256']; // TODO: configurable?
const jwksClient = new JwksClient(async () => {
const url = await options.discovery.getBaseUrl('auth');
return new URL(`${url}/.well-known/jwks.json`);
});
return new UserTokenHandler(algorithms, jwksClient);
}
constructor(
private readonly algorithms: string[],
private readonly jwksClient: JwksClient,
) {}
async verifyToken(token: string) {
const verifyOpts = this.#getTokenVerificationOptions(token);
if (!verifyOpts) {
return undefined;
}
await this.#jwksClient.refreshKeyStore(token);
await this.jwksClient.refreshKeyStore(token);
// Verify a limited token, ensuring the necessarily claims are present and token type is correct
const { payload } = await jwtVerify(
token,
this.#jwksClient.getKey,
this.jwksClient.getKey,
verifyOpts,
).catch(e => {
throw new AuthenticationError('Invalid token', e);
@@ -76,7 +79,7 @@ export class UserTokenHandler {
if (typ === tokenTypes.user.typParam) {
return {
algorithms: this.#algorithms,
algorithms: this.algorithms,
requiredClaims: ['iat', 'exp', 'sub'],
typ: tokenTypes.user.typParam,
};
@@ -84,7 +87,7 @@ export class UserTokenHandler {
if (typ === tokenTypes.limitedUser.typParam) {
return {
algorithms: this.#algorithms,
algorithms: this.algorithms,
requiredClaims: ['iat', 'exp', 'sub'],
typ: tokenTypes.limitedUser.typParam,
};
@@ -93,7 +96,7 @@ export class UserTokenHandler {
const { aud } = decodeJwt(token);
if (aud === tokenTypes.user.audClaim) {
return {
algorithms: this.#algorithms,
algorithms: this.algorithms,
audience: tokenTypes.user.audClaim,
};
}