diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index c1d6643769..b9cc6151af 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -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 diff --git a/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts b/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts index 588a1f1794..79ad8dd3c4 100644 --- a/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts +++ b/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts @@ -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 = { static: staticHandler, legacy: legacyHandler, + jwks: jwksHandler, }; // Load the new-style handlers diff --git a/packages/backend-app-api/src/services/implementations/auth/external/jwks.ts b/packages/backend-app-api/src/services/implementations/auth/external/jwks.ts new file mode 100644 index 0000000000..6ca7d010b2 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/auth/external/jwks.ts @@ -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; + } +}