feat: first attempt at adding jwks-auth to external token handlers

Signed-off-by: Ryan Hanchett <ryan.hanchett@invitae.com>
This commit is contained in:
Ryan Hanchett
2024-05-06 11:51:16 -07:00
parent 9c45fa1399
commit 03045fc530
3 changed files with 108 additions and 0 deletions
+32
View File
@@ -81,6 +81,38 @@ header:
Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW
```
## JWKS Token Auth
This access method allows for external caller token authentication using configured JWKS.
This is useful for callers that are authenticating to your instance of Backstage with
third-party tools, such as Auth0.
You can configure this access method by adding one or more entries of type `jwks`
to the `backend.auth.externalAccess` app-config key:
```yaml title="in e.g. app-config.production.yaml"
backend:
auth:
externalAccess:
- type: jwks
options:
uri: https://example.com/.well-known/jwks.json
issuers:
- https://example.com
algorithms:
- RS256
audiences:
- example
- type: jwks
options:
uri: https://another-example.com/.well-known/jwks.json
issuers:
- https://example.com
```
The subject returned from the token verification will become part of the
credentials object that the request recipients get.
## Legacy Tokens
Plugins and backends that are _not_ on the new backend system use a legacy token
@@ -21,6 +21,7 @@ import {
import { LegacyTokenHandler } from './legacy';
import { StaticTokenHandler } from './static';
import { TokenHandler } from './types';
import { JWKSHandler } from './jwks';
const NEW_CONFIG_KEY = 'backend.auth.externalAccess';
const OLD_CONFIG_KEY = 'backend.auth.keys';
@@ -40,9 +41,11 @@ export class ExternalTokenHandler {
const staticHandler = new StaticTokenHandler();
const legacyHandler = new LegacyTokenHandler();
const jwksHandler = new JWKSHandler();
const handlers: Record<string, TokenHandler> = {
static: staticHandler,
legacy: legacyHandler,
jwks: jwksHandler,
};
// Load the new-style handlers
@@ -0,0 +1,73 @@
/*
* 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 { jwtVerify, createRemoteJWKSet } from 'jose';
import { Config } from '@backstage/config';
import { TokenHandler } from './types';
/**
* Handles `type: jwks` access.
*
* @internal
*/
export class JWKSHandler implements TokenHandler {
#entries: Array<{
algorithms: string[];
audiences: string[];
issuers: string[];
uri: string;
}> = [];
add(options: Config) {
const algorithms = options.getOptionalStringArray('algorithms') ?? [];
const issuers = options.getOptionalStringArray('issuers') ?? [];
const audiences = options.getOptionalStringArray('audiences') ?? [];
const uri = options.getString('uri');
if (!uri.match(/^\S+$/)) {
throw new Error('Illegal token, must be a set of non-space characters');
}
if (!issuers.every(issuer => issuer.match(/^\S+$/))) {
throw new Error('Illegal issuer, must be a set of non-space characters');
}
this.#entries.push({ algorithms, audiences, issuers, uri });
}
async verifyToken(token: string) {
// not sure if we would need to support multiple jwks entries, but implementing to match static/legacy token handlers
for (const entry of this.#entries) {
try {
const jwks = createRemoteJWKSet(new URL(entry.uri));
const {
payload: { sub },
} = await jwtVerify(token, jwks, {
algorithms: entry.algorithms,
issuer: entry.issuers,
audience: entry.audiences,
});
if (sub) {
return { subject: sub };
}
} catch {
continue;
}
}
return undefined;
}
}