diff --git a/.changeset/giant-nails-grow.md b/.changeset/giant-nails-grow.md index be4d5793e7..f96efce2ba 100644 --- a/.changeset/giant-nails-grow.md +++ b/.changeset/giant-nails-grow.md @@ -2,6 +2,11 @@ '@backstage/plugin-auth-backend': minor --- +- Moved `IdentityClient`, `BackstageSignInResult`, `BackstageIdentityResponse`, + and `BackstageUserIdentity` to `@backstage/plugin-auth-node`. + +While moving over, `IdentityClient` was also changed in the following ways: + - Made `IdentityClient.listPublicKeys` private. It was only used in tests, and should not be part of the API surface of that class. - Removed the static `IdentityClient.getBearerToken`. It is now replaced by @@ -9,6 +14,3 @@ Since the `IdentityClient` interface is marked as experimental, this is a breaking change without a deprecation period. - -- Moved `BackstageSignInResult`, `BackstageIdentityResponse`, and - `BackstageUserIdentity` to `@backstage/plugin-auth-node`. diff --git a/packages/backend/package.json b/packages/backend/package.json index c2e8711757..1c709195b0 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -32,6 +32,7 @@ "@backstage/integration": "^0.7.2", "@backstage/plugin-app-backend": "^0.3.24-next.0", "@backstage/plugin-auth-backend": "^0.10.0-next.0", + "@backstage/plugin-auth-node": "^0.0.0", "@backstage/plugin-azure-devops-backend": "^0.3.3-next.0", "@backstage/plugin-badges-backend": "^0.1.18-next.0", "@backstage/plugin-catalog-backend": "^0.21.3-next.0", diff --git a/packages/backend/src/plugins/permission.ts b/packages/backend/src/plugins/permission.ts index 71a9b90311..6ba24ba1f7 100644 --- a/packages/backend/src/plugins/permission.ts +++ b/packages/backend/src/plugins/permission.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { IdentityClient } from '@backstage/plugin-auth-node'; import { createRouter } from '@backstage/plugin-permission-backend'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index e555eb4a36..66153fd0af 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -402,14 +402,6 @@ export type GoogleProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "IdentityClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export class IdentityClient { - constructor(options: { discovery: PluginEndpointDiscovery; issuer: string }); - authenticate(token: string | undefined): Promise; -} - // Warning: (ae-missing-release-tag) "microsoftEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/auth-backend/src/identity/index.ts b/plugins/auth-backend/src/identity/index.ts index 73858d7e07..cb2f369668 100644 --- a/plugins/auth-backend/src/identity/index.ts +++ b/plugins/auth-backend/src/identity/index.ts @@ -15,7 +15,6 @@ */ export { createOidcRouter } from './router'; -export { IdentityClient } from './IdentityClient'; export { TokenFactory } from './TokenFactory'; export { DatabaseKeyStore } from './DatabaseKeyStore'; export { MemoryKeyStore } from './MemoryKeyStore'; diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 66c1b42dd9..d0cade087e 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -21,7 +21,6 @@ */ export * from './service/router'; -export { IdentityClient } from './identity'; export type { TokenIssuer } from './identity'; export * from './providers'; diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index 7795cae2ca..7a04ba8181 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -4,6 +4,7 @@ ```ts import { Entity } from '@backstage/catalog-model'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; // @public export interface BackstageIdentityResponse extends BackstageSignInResult { @@ -30,4 +31,10 @@ export type BackstageUserIdentity = { export function getBearerTokenFromAuthorizationHeader( authorizationHeader: unknown, ): string | undefined; + +// @public +export class IdentityClient { + constructor(options: { discovery: PluginEndpointDiscovery; issuer: string }); + authenticate(token: string | undefined): Promise; +} ``` diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 6b66f37c6a..cec1006ef0 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -22,10 +22,15 @@ "@backstage/backend-common": "^0.10.7-next.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", + "@backstage/errors": "^0.2.0", + "jose": "^1.27.1", + "node-fetch": "^2.6.1", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.2-next.0" + "@backstage/cli": "^0.13.2-next.0", + "msw": "^0.35.0", + "uuid": "^8.0.0" }, "files": [ "dist" diff --git a/plugins/auth-backend/src/identity/IdentityClient.test.ts b/plugins/auth-node/src/IdentityClient.test.ts similarity index 81% rename from plugins/auth-backend/src/identity/IdentityClient.test.ts rename to plugins/auth-node/src/IdentityClient.test.ts index cf60fd0852..cddc5f33a2 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.test.ts +++ b/plugins/auth-node/src/IdentityClient.test.ts @@ -14,19 +14,61 @@ * limitations under the License. */ -import { JWT, JSONWebKey } from 'jose'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { JSONWebKey, JWK, JWS, JWT } from 'jose'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { - getVoidLogger, - PluginEndpointDiscovery, -} from '@backstage/backend-common'; +import { v4 as uuid } from 'uuid'; import { IdentityClient } from './IdentityClient'; -import { MemoryKeyStore } from './MemoryKeyStore'; -import { TokenFactory } from './TokenFactory'; -import { KeyStore } from './types'; -const logger = getVoidLogger(); +interface AnyJWK extends Record { + use: 'sig'; + alg: string; + kid: string; + kty: string; +} + +// Simplified copy of TokenFactory in @backstage/plugin-auth-backend +class FakeTokenFactory { + private readonly keys = new Array(); + + constructor( + private readonly options: { + issuer: string; + keyDurationSeconds: number; + }, + ) {} + + async issueToken(params: { + claims: { + sub: string; + ent?: string[]; + }; + }): Promise { + const key = await JWK.generate('EC', 'P-256', { + use: 'sig', + kid: uuid(), + alg: 'ES256', + }); + this.keys.push(key.toJWK(false) as unknown as AnyJWK); + + const iss = this.options.issuer; + const sub = params.claims.sub; + const ent = params.claims.ent; + const aud = 'backstage'; + const iat = Math.floor(Date.now() / 1000); + const exp = iat + this.options.keyDurationSeconds; + + return JWS.sign({ iss, sub, aud, iat, exp, ent }, key, { + alg: key.alg, + kid: key.kid, + }); + } + + async listPublicKeys(): Promise<{ keys: AnyJWK[] }> { + return { keys: this.keys }; + } +} function jwtKid(jwt: string): string { const { header } = JWT.decode(jwt, { complete: true }) as { @@ -48,8 +90,7 @@ const discovery: PluginEndpointDiscovery = { describe('IdentityClient', () => { let client: IdentityClient; - let factory: TokenFactory; - let keyStore: KeyStore; + let factory: FakeTokenFactory; const keyDurationSeconds = 5; beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); @@ -58,12 +99,9 @@ describe('IdentityClient', () => { beforeEach(() => { client = new IdentityClient({ discovery, issuer: mockBaseUrl }); - keyStore = new MemoryKeyStore(); - factory = new TokenFactory({ + factory = new FakeTokenFactory({ issuer: mockBaseUrl, - keyStore: keyStore, keyDurationSeconds, - logger, }); }); @@ -108,11 +146,9 @@ describe('IdentityClient', () => { }); it('should throw on incorrect issuer', async () => { - const hackerFactory = new TokenFactory({ + const hackerFactory = new FakeTokenFactory({ issuer: 'hacker', - keyStore, keyDurationSeconds, - logger, }); return expect(async () => { const token = await hackerFactory.issueToken({ @@ -137,11 +173,9 @@ describe('IdentityClient', () => { }); it('should throw on incorrect signing key', async () => { - const hackerFactory = new TokenFactory({ + const hackerFactory = new FakeTokenFactory({ issuer: mockBaseUrl, - keyStore: new MemoryKeyStore(), keyDurationSeconds, - logger, }); return expect(async () => { const token = await hackerFactory.issueToken({ diff --git a/plugins/auth-backend/src/identity/IdentityClient.ts b/plugins/auth-node/src/IdentityClient.ts similarity index 95% rename from plugins/auth-backend/src/identity/IdentityClient.ts rename to plugins/auth-node/src/IdentityClient.ts index 6c6c2ca4b1..ddbccff027 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.ts +++ b/plugins/auth-node/src/IdentityClient.ts @@ -14,19 +14,20 @@ * limitations under the License. */ -import fetch from 'node-fetch'; -import { JWK, JWT, JWKS, JSONWebKey } from 'jose'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { AuthenticationError } from '@backstage/errors'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { JSONWebKey, JWK, JWKS, JWT } from 'jose'; +import fetch from 'node-fetch'; +import { BackstageIdentityResponse } from './types'; const CLOCK_MARGIN_S = 10; /** - * A identity client to interact with auth-backend - * and authenticate backstage identity tokens + * An identity client to interact with auth-backend and authenticate Backstage + * tokens * * @experimental This is not a stable API yet + * @public */ export class IdentityClient { private readonly discovery: PluginEndpointDiscovery; diff --git a/plugins/auth-node/src/index.ts b/plugins/auth-node/src/index.ts index 0ecee97e40..5551f2c85d 100644 --- a/plugins/auth-node/src/index.ts +++ b/plugins/auth-node/src/index.ts @@ -21,6 +21,7 @@ */ export { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; +export { IdentityClient } from './IdentityClient'; export type { BackstageIdentityResponse, BackstageSignInResult, diff --git a/plugins/permission-backend/api-report.md b/plugins/permission-backend/api-report.md index 4857c8b044..843c9a315c 100644 --- a/plugins/permission-backend/api-report.md +++ b/plugins/permission-backend/api-report.md @@ -4,7 +4,7 @@ ```ts import express from 'express'; -import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { IdentityClient } from '@backstage/plugin-auth-node'; import { Logger as Logger_2 } from 'winston'; import { PermissionPolicy } from '@backstage/plugin-permission-node'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 9ee959243d..01efa7af04 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -22,7 +22,6 @@ "@backstage/backend-common": "^0.10.7-next.0", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", - "@backstage/plugin-auth-backend": "^0.10.0-next.0", "@backstage/plugin-auth-node": "^0.0.0", "@backstage/plugin-permission-common": "^0.4.0", "@backstage/plugin-permission-node": "^0.4.3-next.0", diff --git a/plugins/permission-backend/src/service/router.test.ts b/plugins/permission-backend/src/service/router.test.ts index fd09cca828..498fcb748b 100644 --- a/plugins/permission-backend/src/service/router.test.ts +++ b/plugins/permission-backend/src/service/router.test.ts @@ -17,7 +17,7 @@ import express from 'express'; import request from 'supertest'; import { getVoidLogger } from '@backstage/backend-common'; -import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { IdentityClient } from '@backstage/plugin-auth-node'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ApplyConditionsRequestEntry, diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index e68f2497e4..413f3e6b45 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -23,10 +23,10 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; -import { IdentityClient } from '@backstage/plugin-auth-backend'; import { getBearerTokenFromAuthorizationHeader, BackstageIdentityResponse, + IdentityClient, } from '@backstage/plugin-auth-node'; import { AuthorizeResult,