diff --git a/.changeset/famous-hats-decide.md b/.changeset/famous-hats-decide.md
new file mode 100644
index 0000000000..e6f3a4bd5a
--- /dev/null
+++ b/.changeset/famous-hats-decide.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-auth-node': minor
+---
+
+Added this package, to hold shared types and functionality that other backend
+packages need to import.
diff --git a/.changeset/giant-nails-grow.md b/.changeset/giant-nails-grow.md
new file mode 100644
index 0000000000..7ea36968b9
--- /dev/null
+++ b/.changeset/giant-nails-grow.md
@@ -0,0 +1,51 @@
+---
+'@backstage/plugin-auth-backend': minor
+---
+
+The following breaking changes were made, which may imply specifically needing
+to make small adjustments in your custom auth providers.
+
+- **BREAKING**: Moved `IdentityClient`, `BackstageSignInResult`,
+ `BackstageIdentityResponse`, and `BackstageUserIdentity` to
+ `@backstage/plugin-auth-node`.
+- **BREAKING**: Removed deprecated type `BackstageIdentity`, please use
+ `BackstageSignInResult` from `@backstage/plugin-auth-node` instead.
+
+While moving over, `IdentityClient` was also changed in the following ways:
+
+- **BREAKING**: Made `IdentityClient.listPublicKeys` private. It was only used
+ in tests, and should not be part of the API surface of that class.
+- **BREAKING**: Removed the static `IdentityClient.getBearerToken`. It is now
+ replaced by `getBearerTokenFromAuthorizationHeader` from
+ `@backstage/plugin-auth-node`.
+- **BREAKING**: Removed the constructor. Please use the `IdentityClient.create`
+ static method instead.
+
+Since the `IdentityClient` interface is marked as experimental, this is a
+breaking change without a deprecation period.
+
+In your auth providers, you may need to update your imports and usages as
+follows (example code; yours may be slightly different):
+
+````diff
+-import { IdentityClient } from '@backstage/plugin-auth-backend';
++import {
++ IdentityClient,
++ getBearerTokenFromAuthorizationHeader
++} from '@backstage/plugin-auth-node';
+
+ // ...
+
+- const identity = new IdentityClient({
++ const identity = IdentityClient.create({
+ discovery,
+ issuer: await discovery.getExternalBaseUrl('auth'),
+ });```
+
+ // ...
+
+ const token =
+- IdentityClient.getBearerToken(req.headers.authorization) ||
++ getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
+ req.cookies['token'];
+````
diff --git a/.changeset/little-onions-fly.md b/.changeset/little-onions-fly.md
new file mode 100644
index 0000000000..8bff96d866
--- /dev/null
+++ b/.changeset/little-onions-fly.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-permission-backend': patch
+'@backstage/plugin-search-backend': patch
+---
+
+Use `getBearerTokenFromAuthorizationHeader` from `@backstage/plugin-auth-node` instead of the deprecated `IdentityClient` method.
diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md
index 25dd08e054..45ae38b8b7 100644
--- a/contrib/docs/tutorials/authenticate-api-requests.md
+++ b/contrib/docs/tutorials/authenticate-api-requests.md
@@ -15,7 +15,10 @@ import cookieParser from 'cookie-parser';
import { Request, Response, NextFunction } from 'express';
import { JWT } from 'jose';
import { URL } from 'url';
-import { IdentityClient } from '@backstage/plugin-auth-backend';
+import {
+ IdentityClient,
+ getBearerTokenFromAuthorizationHeader,
+} from '@backstage/plugin-auth-node';
// ...
@@ -44,7 +47,7 @@ async function main() {
// ...
const discovery = SingleHostDiscovery.fromConfig(config);
- const identity = new IdentityClient({
+ const identity = IdentityClient.create({
discovery,
issuer: await discovery.getExternalBaseUrl('auth'),
});
@@ -58,7 +61,7 @@ async function main() {
) => {
try {
const token =
- IdentityClient.getBearerToken(req.headers.authorization) ||
+ getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
req.cookies['token'];
req.user = await identity.authenticate(token);
if (!req.headers.authorization) {
@@ -80,7 +83,7 @@ async function main() {
const apiRouter = Router();
apiRouter.use(cookieParser());
- // The auth route must be publically available as it is used during login
+ // The auth route must be publicly available as it is used during login
apiRouter.use('/auth', await auth(authEnv));
// Add a simple endpoint to be used when setting a token cookie
apiRouter.use('/cookie', authMiddleware, (_req, res) => {
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..276d16ff97 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 {
@@ -40,7 +40,7 @@ export default async function createPlugin(
logger,
discovery,
policy: new AllowAllPermissionPolicy(),
- identity: new IdentityClient({
+ identity: IdentityClient.create({
discovery,
issuer: await discovery.getExternalBaseUrl('auth'),
}),
diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md
index aaacd05815..416da59b46 100644
--- a/plugins/auth-backend/api-report.md
+++ b/plugins/auth-backend/api-report.md
@@ -5,12 +5,12 @@
```ts
///
+import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
+import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
-import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { JsonValue } from '@backstage/types';
-import { JSONWebKey } from 'jose';
import { Logger as Logger_2 } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
@@ -128,30 +128,6 @@ export type AwsAlbProviderOptions = {
};
};
-// @public @deprecated
-export type BackstageIdentity = BackstageSignInResult;
-
-// @public
-export interface BackstageIdentityResponse extends BackstageSignInResult {
- identity: BackstageUserIdentity;
-}
-
-// @public
-export interface BackstageSignInResult {
- // @deprecated
- entity?: Entity;
- // @deprecated
- id: string;
- token: string;
-}
-
-// @public
-export type BackstageUserIdentity = {
- type: 'user';
- userEntityRef: string;
- ownershipEntityRefs: string[];
-};
-
// Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -423,20 +399,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;
- static getBearerToken(
- authorizationHeader: string | undefined,
- ): string | undefined;
- listPublicKeys(): Promise<{
- keys: JSONWebKey[];
- }>;
-}
-
// 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/package.json b/plugins/auth-backend/package.json
index 3dab33d8da..4f2ef4e76c 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -30,6 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
+ "@backstage/plugin-auth-node": "^0.0.0",
"@backstage/backend-common": "^0.10.7-next.0",
"@backstage/catalog-client": "^0.5.5",
"@backstage/catalog-model": "^0.9.10",
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-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
index 82a43c7c64..75a3605483 100644
--- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
+++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
@@ -23,10 +23,12 @@ import {
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
- AuthProviderRouteHandlers,
- AuthProviderConfig,
BackstageIdentityResponse,
BackstageSignInResult,
+} from '@backstage/plugin-auth-node';
+import {
+ AuthProviderRouteHandlers,
+ AuthProviderConfig,
} from '../../providers/types';
import {
AuthenticationError,
diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts
index 6973e99569..6e5fe19859 100644
--- a/plugins/auth-backend/src/lib/oauth/types.ts
+++ b/plugins/auth-backend/src/lib/oauth/types.ts
@@ -16,11 +16,8 @@
import express from 'express';
import { Profile as PassportProfile } from 'passport';
-import {
- RedirectInfo,
- BackstageSignInResult,
- ProfileInfo,
-} from '../../providers/types';
+import { BackstageSignInResult } from '@backstage/plugin-auth-node';
+import { RedirectInfo, ProfileInfo } from '../../providers/types';
/**
* Common options for passport.js-based OAuth providers
diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts
index 1207254e11..78814ae7e6 100644
--- a/plugins/auth-backend/src/providers/index.ts
+++ b/plugins/auth-backend/src/providers/index.ts
@@ -48,13 +48,6 @@ export type {
// These types are needed for a postMessage from the login pop-up
// to the frontend
-export type {
- AuthResponse,
- BackstageIdentity,
- BackstageUserIdentity,
- BackstageIdentityResponse,
- BackstageSignInResult,
- ProfileInfo,
-} from './types';
+export type { AuthResponse, ProfileInfo } from './types';
export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse';
diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts
index 24bb4d0362..8c4dcc3249 100644
--- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts
+++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts
@@ -17,6 +17,7 @@
import express from 'express';
import { Logger } from 'winston';
import { AuthenticationError } from '@backstage/errors';
+import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
import {
AuthHandler,
SignInResolver,
@@ -26,7 +27,6 @@ import {
} from '../types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { JWT } from 'jose';
-import { IdentityClient } from '../../identity';
import { TokenIssuer } from '../../identity/types';
import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
@@ -156,7 +156,7 @@ export class Oauth2ProxyAuthProvider
private getResult(req: express.Request): OAuth2ProxyResult {
const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
- const jwt = IdentityClient.getBearerToken(authHeader);
+ const jwt = getBearerTokenFromAuthorizationHeader(authHeader);
if (!jwt) {
throw new AuthenticationError(
diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts
index e77812e750..9ccdf25f74 100644
--- a/plugins/auth-backend/src/providers/oidc/provider.ts
+++ b/plugins/auth-backend/src/providers/oidc/provider.ts
@@ -234,7 +234,7 @@ export const oAuth2DefaultSignInResolver: SignInResolver<
* can be passed while creating a OIDC provider.
*
* authHandler : called after sign in was successful, a new object must be returned which includes a profile
- * signInResolver: called after sign in was successful, expects to return a new {@link BackstageSignInResult}
+ * signInResolver: called after sign in was successful, expects to return a new {@link @backstage/plugin-auth-node#BackstageSignInResult}
*
* Both options are optional. There is fallback for authHandler where the default handler expect an e-mail explicitly
* otherwise it throws an error
diff --git a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts
index bb99cb3487..f3b234c08e 100644
--- a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts
+++ b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts
@@ -19,7 +19,10 @@ import {
parseEntityRef,
stringifyEntityRef,
} from '@backstage/catalog-model';
-import { BackstageIdentityResponse, BackstageSignInResult } from './types';
+import {
+ BackstageIdentityResponse,
+ BackstageSignInResult,
+} from '@backstage/plugin-auth-node';
function parseJwtPayload(token: string) {
const [_header, payload, _signature] = token.split('.');
@@ -28,7 +31,7 @@ function parseJwtPayload(token: string) {
/**
* Parses a Backstage-issued token and decorates the
- * {@link BackstageIdentityResponse} with identity information sourced from the
+ * {@link @backstage/plugin-auth-node#BackstageIdentityResponse} with identity information sourced from the
* token.
*
* @public
diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts
index f3a1a95919..5bd52f0c94 100644
--- a/plugins/auth-backend/src/providers/types.ts
+++ b/plugins/auth-backend/src/providers/types.ts
@@ -19,8 +19,11 @@ import {
TokenManager,
} from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
-import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
+import {
+ BackstageIdentityResponse,
+ BackstageSignInResult,
+} from '@backstage/plugin-auth-node';
import express from 'express';
import { Logger } from 'winston';
import { TokenIssuer } from '../identity/types';
@@ -161,85 +164,6 @@ export type AuthResponse = {
backstageIdentity?: BackstageIdentityResponse;
};
-/**
- * User identity information within Backstage.
- *
- * @public
- */
-export type BackstageUserIdentity = {
- /**
- * The type of identity that this structure represents. In the frontend app
- * this will currently always be 'user'.
- */
- type: 'user';
-
- /**
- * The entityRef of the user in the catalog.
- * For example User:default/sandra
- */
- userEntityRef: string;
-
- /**
- * The user and group entities that the user claims ownership through
- */
- ownershipEntityRefs: string[];
-};
-
-/**
- * A representation of a successful Backstage sign-in.
- *
- * Compared to the {@link BackstageIdentityResponse} this type omits
- * the decoded identity information embedded in the token.
- *
- * @public
- */
-export interface BackstageSignInResult {
- /**
- * An opaque ID that uniquely identifies the user within Backstage.
- *
- * This is typically the same as the user entity `metadata.name`.
- *
- * @deprecated Use the `identity` field instead
- */
- id: string;
-
- /**
- * The entity that the user is represented by within Backstage.
- *
- * This entity may or may not exist within the Catalog, and it can be used
- * to read and store additional metadata about the user.
- *
- * @deprecated Use the `identity` field instead.
- */
- entity?: Entity;
-
- /**
- * The token used to authenticate the user within Backstage.
- */
- token: string;
-}
-
-/**
- * The old exported symbol for {@link BackstageSignInResult}.
- *
- * @public
- * @deprecated Use the {@link BackstageSignInResult} instead.
- */
-export type BackstageIdentity = BackstageSignInResult;
-
-/**
- * Response object containing the {@link BackstageUserIdentity} and the token
- * from the authentication provider.
- *
- * @public
- */
-export interface BackstageIdentityResponse extends BackstageSignInResult {
- /**
- * A plaintext description of the identity that is encapsulated within the token.
- */
- identity: BackstageUserIdentity;
-}
-
/**
* Used to display login information to user, i.e. sidebar popup.
*
@@ -286,7 +210,7 @@ export type SignInInfo = {
/**
* Describes the function which handles the result of a successful
- * authentication. Must return a valid {@link BackstageSignInResult}.
+ * authentication. Must return a valid {@link @backstage/plugin-auth-node#BackstageSignInResult}.
*
* @public
*/
diff --git a/plugins/auth-node/.eslintrc.js b/plugins/auth-node/.eslintrc.js
new file mode 100644
index 0000000000..16a033dbc6
--- /dev/null
+++ b/plugins/auth-node/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint.backend')],
+};
diff --git a/plugins/auth-node/README.md b/plugins/auth-node/README.md
new file mode 100644
index 0000000000..3558e031b2
--- /dev/null
+++ b/plugins/auth-node/README.md
@@ -0,0 +1,3 @@
+# Auth Node
+
+Common functionality and types for the Backstage `auth` plugin.
diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md
new file mode 100644
index 0000000000..7840fc7d74
--- /dev/null
+++ b/plugins/auth-node/api-report.md
@@ -0,0 +1,43 @@
+## API Report File for "@backstage/plugin-auth-node"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { Entity } from '@backstage/catalog-model';
+import { PluginEndpointDiscovery } from '@backstage/backend-common';
+
+// @public
+export interface BackstageIdentityResponse extends BackstageSignInResult {
+ identity: BackstageUserIdentity;
+}
+
+// @public
+export interface BackstageSignInResult {
+ // @deprecated
+ entity?: Entity;
+ // @deprecated
+ id: string;
+ token: string;
+}
+
+// @public
+export type BackstageUserIdentity = {
+ type: 'user';
+ userEntityRef: string;
+ ownershipEntityRefs: string[];
+};
+
+// @public
+export function getBearerTokenFromAuthorizationHeader(
+ authorizationHeader: unknown,
+): string | undefined;
+
+// @public
+export class IdentityClient {
+ authenticate(token: string | undefined): Promise;
+ static create(options: {
+ discovery: PluginEndpointDiscovery;
+ issuer: string;
+ }): IdentityClient;
+}
+```
diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json
new file mode 100644
index 0000000000..cec1006ef0
--- /dev/null
+++ b/plugins/auth-node/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@backstage/plugin-auth-node",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": false,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "scripts": {
+ "build": "backstage-cli backend:build",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test",
+ "prepack": "backstage-cli prepack",
+ "postpack": "backstage-cli postpack",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@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",
+ "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 76%
rename from plugins/auth-backend/src/identity/IdentityClient.test.ts
rename to plugins/auth-node/src/IdentityClient.test.ts
index 9f5e1ce489..72ef7f2a57 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' }));
@@ -57,13 +98,10 @@ describe('IdentityClient', () => {
afterEach(() => server.resetHandlers());
beforeEach(() => {
- client = new IdentityClient({ discovery, issuer: mockBaseUrl });
- keyStore = new MemoryKeyStore();
- factory = new TokenFactory({
+ client = IdentityClient.create({ discovery, issuer: mockBaseUrl });
+ factory = new FakeTokenFactory({
issuer: mockBaseUrl,
- keyStore: keyStore,
keyDurationSeconds,
- logger,
});
});
@@ -83,7 +121,7 @@ describe('IdentityClient', () => {
it('should use the correct endpoint', async () => {
await factory.issueToken({ claims: { sub: 'foo' } });
const keys = await factory.listPublicKeys();
- const response = await client.listPublicKeys();
+ const response = await (client as any).listPublicKeys();
expect(response).toEqual(keys);
});
@@ -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({
@@ -199,38 +233,6 @@ describe('IdentityClient', () => {
});
});
- describe('getBearerToken', () => {
- it('should return undefined on undefined input', async () => {
- const token = IdentityClient.getBearerToken(undefined);
- expect(token).toBeUndefined();
- });
-
- it('should return undefined on malformed input', async () => {
- const token = IdentityClient.getBearerToken('malformed');
- expect(token).toBeUndefined();
- });
-
- it('should return undefined on unexpected scheme', async () => {
- const token = IdentityClient.getBearerToken('Basic token');
- expect(token).toBeUndefined();
- });
-
- it('should return Bearer token', async () => {
- const token = IdentityClient.getBearerToken('Bearer token');
- expect(token).toEqual('token');
- });
-
- it('should return Bearer token despite extra space', async () => {
- const token = IdentityClient.getBearerToken('Bearer \n token ');
- expect(token).toEqual('token');
- });
-
- it('should return Bearer token despite unconventionial case', async () => {
- const token = IdentityClient.getBearerToken('bEARER token');
- expect(token).toEqual('token');
- });
- });
-
describe('listPublicKeys', () => {
const defaultServiceResponse: {
keys: JSONWebKey[];
@@ -257,7 +259,7 @@ describe('IdentityClient', () => {
});
it('should use the correct endpoint', async () => {
- const response = await client.listPublicKeys();
+ const response = await (client as any).listPublicKeys();
expect(response).toEqual(defaultServiceResponse);
});
});
diff --git a/plugins/auth-backend/src/identity/IdentityClient.ts b/plugins/auth-node/src/IdentityClient.ts
similarity index 85%
rename from plugins/auth-backend/src/identity/IdentityClient.ts
rename to plugins/auth-node/src/IdentityClient.ts
index 552d231e1f..d8e841bf75 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 '../providers/types';
+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;
@@ -34,7 +35,20 @@ export class IdentityClient {
private keyStore: JWKS.KeyStore;
private keyStoreUpdated: number;
- constructor(options: { discovery: PluginEndpointDiscovery; issuer: string }) {
+ /**
+ * Create a new {@link IdentityClient} instance.
+ */
+ static create(options: {
+ discovery: PluginEndpointDiscovery;
+ issuer: string;
+ }): IdentityClient {
+ return new IdentityClient(options);
+ }
+
+ private constructor(options: {
+ discovery: PluginEndpointDiscovery;
+ issuer: string;
+ }) {
this.discovery = options.discovery;
this.issuer = options.issuer;
this.keyStore = new JWKS.KeyStore();
@@ -84,20 +98,6 @@ export class IdentityClient {
return user;
}
- /**
- * Parses the given authorization header and returns
- * the bearer token, or null if no bearer token is given
- */
- static getBearerToken(
- authorizationHeader: string | undefined,
- ): string | undefined {
- if (typeof authorizationHeader !== 'string') {
- return undefined;
- }
- const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
- return matches?.[1];
- }
-
/**
* Returns the public signing key matching the given jwt token,
* or null if no matching key was found
@@ -125,7 +125,7 @@ export class IdentityClient {
/**
* Lists public part of keys used to sign Backstage Identity tokens
*/
- async listPublicKeys(): Promise<{
+ private async listPublicKeys(): Promise<{
keys: JSONWebKey[];
}> {
const url = `${await this.discovery.getBaseUrl(
diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts
new file mode 100644
index 0000000000..0bd7b5ea93
--- /dev/null
+++ b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 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 { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader';
+
+describe('getBearerToken', () => {
+ it('should return undefined on bad input', async () => {
+ expect(getBearerTokenFromAuthorizationHeader(undefined)).toBeUndefined();
+ expect(getBearerTokenFromAuthorizationHeader(7)).toBeUndefined();
+ expect(
+ getBearerTokenFromAuthorizationHeader('Bearer \n token'),
+ ).toBeUndefined();
+ expect(
+ getBearerTokenFromAuthorizationHeader('Bearer token '),
+ ).toBeUndefined();
+ });
+
+ it('should return undefined on malformed input', async () => {
+ const token = getBearerTokenFromAuthorizationHeader('malformed');
+ expect(token).toBeUndefined();
+ });
+
+ it('should return undefined on unexpected scheme', async () => {
+ const token = getBearerTokenFromAuthorizationHeader('Basic token');
+ expect(token).toBeUndefined();
+ });
+
+ it('should return Bearer token', async () => {
+ const token = getBearerTokenFromAuthorizationHeader('Bearer token');
+ expect(token).toEqual('token');
+ });
+
+ it('should return Bearer token despite unconventional case', async () => {
+ const token = getBearerTokenFromAuthorizationHeader('bEARER token');
+ expect(token).toEqual('token');
+ });
+});
diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts
new file mode 100644
index 0000000000..8451cd6ab1
--- /dev/null
+++ b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 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.
+ */
+
+/**
+ * Parses the given authorization header and returns the bearer token, or
+ * undefined if no bearer token is given.
+ *
+ * @remarks
+ *
+ * This function is explicitly built to tolerate bad inputs safely, so you may
+ * call it directly with e.g. the output of `req.header('authorization')`
+ * without first checking that it exists.
+ *
+ * @public
+ */
+export function getBearerTokenFromAuthorizationHeader(
+ authorizationHeader: unknown,
+): string | undefined {
+ if (typeof authorizationHeader !== 'string') {
+ return undefined;
+ }
+ const matches = authorizationHeader.match(/^Bearer[ ]+(\S+)$/i);
+ return matches?.[1];
+}
diff --git a/plugins/auth-node/src/index.ts b/plugins/auth-node/src/index.ts
new file mode 100644
index 0000000000..5551f2c85d
--- /dev/null
+++ b/plugins/auth-node/src/index.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 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.
+ */
+
+/**
+ * Common functionality and types for the Backstage auth plugin.
+ *
+ * @packageDocumentation
+ */
+
+export { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader';
+export { IdentityClient } from './IdentityClient';
+export type {
+ BackstageIdentityResponse,
+ BackstageSignInResult,
+ BackstageUserIdentity,
+} from './types';
diff --git a/plugins/auth-node/src/setupTests.ts b/plugins/auth-node/src/setupTests.ts
new file mode 100644
index 0000000000..d3232290a7
--- /dev/null
+++ b/plugins/auth-node/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 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.
+ */
+
+export {};
diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts
new file mode 100644
index 0000000000..b574c2204d
--- /dev/null
+++ b/plugins/auth-node/src/types.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2022 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 { Entity } from '@backstage/catalog-model';
+
+/**
+ * A representation of a successful Backstage sign-in.
+ *
+ * Compared to the {@link BackstageIdentityResponse} this type omits
+ * the decoded identity information embedded in the token.
+ *
+ * @public
+ */
+export interface BackstageSignInResult {
+ /**
+ * An opaque ID that uniquely identifies the user within Backstage.
+ *
+ * This is typically the same as the user entity `metadata.name`.
+ *
+ * @deprecated Use the `identity` field instead
+ */
+ id: string;
+
+ /**
+ * The entity that the user is represented by within Backstage.
+ *
+ * This entity may or may not exist within the Catalog, and it can be used
+ * to read and store additional metadata about the user.
+ *
+ * @deprecated Use the `identity` field instead.
+ */
+ entity?: Entity;
+
+ /**
+ * The token used to authenticate the user within Backstage.
+ */
+ token: string;
+}
+
+/**
+ * Response object containing the {@link BackstageUserIdentity} and the token
+ * from the authentication provider.
+ *
+ * @public
+ */
+export interface BackstageIdentityResponse extends BackstageSignInResult {
+ /**
+ * A plaintext description of the identity that is encapsulated within the token.
+ */
+ identity: BackstageUserIdentity;
+}
+
+/**
+ * User identity information within Backstage.
+ *
+ * @public
+ */
+export type BackstageUserIdentity = {
+ /**
+ * The type of identity that this structure represents. In the frontend app
+ * this will currently always be 'user'.
+ */
+ type: 'user';
+
+ /**
+ * The entityRef of the user in the catalog.
+ * For example User:default/sandra
+ */
+ userEntityRef: string;
+
+ /**
+ * The user and group entities that the user claims ownership through
+ */
+ ownershipEntityRefs: string[];
+};
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 bc6faae17b..01efa7af04 100644
--- a/plugins/permission-backend/package.json
+++ b/plugins/permission-backend/package.json
@@ -22,7 +22,7 @@
"@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",
"@types/express": "*",
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 e22aec8129..413f3e6b45 100644
--- a/plugins/permission-backend/src/service/router.ts
+++ b/plugins/permission-backend/src/service/router.ts
@@ -24,9 +24,10 @@ import {
} from '@backstage/backend-common';
import { InputError } from '@backstage/errors';
import {
+ getBearerTokenFromAuthorizationHeader,
BackstageIdentityResponse,
IdentityClient,
-} from '@backstage/plugin-auth-backend';
+} from '@backstage/plugin-auth-node';
import {
AuthorizeResult,
AuthorizeDecision,
@@ -157,7 +158,9 @@ export async function createRouter(
req: Request,
res: Response,
) => {
- const token = IdentityClient.getBearerToken(req.header('authorization'));
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
const user = token ? await identity.authenticate(token) : undefined;
const parseResult = requestSchema.safeParse(req.body);
diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md
index 43bb548a06..534ad45110 100644
--- a/plugins/permission-node/api-report.md
+++ b/plugins/permission-node/api-report.md
@@ -7,7 +7,7 @@ import { AuthorizeDecision } from '@backstage/plugin-permission-common';
import { AuthorizeQuery } from '@backstage/plugin-permission-common';
import { AuthorizeRequestOptions } from '@backstage/plugin-permission-common';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
-import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend';
+import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import { Config } from '@backstage/config';
import express from 'express';
import { Identified } from '@backstage/plugin-permission-common';
diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json
index 6fb58251e3..7977e9057f 100644
--- a/plugins/permission-node/package.json
+++ b/plugins/permission-node/package.json
@@ -32,7 +32,7 @@
"@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",
"@types/express": "^4.17.6",
"express": "^4.17.1",
diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts
index 2e344d6d96..19b12b8f28 100644
--- a/plugins/permission-node/src/policy/types.ts
+++ b/plugins/permission-node/src/policy/types.ts
@@ -20,7 +20,7 @@ import {
PermissionCondition,
PermissionCriteria,
} from '@backstage/plugin-permission-common';
-import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend';
+import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
/**
* An authorization request to be evaluated by the {@link PermissionPolicy}.
diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json
index ee064851fa..3770dbd875 100644
--- a/plugins/search-backend/package.json
+++ b/plugins/search-backend/package.json
@@ -24,7 +24,7 @@
"@backstage/config": "^0.1.13",
"@backstage/errors": "^0.2.0",
"@backstage/search-common": "^0.2.2",
- "@backstage/plugin-auth-backend": "^0.10.0-next.0",
+ "@backstage/plugin-auth-node": "^0.0.0",
"@backstage/plugin-permission-common": "^0.4.0-next.0",
"@backstage/plugin-permission-node": "^0.4.3-next.0",
"@backstage/plugin-search-backend-node": "^0.4.5",
diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts
index 28a3247919..3f25c4f24a 100644
--- a/plugins/search-backend/src/service/router.ts
+++ b/plugins/search-backend/src/service/router.ts
@@ -22,7 +22,7 @@ import { errorHandler } from '@backstage/backend-common';
import { InputError } from '@backstage/errors';
import { Config } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
-import { IdentityClient } from '@backstage/plugin-auth-backend';
+import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
import { DocumentTypeInfo, SearchResultSet } from '@backstage/search-common';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
@@ -106,7 +106,9 @@ export async function createRouter(
}, pageCursor=${query.pageCursor ?? ''}`,
);
- const token = IdentityClient.getBearerToken(req.header('authorization'));
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
try {
const resultSet = await engine?.query(query, { token });
diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts
index 808af8754d..f026adbc9b 100644
--- a/scripts/api-extractor.ts
+++ b/scripts/api-extractor.ts
@@ -218,6 +218,7 @@ const NO_WARNING_PACKAGES = [
'packages/types',
'packages/release-manifests',
'packages/version-bridge',
+ 'plugins/auth-node',
'plugins/catalog-backend-module-ldap',
'plugins/catalog-backend-module-msgraph',
'plugins/catalog-common',