Merge pull request #9408 from backstage/freben/node-common-again
Add the `@backstage/plugin-auth-node` package
This commit is contained in:
@@ -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.
|
||||
@@ -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'];
|
||||
````
|
||||
@@ -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.
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
}),
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
```ts
|
||||
/// <reference types="node" />
|
||||
|
||||
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<BackstageIdentityResponse>;
|
||||
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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
export { createOidcRouter } from './router';
|
||||
export { IdentityClient } from './IdentityClient';
|
||||
export { TokenFactory } from './TokenFactory';
|
||||
export { DatabaseKeyStore } from './DatabaseKeyStore';
|
||||
export { MemoryKeyStore } from './MemoryKeyStore';
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
*/
|
||||
|
||||
export * from './service/router';
|
||||
export { IdentityClient } from './identity';
|
||||
export type { TokenIssuer } from './identity';
|
||||
export * from './providers';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<JWTPayload>
|
||||
|
||||
private getResult(req: express.Request): OAuth2ProxyResult<JWTPayload> {
|
||||
const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
|
||||
const jwt = IdentityClient.getBearerToken(authHeader);
|
||||
const jwt = getBearerTokenFromAuthorizationHeader(authHeader);
|
||||
|
||||
if (!jwt) {
|
||||
throw new AuthenticationError(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ProviderInfo> = {
|
||||
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<TAuthResult> = {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
# Auth Node
|
||||
|
||||
Common functionality and types for the Backstage `auth` plugin.
|
||||
@@ -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<BackstageIdentityResponse>;
|
||||
static create(options: {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
issuer: string;
|
||||
}): IdentityClient;
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
+58
-56
@@ -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<string, string> {
|
||||
use: 'sig';
|
||||
alg: string;
|
||||
kid: string;
|
||||
kty: string;
|
||||
}
|
||||
|
||||
// Simplified copy of TokenFactory in @backstage/plugin-auth-backend
|
||||
class FakeTokenFactory {
|
||||
private readonly keys = new Array<AnyJWK>();
|
||||
|
||||
constructor(
|
||||
private readonly options: {
|
||||
issuer: string;
|
||||
keyDurationSeconds: number;
|
||||
},
|
||||
) {}
|
||||
|
||||
async issueToken(params: {
|
||||
claims: {
|
||||
sub: string;
|
||||
ent?: string[];
|
||||
};
|
||||
}): Promise<string> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
+21
-21
@@ -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(
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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 {};
|
||||
@@ -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[];
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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": "*",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AuthorizeRequest>,
|
||||
res: Response<AuthorizeResponse>,
|
||||
) => {
|
||||
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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user