Merge pull request #9408 from backstage/freben/node-common-again

Add the `@backstage/plugin-auth-node` package
This commit is contained in:
Fredrik Adelöw
2022-02-10 11:08:32 +01:00
committed by GitHub
38 changed files with 500 additions and 237 deletions
+6
View File
@@ -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.
+51
View File
@@ -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'];
````
+6
View File
@@ -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) => {
+1
View File
@@ -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",
+2 -2
View File
@@ -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'),
}),
+2 -40
View File
@@ -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)
+1
View File
@@ -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';
-1
View File
@@ -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,
+2 -5
View File
@@ -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
+1 -8
View File
@@ -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
+5 -81
View File
@@ -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
*/
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+3
View File
@@ -0,0 +1,3 @@
# Auth Node
Common functionality and types for the Backstage `auth` plugin.
+43
View File
@@ -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;
}
```
+38
View File
@@ -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"
]
}
@@ -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);
});
});
@@ -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];
}
+29
View File
@@ -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';
+17
View File
@@ -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 {};
+88
View File
@@ -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[];
};
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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}.
+1 -1
View File
@@ -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",
+4 -2
View File
@@ -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 });
+1
View File
@@ -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',