@backstage/plugin-auth-node 0.0.0-nightly-20220211021943

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # @backstage/plugin-auth-node
2
+
3
+ ## 0.0.0-nightly-20220211021943
4
+
5
+ ### Patch Changes
6
+
7
+ - 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5
8
+ - Updated dependencies
9
+ - @backstage/backend-common@0.0.0-nightly-20220211021943
10
+ - @backstage/errors@0.0.0-nightly-20220211021943
11
+
12
+ ## 0.1.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 9058bb1b5e: Added this package, to hold shared types and functionality that other backend
17
+ packages need to import.
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+ - @backstage/backend-common@0.10.7
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Auth Node
2
+
3
+ Common functionality and types for the Backstage `auth` plugin.
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var errors = require('@backstage/errors');
6
+ var jose = require('jose');
7
+ var fetch = require('node-fetch');
8
+
9
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
10
+
11
+ var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
12
+
13
+ function getBearerTokenFromAuthorizationHeader(authorizationHeader) {
14
+ if (typeof authorizationHeader !== "string") {
15
+ return void 0;
16
+ }
17
+ const matches = authorizationHeader.match(/^Bearer[ ]+(\S+)$/i);
18
+ return matches == null ? void 0 : matches[1];
19
+ }
20
+
21
+ const CLOCK_MARGIN_S = 10;
22
+ class IdentityClient {
23
+ static create(options) {
24
+ return new IdentityClient(options);
25
+ }
26
+ constructor(options) {
27
+ this.discovery = options.discovery;
28
+ this.issuer = options.issuer;
29
+ this.keyStore = new jose.JWKS.KeyStore();
30
+ this.keyStoreUpdated = 0;
31
+ }
32
+ async authenticate(token) {
33
+ var _a;
34
+ if (!token) {
35
+ throw new errors.AuthenticationError("No token specified");
36
+ }
37
+ const key = await this.getKey(token);
38
+ if (!key) {
39
+ throw new errors.AuthenticationError("No signing key matching token found");
40
+ }
41
+ const decoded = jose.JWT.IdToken.verify(token, key, {
42
+ algorithms: ["ES256"],
43
+ audience: "backstage",
44
+ issuer: this.issuer
45
+ });
46
+ if (!decoded.sub) {
47
+ throw new errors.AuthenticationError("No user sub found in token");
48
+ }
49
+ const user = {
50
+ id: decoded.sub,
51
+ token,
52
+ identity: {
53
+ type: "user",
54
+ userEntityRef: decoded.sub,
55
+ ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
56
+ }
57
+ };
58
+ return user;
59
+ }
60
+ async getKey(rawJwtToken) {
61
+ const { header, payload } = jose.JWT.decode(rawJwtToken, {
62
+ complete: true
63
+ });
64
+ const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
65
+ const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
66
+ if (!keyStoreHasKey && issuedAfterLastRefresh) {
67
+ await this.refreshKeyStore();
68
+ }
69
+ return this.keyStore.get({ kid: header.kid });
70
+ }
71
+ async listPublicKeys() {
72
+ const url = `${await this.discovery.getBaseUrl("auth")}/.well-known/jwks.json`;
73
+ const response = await fetch__default["default"](url);
74
+ if (!response.ok) {
75
+ const payload = await response.text();
76
+ const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
77
+ throw new Error(message);
78
+ }
79
+ const publicKeys = await response.json();
80
+ return publicKeys;
81
+ }
82
+ async refreshKeyStore() {
83
+ const now = Date.now() / 1e3;
84
+ const publicKeys = await this.listPublicKeys();
85
+ this.keyStore = jose.JWKS.asKeyStore({
86
+ keys: publicKeys.keys.map((key) => key)
87
+ });
88
+ this.keyStoreUpdated = now;
89
+ }
90
+ }
91
+
92
+ exports.IdentityClient = IdentityClient;
93
+ exports.getBearerTokenFromAuthorizationHeader = getBearerTokenFromAuthorizationHeader;
94
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/getBearerTokenFromAuthorizationHeader.ts","../src/IdentityClient.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Parses the given authorization header and returns the bearer token, or\n * undefined if no bearer token is given.\n *\n * @remarks\n *\n * This function is explicitly built to tolerate bad inputs safely, so you may\n * call it directly with e.g. the output of `req.header('authorization')`\n * without first checking that it exists.\n *\n * @public\n */\nexport function getBearerTokenFromAuthorizationHeader(\n authorizationHeader: unknown,\n): string | undefined {\n if (typeof authorizationHeader !== 'string') {\n return undefined;\n }\n const matches = authorizationHeader.match(/^Bearer[ ]+(\\S+)$/i);\n return matches?.[1];\n}\n","/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PluginEndpointDiscovery } from '@backstage/backend-common';\nimport { AuthenticationError } from '@backstage/errors';\nimport { JSONWebKey, JWK, JWKS, JWT } from 'jose';\nimport fetch from 'node-fetch';\nimport { BackstageIdentityResponse } from './types';\n\nconst CLOCK_MARGIN_S = 10;\n\n/**\n * An identity client to interact with auth-backend and authenticate Backstage\n * tokens\n *\n * @experimental This is not a stable API yet\n * @public\n */\nexport class IdentityClient {\n private readonly discovery: PluginEndpointDiscovery;\n private readonly issuer: string;\n private keyStore: JWKS.KeyStore;\n private keyStoreUpdated: number;\n\n /**\n * Create a new {@link IdentityClient} instance.\n */\n static create(options: {\n discovery: PluginEndpointDiscovery;\n issuer: string;\n }): IdentityClient {\n return new IdentityClient(options);\n }\n\n private constructor(options: {\n discovery: PluginEndpointDiscovery;\n issuer: string;\n }) {\n this.discovery = options.discovery;\n this.issuer = options.issuer;\n this.keyStore = new JWKS.KeyStore();\n this.keyStoreUpdated = 0;\n }\n\n /**\n * Verifies the given backstage identity token\n * Returns a BackstageIdentity (user) matching the token.\n * The method throws an error if verification fails.\n */\n async authenticate(\n token: string | undefined,\n ): Promise<BackstageIdentityResponse> {\n // Extract token from header\n if (!token) {\n throw new AuthenticationError('No token specified');\n }\n // Get signing key matching token\n const key = await this.getKey(token);\n if (!key) {\n throw new AuthenticationError('No signing key matching token found');\n }\n // Verify token claims and signature\n // Note: Claims must match those set by TokenFactory when issuing tokens\n // Note: verify throws if verification fails\n const decoded = JWT.IdToken.verify(token, key, {\n algorithms: ['ES256'],\n audience: 'backstage',\n issuer: this.issuer,\n }) as { sub: string; ent: string[] };\n // Verified, return the matching user as BackstageIdentity\n // TODO: Settle internal user format/properties\n if (!decoded.sub) {\n throw new AuthenticationError('No user sub found in token');\n }\n\n const user: BackstageIdentityResponse = {\n id: decoded.sub,\n token,\n identity: {\n type: 'user',\n userEntityRef: decoded.sub,\n ownershipEntityRefs: decoded.ent ?? [],\n },\n };\n return user;\n }\n\n /**\n * Returns the public signing key matching the given jwt token,\n * or null if no matching key was found\n */\n private async getKey(rawJwtToken: string): Promise<JWK.Key | null> {\n const { header, payload } = JWT.decode(rawJwtToken, {\n complete: true,\n }) as {\n header: { kid: string };\n payload: { iat: number };\n };\n\n // Refresh public keys if needed\n // Add a small margin in case clocks are out of sync\n const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });\n const issuedAfterLastRefresh =\n payload?.iat && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;\n if (!keyStoreHasKey && issuedAfterLastRefresh) {\n await this.refreshKeyStore();\n }\n\n return this.keyStore.get({ kid: header.kid });\n }\n\n /**\n * Lists public part of keys used to sign Backstage Identity tokens\n */\n private async listPublicKeys(): Promise<{\n keys: JSONWebKey[];\n }> {\n const url = `${await this.discovery.getBaseUrl(\n 'auth',\n )}/.well-known/jwks.json`;\n const response = await fetch(url);\n\n if (!response.ok) {\n const payload = await response.text();\n const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;\n throw new Error(message);\n }\n\n const publicKeys: { keys: JSONWebKey[] } = await response.json();\n\n return publicKeys;\n }\n\n /**\n * Fetches public keys and caches them locally\n */\n private async refreshKeyStore(): Promise<void> {\n const now = Date.now() / 1000;\n const publicKeys = await this.listPublicKeys();\n this.keyStore = JWKS.asKeyStore({\n keys: publicKeys.keys.map(key => key as JSONWebKey),\n });\n this.keyStoreUpdated = now;\n }\n}\n"],"names":["JWKS","AuthenticationError","JWT","fetch"],"mappings":";;;;;;;;;;;;+CA6BE,qBACoB;AACpB,MAAI,OAAO,wBAAwB,UAAU;AAC3C,WAAO;AAAA;AAET,QAAM,UAAU,oBAAoB,MAAM;AAC1C,SAAO,mCAAU;AAAA;;ACbnB,MAAM,iBAAiB;qBASK;AAAA,SASnB,OAAO,SAGK;AACjB,WAAO,IAAI,eAAe;AAAA;AAAA,EAGpB,YAAY,SAGjB;AACD,SAAK,YAAY,QAAQ;AACzB,SAAK,SAAS,QAAQ;AACtB,SAAK,WAAW,IAAIA,UAAK;AACzB,SAAK,kBAAkB;AAAA;AAAA,QAQnB,aACJ,OACoC;AAhExC;AAkEI,QAAI,CAAC,OAAO;AACV,YAAM,IAAIC,2BAAoB;AAAA;AAGhC,UAAM,MAAM,MAAM,KAAK,OAAO;AAC9B,QAAI,CAAC,KAAK;AACR,YAAM,IAAIA,2BAAoB;AAAA;AAKhC,UAAM,UAAUC,SAAI,QAAQ,OAAO,OAAO,KAAK;AAAA,MAC7C,YAAY,CAAC;AAAA,MACb,UAAU;AAAA,MACV,QAAQ,KAAK;AAAA;AAIf,QAAI,CAAC,QAAQ,KAAK;AAChB,YAAM,IAAID,2BAAoB;AAAA;AAGhC,UAAM,OAAkC;AAAA,MACtC,IAAI,QAAQ;AAAA,MACZ;AAAA,MACA,UAAU;AAAA,QACR,MAAM;AAAA,QACN,eAAe,QAAQ;AAAA,QACvB,qBAAqB,cAAQ,QAAR,YAAe;AAAA;AAAA;AAGxC,WAAO;AAAA;AAAA,QAOK,OAAO,aAA8C;AACjE,UAAM,EAAE,QAAQ,YAAYC,SAAI,OAAO,aAAa;AAAA,MAClD,UAAU;AAAA;AAQZ,UAAM,iBAAiB,CAAC,CAAC,KAAK,SAAS,IAAI,EAAE,KAAK,OAAO;AACzD,UAAM,yBACJ,oCAAS,QAAO,QAAQ,MAAM,KAAK,kBAAkB;AACvD,QAAI,CAAC,kBAAkB,wBAAwB;AAC7C,YAAM,KAAK;AAAA;AAGb,WAAO,KAAK,SAAS,IAAI,EAAE,KAAK,OAAO;AAAA;AAAA,QAM3B,iBAEX;AACD,UAAM,MAAM,GAAG,MAAM,KAAK,UAAU,WAClC;AAEF,UAAM,WAAW,MAAMC,0BAAM;AAE7B,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,UAAU,MAAM,SAAS;AAC/B,YAAM,UAAU,uBAAuB,SAAS,UAAU,SAAS,eAAe;AAClF,YAAM,IAAI,MAAM;AAAA;AAGlB,UAAM,aAAqC,MAAM,SAAS;AAE1D,WAAO;AAAA;AAAA,QAMK,kBAAiC;AAC7C,UAAM,MAAM,KAAK,QAAQ;AACzB,UAAM,aAAa,MAAM,KAAK;AAC9B,SAAK,WAAWH,UAAK,WAAW;AAAA,MAC9B,MAAM,WAAW,KAAK,IAAI,SAAO;AAAA;AAEnC,SAAK,kBAAkB;AAAA;AAAA;;;;;"}
@@ -0,0 +1,124 @@
1
+ import { PluginEndpointDiscovery } from '@backstage/backend-common';
2
+ import { Entity } from '@backstage/catalog-model';
3
+
4
+ /**
5
+ * Parses the given authorization header and returns the bearer token, or
6
+ * undefined if no bearer token is given.
7
+ *
8
+ * @remarks
9
+ *
10
+ * This function is explicitly built to tolerate bad inputs safely, so you may
11
+ * call it directly with e.g. the output of `req.header('authorization')`
12
+ * without first checking that it exists.
13
+ *
14
+ * @public
15
+ */
16
+ declare function getBearerTokenFromAuthorizationHeader(authorizationHeader: unknown): string | undefined;
17
+
18
+ /**
19
+ * A representation of a successful Backstage sign-in.
20
+ *
21
+ * Compared to the {@link BackstageIdentityResponse} this type omits
22
+ * the decoded identity information embedded in the token.
23
+ *
24
+ * @public
25
+ */
26
+ interface BackstageSignInResult {
27
+ /**
28
+ * An opaque ID that uniquely identifies the user within Backstage.
29
+ *
30
+ * This is typically the same as the user entity `metadata.name`.
31
+ *
32
+ * @deprecated Use the `identity` field instead
33
+ */
34
+ id: string;
35
+ /**
36
+ * The entity that the user is represented by within Backstage.
37
+ *
38
+ * This entity may or may not exist within the Catalog, and it can be used
39
+ * to read and store additional metadata about the user.
40
+ *
41
+ * @deprecated Use the `identity` field instead.
42
+ */
43
+ entity?: Entity;
44
+ /**
45
+ * The token used to authenticate the user within Backstage.
46
+ */
47
+ token: string;
48
+ }
49
+ /**
50
+ * Response object containing the {@link BackstageUserIdentity} and the token
51
+ * from the authentication provider.
52
+ *
53
+ * @public
54
+ */
55
+ interface BackstageIdentityResponse extends BackstageSignInResult {
56
+ /**
57
+ * A plaintext description of the identity that is encapsulated within the token.
58
+ */
59
+ identity: BackstageUserIdentity;
60
+ }
61
+ /**
62
+ * User identity information within Backstage.
63
+ *
64
+ * @public
65
+ */
66
+ declare type BackstageUserIdentity = {
67
+ /**
68
+ * The type of identity that this structure represents. In the frontend app
69
+ * this will currently always be 'user'.
70
+ */
71
+ type: 'user';
72
+ /**
73
+ * The entityRef of the user in the catalog.
74
+ * For example User:default/sandra
75
+ */
76
+ userEntityRef: string;
77
+ /**
78
+ * The user and group entities that the user claims ownership through
79
+ */
80
+ ownershipEntityRefs: string[];
81
+ };
82
+
83
+ /**
84
+ * An identity client to interact with auth-backend and authenticate Backstage
85
+ * tokens
86
+ *
87
+ * @experimental This is not a stable API yet
88
+ * @public
89
+ */
90
+ declare class IdentityClient {
91
+ private readonly discovery;
92
+ private readonly issuer;
93
+ private keyStore;
94
+ private keyStoreUpdated;
95
+ /**
96
+ * Create a new {@link IdentityClient} instance.
97
+ */
98
+ static create(options: {
99
+ discovery: PluginEndpointDiscovery;
100
+ issuer: string;
101
+ }): IdentityClient;
102
+ private constructor();
103
+ /**
104
+ * Verifies the given backstage identity token
105
+ * Returns a BackstageIdentity (user) matching the token.
106
+ * The method throws an error if verification fails.
107
+ */
108
+ authenticate(token: string | undefined): Promise<BackstageIdentityResponse>;
109
+ /**
110
+ * Returns the public signing key matching the given jwt token,
111
+ * or null if no matching key was found
112
+ */
113
+ private getKey;
114
+ /**
115
+ * Lists public part of keys used to sign Backstage Identity tokens
116
+ */
117
+ private listPublicKeys;
118
+ /**
119
+ * Fetches public keys and caches them locally
120
+ */
121
+ private refreshKeyStore;
122
+ }
123
+
124
+ export { BackstageIdentityResponse, BackstageSignInResult, BackstageUserIdentity, IdentityClient, getBearerTokenFromAuthorizationHeader };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@backstage/plugin-auth-node",
3
+ "version": "0.0.0-nightly-20220211021943",
4
+ "main": "dist/index.cjs.js",
5
+ "types": "dist/index.d.ts",
6
+ "license": "Apache-2.0",
7
+ "private": false,
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "main": "dist/index.cjs.js",
11
+ "types": "dist/index.d.ts"
12
+ },
13
+ "scripts": {
14
+ "build": "backstage-cli backend:build",
15
+ "lint": "backstage-cli lint",
16
+ "test": "backstage-cli test",
17
+ "prepack": "backstage-cli prepack",
18
+ "postpack": "backstage-cli postpack",
19
+ "clean": "backstage-cli clean"
20
+ },
21
+ "dependencies": {
22
+ "@backstage/backend-common": "^0.0.0-nightly-20220211021943",
23
+ "@backstage/catalog-model": "^0.9.10",
24
+ "@backstage/config": "^0.1.13",
25
+ "@backstage/errors": "^0.0.0-nightly-20220211021943",
26
+ "jose": "^1.27.1",
27
+ "node-fetch": "^2.6.7",
28
+ "winston": "^3.2.1"
29
+ },
30
+ "devDependencies": {
31
+ "@backstage/cli": "^0.0.0-nightly-20220211021943",
32
+ "msw": "^0.35.0",
33
+ "uuid": "^8.0.0"
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ]
38
+ }