@backstage/plugin-auth-backend 0.25.1 → 0.25.2-next.1

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @backstage/plugin-auth-backend
2
2
 
3
+ ## 0.25.2-next.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e88cb70: Small internal refactor to move out the `userInfo` database from the `tokenIssuer`. Also removes `exp` from being stored in `UserInfo` and it's now replaced with `created_at` and `updated_at` in the database instead.
8
+ - 207778c: Internal refactor of OIDC endpoints and `UserInfoDatabase`
9
+
10
+ ## 0.25.2-next.0
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies
15
+ - @backstage/config@1.3.3-next.0
16
+ - @backstage/catalog-model@1.7.5-next.0
17
+ - @backstage/backend-plugin-api@1.4.1-next.0
18
+ - @backstage/plugin-auth-node@0.6.5-next.0
19
+ - @backstage/plugin-catalog-node@1.17.2-next.0
20
+
3
21
  ## 0.25.1
4
22
 
5
23
  ### Patch Changes
@@ -3,7 +3,7 @@
3
3
  var luxon = require('luxon');
4
4
 
5
5
  const TABLE = "user_info";
6
- class UserInfoDatabaseHandler {
6
+ class UserInfoDatabase {
7
7
  constructor(client) {
8
8
  this.client = client;
9
9
  }
@@ -11,9 +11,7 @@ class UserInfoDatabaseHandler {
11
11
  await this.client(TABLE).insert({
12
12
  user_entity_ref: userInfo.claims.sub,
13
13
  user_info: JSON.stringify(userInfo),
14
- exp: luxon.DateTime.fromSeconds(userInfo.claims.exp, {
15
- zone: "utc"
16
- }).toSQL({ includeOffset: false })
14
+ updated_at: luxon.DateTime.utc().toSQL({ includeOffset: false })
17
15
  }).onConflict("user_entity_ref").merge();
18
16
  }
19
17
  async getUserInfo(userEntityRef) {
@@ -24,7 +22,11 @@ class UserInfoDatabaseHandler {
24
22
  const userInfo = JSON.parse(info.user_info);
25
23
  return userInfo;
26
24
  }
25
+ static async create(options) {
26
+ const client = await options.database.get();
27
+ return new UserInfoDatabase(client);
28
+ }
27
29
  }
28
30
 
29
- exports.UserInfoDatabaseHandler = UserInfoDatabaseHandler;
30
- //# sourceMappingURL=UserInfoDatabaseHandler.cjs.js.map
31
+ exports.UserInfoDatabase = UserInfoDatabase;
32
+ //# sourceMappingURL=UserInfoDatabase.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserInfoDatabase.cjs.js","sources":["../../src/database/UserInfoDatabase.ts"],"sourcesContent":["/*\n * Copyright 2024 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 { DateTime } from 'luxon';\nimport { Knex } from 'knex';\n\nimport { AuthDatabase } from './AuthDatabase';\nimport { JsonObject } from '@backstage/types';\n\nconst TABLE = 'user_info';\n\ntype Row = {\n user_entity_ref: string;\n user_info: string;\n updated_at: string;\n};\n\ntype UserInfo = {\n claims: JsonObject;\n};\n\nexport class UserInfoDatabase {\n private constructor(private readonly client: Knex) {}\n\n async addUserInfo(userInfo: UserInfo): Promise<void> {\n await this.client<Row>(TABLE)\n .insert({\n user_entity_ref: userInfo.claims.sub as string,\n user_info: JSON.stringify(userInfo),\n updated_at: DateTime.utc().toSQL({ includeOffset: false }),\n })\n .onConflict('user_entity_ref')\n .merge();\n }\n\n async getUserInfo(userEntityRef: string): Promise<UserInfo | undefined> {\n const info = await this.client<Row>(TABLE)\n .where({ user_entity_ref: userEntityRef })\n .first();\n\n if (!info) {\n return undefined;\n }\n\n const userInfo = JSON.parse(info.user_info);\n return userInfo;\n }\n\n static async create(options: { database: AuthDatabase }) {\n const client = await options.database.get();\n return new UserInfoDatabase(client);\n }\n}\n"],"names":["DateTime"],"mappings":";;;;AAsBA,MAAM,KAAQ,GAAA,WAAA;AAYP,MAAM,gBAAiB,CAAA;AAAA,EACpB,YAA6B,MAAc,EAAA;AAAd,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA;AAAe,EAEpD,MAAM,YAAY,QAAmC,EAAA;AACnD,IAAA,MAAM,IAAK,CAAA,MAAA,CAAY,KAAK,CAAA,CACzB,MAAO,CAAA;AAAA,MACN,eAAA,EAAiB,SAAS,MAAO,CAAA,GAAA;AAAA,MACjC,SAAA,EAAW,IAAK,CAAA,SAAA,CAAU,QAAQ,CAAA;AAAA,MAClC,UAAA,EAAYA,eAAS,GAAI,EAAA,CAAE,MAAM,EAAE,aAAA,EAAe,OAAO;AAAA,KAC1D,CAAA,CACA,UAAW,CAAA,iBAAiB,EAC5B,KAAM,EAAA;AAAA;AACX,EAEA,MAAM,YAAY,aAAsD,EAAA;AACtE,IAAA,MAAM,IAAO,GAAA,MAAM,IAAK,CAAA,MAAA,CAAY,KAAK,CAAA,CACtC,KAAM,CAAA,EAAE,eAAiB,EAAA,aAAA,EAAe,CAAA,CACxC,KAAM,EAAA;AAET,IAAA,IAAI,CAAC,IAAM,EAAA;AACT,MAAO,OAAA,KAAA,CAAA;AAAA;AAGT,IAAA,MAAM,QAAW,GAAA,IAAA,CAAK,KAAM,CAAA,IAAA,CAAK,SAAS,CAAA;AAC1C,IAAO,OAAA,QAAA;AAAA;AACT,EAEA,aAAa,OAAO,OAAqC,EAAA;AACvD,IAAA,MAAM,MAAS,GAAA,MAAM,OAAQ,CAAA,QAAA,CAAS,GAAI,EAAA;AAC1C,IAAO,OAAA,IAAI,iBAAiB,MAAM,CAAA;AAAA;AAEtC;;;;"}
@@ -8,14 +8,12 @@ class StaticTokenIssuer {
8
8
  keyStore;
9
9
  sessionExpirationSeconds;
10
10
  omitClaimsFromToken;
11
- userInfoDatabaseHandler;
12
11
  constructor(options, keyStore) {
13
12
  this.issuer = options.issuer;
14
13
  this.logger = options.logger;
15
14
  this.sessionExpirationSeconds = options.sessionExpirationSeconds;
16
15
  this.keyStore = keyStore;
17
16
  this.omitClaimsFromToken = options.omitClaimsFromToken;
18
- this.userInfoDatabaseHandler = options.userInfoDatabaseHandler;
19
17
  }
20
18
  async issueToken(params) {
21
19
  const key = await this.getSigningKey();
@@ -25,8 +23,7 @@ class StaticTokenIssuer {
25
23
  keyDurationSeconds: this.sessionExpirationSeconds,
26
24
  logger: this.logger,
27
25
  omitClaimsFromToken: this.omitClaimsFromToken,
28
- params,
29
- userInfoDatabaseHandler: this.userInfoDatabaseHandler
26
+ params
30
27
  });
31
28
  }
32
29
  async getSigningKey() {
@@ -1 +1 @@
1
- {"version":3,"file":"StaticTokenIssuer.cjs.js","sources":["../../src/identity/StaticTokenIssuer.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { AnyJWK, TokenIssuer } from './types';\nimport { JWK } from 'jose';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { StaticKeyStore } from './StaticKeyStore';\nimport {\n BackstageSignInResult,\n TokenParams,\n} from '@backstage/plugin-auth-node';\nimport { UserInfoDatabaseHandler } from './UserInfoDatabaseHandler';\nimport { issueUserToken } from './issueUserToken';\n\nexport type Config = {\n publicKeyFile: string;\n privateKeyFile: string;\n keyId: string;\n algorithm?: string;\n};\n\nexport type Options = {\n logger: LoggerService;\n /** Value of the issuer claim in issued tokens */\n issuer: string;\n /** Expiration time of the JWT in seconds */\n sessionExpirationSeconds: number;\n /**\n * A list of claims to omit from issued tokens and only store in the user info database\n */\n omitClaimsFromToken?: string[];\n userInfoDatabaseHandler: UserInfoDatabaseHandler;\n};\n\n/**\n * A token issuer that issues tokens from predefined\n * public/private key pair stored in the static key store.\n */\nexport class StaticTokenIssuer implements TokenIssuer {\n private readonly issuer: string;\n private readonly logger: LoggerService;\n private readonly keyStore: StaticKeyStore;\n private readonly sessionExpirationSeconds: number;\n private readonly omitClaimsFromToken?: string[];\n private readonly userInfoDatabaseHandler: UserInfoDatabaseHandler;\n\n public constructor(options: Options, keyStore: StaticKeyStore) {\n this.issuer = options.issuer;\n this.logger = options.logger;\n this.sessionExpirationSeconds = options.sessionExpirationSeconds;\n this.keyStore = keyStore;\n this.omitClaimsFromToken = options.omitClaimsFromToken;\n this.userInfoDatabaseHandler = options.userInfoDatabaseHandler;\n }\n\n public async issueToken(params: TokenParams): Promise<BackstageSignInResult> {\n const key = await this.getSigningKey();\n\n return issueUserToken({\n issuer: this.issuer,\n key,\n keyDurationSeconds: this.sessionExpirationSeconds,\n logger: this.logger,\n omitClaimsFromToken: this.omitClaimsFromToken,\n params,\n userInfoDatabaseHandler: this.userInfoDatabaseHandler,\n });\n }\n\n private async getSigningKey(): Promise<JWK> {\n const { items: keys } = await this.keyStore.listKeys();\n if (keys.length >= 1) {\n return this.keyStore.getPrivateKey(keys[0].key.kid);\n }\n throw new Error('Keystore should hold at least 1 key');\n }\n\n public async listPublicKeys(): Promise<{ keys: AnyJWK[] }> {\n const { items: keys } = await this.keyStore.listKeys();\n return { keys: keys.map(({ key }) => key) };\n }\n}\n"],"names":["issueUserToken"],"mappings":";;;;AAmDO,MAAM,iBAAyC,CAAA;AAAA,EACnC,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,wBAAA;AAAA,EACA,mBAAA;AAAA,EACA,uBAAA;AAAA,EAEV,WAAA,CAAY,SAAkB,QAA0B,EAAA;AAC7D,IAAA,IAAA,CAAK,SAAS,OAAQ,CAAA,MAAA;AACtB,IAAA,IAAA,CAAK,SAAS,OAAQ,CAAA,MAAA;AACtB,IAAA,IAAA,CAAK,2BAA2B,OAAQ,CAAA,wBAAA;AACxC,IAAA,IAAA,CAAK,QAAW,GAAA,QAAA;AAChB,IAAA,IAAA,CAAK,sBAAsB,OAAQ,CAAA,mBAAA;AACnC,IAAA,IAAA,CAAK,0BAA0B,OAAQ,CAAA,uBAAA;AAAA;AACzC,EAEA,MAAa,WAAW,MAAqD,EAAA;AAC3E,IAAM,MAAA,GAAA,GAAM,MAAM,IAAA,CAAK,aAAc,EAAA;AAErC,IAAA,OAAOA,6BAAe,CAAA;AAAA,MACpB,QAAQ,IAAK,CAAA,MAAA;AAAA,MACb,GAAA;AAAA,MACA,oBAAoB,IAAK,CAAA,wBAAA;AAAA,MACzB,QAAQ,IAAK,CAAA,MAAA;AAAA,MACb,qBAAqB,IAAK,CAAA,mBAAA;AAAA,MAC1B,MAAA;AAAA,MACA,yBAAyB,IAAK,CAAA;AAAA,KAC/B,CAAA;AAAA;AACH,EAEA,MAAc,aAA8B,GAAA;AAC1C,IAAA,MAAM,EAAE,KAAO,EAAA,IAAA,KAAS,MAAM,IAAA,CAAK,SAAS,QAAS,EAAA;AACrD,IAAI,IAAA,IAAA,CAAK,UAAU,CAAG,EAAA;AACpB,MAAA,OAAO,KAAK,QAAS,CAAA,aAAA,CAAc,KAAK,CAAC,CAAA,CAAE,IAAI,GAAG,CAAA;AAAA;AAEpD,IAAM,MAAA,IAAI,MAAM,qCAAqC,CAAA;AAAA;AACvD,EAEA,MAAa,cAA8C,GAAA;AACzD,IAAA,MAAM,EAAE,KAAO,EAAA,IAAA,KAAS,MAAM,IAAA,CAAK,SAAS,QAAS,EAAA;AACrD,IAAO,OAAA,EAAE,MAAM,IAAK,CAAA,GAAA,CAAI,CAAC,EAAE,GAAA,EAAU,KAAA,GAAG,CAAE,EAAA;AAAA;AAE9C;;;;"}
1
+ {"version":3,"file":"StaticTokenIssuer.cjs.js","sources":["../../src/identity/StaticTokenIssuer.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { AnyJWK, TokenIssuer } from './types';\nimport { JWK } from 'jose';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { StaticKeyStore } from './StaticKeyStore';\nimport {\n BackstageSignInResult,\n TokenParams,\n} from '@backstage/plugin-auth-node';\nimport { issueUserToken } from './issueUserToken';\n\nexport type Config = {\n publicKeyFile: string;\n privateKeyFile: string;\n keyId: string;\n algorithm?: string;\n};\n\nexport type Options = {\n logger: LoggerService;\n /** Value of the issuer claim in issued tokens */\n issuer: string;\n /** Expiration time of the JWT in seconds */\n sessionExpirationSeconds: number;\n /**\n * A list of claims to omit from issued tokens and only store in the user info database\n */\n omitClaimsFromToken?: string[];\n};\n\n/**\n * A token issuer that issues tokens from predefined\n * public/private key pair stored in the static key store.\n */\nexport class StaticTokenIssuer implements TokenIssuer {\n private readonly issuer: string;\n private readonly logger: LoggerService;\n private readonly keyStore: StaticKeyStore;\n private readonly sessionExpirationSeconds: number;\n private readonly omitClaimsFromToken?: string[];\n\n public constructor(options: Options, keyStore: StaticKeyStore) {\n this.issuer = options.issuer;\n this.logger = options.logger;\n this.sessionExpirationSeconds = options.sessionExpirationSeconds;\n this.keyStore = keyStore;\n this.omitClaimsFromToken = options.omitClaimsFromToken;\n }\n\n public async issueToken(\n params: TokenParams & { claims: { ent: string[] } },\n ): Promise<BackstageSignInResult> {\n const key = await this.getSigningKey();\n\n return issueUserToken({\n issuer: this.issuer,\n key,\n keyDurationSeconds: this.sessionExpirationSeconds,\n logger: this.logger,\n omitClaimsFromToken: this.omitClaimsFromToken,\n params,\n });\n }\n\n private async getSigningKey(): Promise<JWK> {\n const { items: keys } = await this.keyStore.listKeys();\n if (keys.length >= 1) {\n return this.keyStore.getPrivateKey(keys[0].key.kid);\n }\n throw new Error('Keystore should hold at least 1 key');\n }\n\n public async listPublicKeys(): Promise<{ keys: AnyJWK[] }> {\n const { items: keys } = await this.keyStore.listKeys();\n return { keys: keys.map(({ key }) => key) };\n }\n}\n"],"names":["issueUserToken"],"mappings":";;;;AAiDO,MAAM,iBAAyC,CAAA;AAAA,EACnC,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,wBAAA;AAAA,EACA,mBAAA;AAAA,EAEV,WAAA,CAAY,SAAkB,QAA0B,EAAA;AAC7D,IAAA,IAAA,CAAK,SAAS,OAAQ,CAAA,MAAA;AACtB,IAAA,IAAA,CAAK,SAAS,OAAQ,CAAA,MAAA;AACtB,IAAA,IAAA,CAAK,2BAA2B,OAAQ,CAAA,wBAAA;AACxC,IAAA,IAAA,CAAK,QAAW,GAAA,QAAA;AAChB,IAAA,IAAA,CAAK,sBAAsB,OAAQ,CAAA,mBAAA;AAAA;AACrC,EAEA,MAAa,WACX,MACgC,EAAA;AAChC,IAAM,MAAA,GAAA,GAAM,MAAM,IAAA,CAAK,aAAc,EAAA;AAErC,IAAA,OAAOA,6BAAe,CAAA;AAAA,MACpB,QAAQ,IAAK,CAAA,MAAA;AAAA,MACb,GAAA;AAAA,MACA,oBAAoB,IAAK,CAAA,wBAAA;AAAA,MACzB,QAAQ,IAAK,CAAA,MAAA;AAAA,MACb,qBAAqB,IAAK,CAAA,mBAAA;AAAA,MAC1B;AAAA,KACD,CAAA;AAAA;AACH,EAEA,MAAc,aAA8B,GAAA;AAC1C,IAAA,MAAM,EAAE,KAAO,EAAA,IAAA,KAAS,MAAM,IAAA,CAAK,SAAS,QAAS,EAAA;AACrD,IAAI,IAAA,IAAA,CAAK,UAAU,CAAG,EAAA;AACpB,MAAA,OAAO,KAAK,QAAS,CAAA,aAAA,CAAc,KAAK,CAAC,CAAA,CAAE,IAAI,GAAG,CAAA;AAAA;AAEpD,IAAM,MAAA,IAAI,MAAM,qCAAqC,CAAA;AAAA;AACvD,EAEA,MAAa,cAA8C,GAAA;AACzD,IAAA,MAAM,EAAE,KAAO,EAAA,IAAA,KAAS,MAAM,IAAA,CAAK,SAAS,QAAS,EAAA;AACrD,IAAO,OAAA,EAAE,MAAM,IAAK,CAAA,GAAA,CAAI,CAAC,EAAE,GAAA,EAAU,KAAA,GAAG,CAAE,EAAA;AAAA;AAE9C;;;;"}
@@ -12,7 +12,6 @@ class TokenFactory {
12
12
  keyDurationSeconds;
13
13
  algorithm;
14
14
  omitClaimsFromToken;
15
- userInfoDatabaseHandler;
16
15
  keyExpiry;
17
16
  privateKeyPromise;
18
17
  constructor(options) {
@@ -22,7 +21,6 @@ class TokenFactory {
22
21
  this.keyDurationSeconds = options.keyDurationSeconds;
23
22
  this.algorithm = options.algorithm ?? "ES256";
24
23
  this.omitClaimsFromToken = options.omitClaimsFromToken;
25
- this.userInfoDatabaseHandler = options.userInfoDatabaseHandler;
26
24
  }
27
25
  async issueToken(params) {
28
26
  const key = await this.getKey();
@@ -32,8 +30,7 @@ class TokenFactory {
32
30
  keyDurationSeconds: this.keyDurationSeconds,
33
31
  logger: this.logger,
34
32
  omitClaimsFromToken: this.omitClaimsFromToken,
35
- params,
36
- userInfoDatabaseHandler: this.userInfoDatabaseHandler
33
+ params
37
34
  });
38
35
  }
39
36
  // This will be called by other services that want to verify ID tokens.
@@ -1 +1 @@
1
- {"version":3,"file":"TokenFactory.cjs.js","sources":["../../src/identity/TokenFactory.ts"],"sourcesContent":["/*\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 { exportJWK, generateKeyPair, JWK } from 'jose';\nimport { DateTime } from 'luxon';\nimport { v4 as uuid } from 'uuid';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport {\n BackstageSignInResult,\n TokenParams,\n tokenTypes,\n} from '@backstage/plugin-auth-node';\nimport { AnyJWK, KeyStore, TokenIssuer } from './types';\nimport { JsonValue } from '@backstage/types';\nimport { UserInfoDatabaseHandler } from './UserInfoDatabaseHandler';\nimport { issueUserToken } from './issueUserToken';\n\n/**\n * The payload contents of a valid Backstage JWT token\n */\nexport interface BackstageTokenPayload {\n /**\n * The issuer of the token, currently the discovery URL of the auth backend\n */\n iss: string;\n\n /**\n * The entity ref of the user\n */\n sub: string;\n\n /**\n * The entity refs that the user claims ownership through\n */\n ent: string[];\n\n /**\n * A hard coded audience string\n */\n aud: typeof tokenTypes.user.audClaim;\n\n /**\n * Standard expiry in epoch seconds\n */\n exp: number;\n\n /**\n * Standard issue time in epoch seconds\n */\n iat: number;\n\n /**\n * A separate user identity proof that the auth service can convert to a limited user token\n */\n uip: string;\n\n /**\n * Any other custom claims that the adopter may have added\n */\n [claim: string]: JsonValue;\n}\n\ntype Options = {\n logger: LoggerService;\n /** Value of the issuer claim in issued tokens */\n issuer: string;\n /** Key store used for storing signing keys */\n keyStore: KeyStore;\n /** Expiration time of signing keys in seconds */\n keyDurationSeconds: number;\n /** JWS \"alg\" (Algorithm) Header Parameter value. Defaults to ES256.\n * Must match one of the algorithms defined for IdentityClient.\n * When setting a different algorithm, check if the `key` field\n * of the `signing_keys` table can fit the length of the generated keys.\n * If not, add a knex migration file in the migrations folder.\n * More info on supported algorithms: https://github.com/panva/jose */\n algorithm?: string;\n /**\n * A list of claims to omit from issued tokens and only store in the user info database\n */\n omitClaimsFromToken?: string[];\n userInfoDatabaseHandler: UserInfoDatabaseHandler;\n};\n\n/**\n * A token issuer that is able to issue tokens in a distributed system\n * backed by a single database. Tokens are issued using lazily generated\n * signing keys, where each running instance of the auth service uses its own\n * signing key.\n *\n * The public parts of the keys are all stored in the shared key storage,\n * and any of the instances of the auth service will return the full list\n * of public keys that are currently in storage.\n *\n * Signing keys are automatically rotated at the same interval as the token\n * duration. Expired keys are kept in storage until there are no valid tokens\n * in circulation that could have been signed by that key.\n */\nexport class TokenFactory implements TokenIssuer {\n private readonly issuer: string;\n private readonly logger: LoggerService;\n private readonly keyStore: KeyStore;\n private readonly keyDurationSeconds: number;\n private readonly algorithm: string;\n private readonly omitClaimsFromToken?: string[];\n private readonly userInfoDatabaseHandler: UserInfoDatabaseHandler;\n\n private keyExpiry?: Date;\n private privateKeyPromise?: Promise<JWK>;\n\n constructor(options: Options) {\n this.issuer = options.issuer;\n this.logger = options.logger;\n this.keyStore = options.keyStore;\n this.keyDurationSeconds = options.keyDurationSeconds;\n this.algorithm = options.algorithm ?? 'ES256';\n this.omitClaimsFromToken = options.omitClaimsFromToken;\n this.userInfoDatabaseHandler = options.userInfoDatabaseHandler;\n }\n\n async issueToken(params: TokenParams): Promise<BackstageSignInResult> {\n const key = await this.getKey();\n\n return issueUserToken({\n issuer: this.issuer,\n key,\n keyDurationSeconds: this.keyDurationSeconds,\n logger: this.logger,\n omitClaimsFromToken: this.omitClaimsFromToken,\n params,\n userInfoDatabaseHandler: this.userInfoDatabaseHandler,\n });\n }\n\n // This will be called by other services that want to verify ID tokens.\n // It is important that it returns a list of all public keys that could\n // have been used to sign tokens that have not yet expired.\n async listPublicKeys(): Promise<{ keys: AnyJWK[] }> {\n const { items: keys } = await this.keyStore.listKeys();\n\n const validKeys = [];\n const expiredKeys = [];\n\n for (const key of keys) {\n // Allow for a grace period of another full key duration before we remove the keys from the database\n const expireAt = DateTime.fromJSDate(key.createdAt).plus({\n seconds: 3 * this.keyDurationSeconds,\n });\n if (expireAt < DateTime.local()) {\n expiredKeys.push(key);\n } else {\n validKeys.push(key);\n }\n }\n\n // Lazily prune expired keys. This may cause duplicate removals if we have concurrent callers, but w/e\n if (expiredKeys.length > 0) {\n const kids = expiredKeys.map(({ key }) => key.kid);\n\n this.logger.info(`Removing expired signing keys, '${kids.join(\"', '\")}'`);\n\n // We don't await this, just let it run in the background\n this.keyStore.removeKeys(kids).catch(error => {\n this.logger.error(`Failed to remove expired keys, ${error}`);\n });\n }\n\n // NOTE: we're currently only storing public keys, but if we start storing private keys we'd have to convert here\n return { keys: validKeys.map(({ key }) => key) };\n }\n\n private async getKey(): Promise<JWK> {\n // Make sure that we only generate one key at a time\n if (this.privateKeyPromise) {\n if (\n this.keyExpiry &&\n DateTime.fromJSDate(this.keyExpiry) > DateTime.local()\n ) {\n return this.privateKeyPromise;\n }\n this.logger.info(`Signing key has expired, generating new key`);\n delete this.privateKeyPromise;\n }\n\n this.keyExpiry = DateTime.utc()\n .plus({\n seconds: this.keyDurationSeconds,\n })\n .toJSDate();\n const promise = (async () => {\n // This generates a new signing key to be used to sign tokens until the next key rotation\n const key = await generateKeyPair(this.algorithm);\n const publicKey = await exportJWK(key.publicKey);\n const privateKey = await exportJWK(key.privateKey);\n publicKey.kid = privateKey.kid = uuid();\n publicKey.alg = privateKey.alg = this.algorithm;\n\n // We're not allowed to use the key until it has been successfully stored\n // TODO: some token verification implementations aggressively cache the list of keys, and\n // don't attempt to fetch new ones even if they encounter an unknown kid. Therefore we\n // may want to keep using the existing key for some period of time until we switch to\n // the new one. This also needs to be implemented cross-service though, meaning new services\n // that boot up need to be able to grab an existing key to use for signing.\n this.logger.info(`Created new signing key ${publicKey.kid}`);\n await this.keyStore.addKey(publicKey as AnyJWK);\n\n // At this point we are allowed to start using the new key\n return privateKey;\n })();\n\n this.privateKeyPromise = promise;\n\n try {\n // If we fail to generate a new key, we need to clear the state so that\n // the next caller will try to generate another key.\n await promise;\n } catch (error) {\n this.logger.error(`Failed to generate new signing key, ${error}`);\n delete this.keyExpiry;\n delete this.privateKeyPromise;\n }\n\n return promise;\n }\n}\n"],"names":["issueUserToken","DateTime","generateKeyPair","exportJWK","uuid"],"mappings":"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"}
1
+ {"version":3,"file":"TokenFactory.cjs.js","sources":["../../src/identity/TokenFactory.ts"],"sourcesContent":["/*\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 { exportJWK, generateKeyPair, JWK } from 'jose';\nimport { DateTime } from 'luxon';\nimport { v4 as uuid } from 'uuid';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport {\n BackstageSignInResult,\n TokenParams,\n tokenTypes,\n} from '@backstage/plugin-auth-node';\nimport { AnyJWK, KeyStore, TokenIssuer } from './types';\nimport { JsonValue } from '@backstage/types';\nimport { issueUserToken } from './issueUserToken';\n\n/**\n * The payload contents of a valid Backstage JWT token\n */\nexport interface BackstageTokenPayload {\n /**\n * The issuer of the token, currently the discovery URL of the auth backend\n */\n iss: string;\n\n /**\n * The entity ref of the user\n */\n sub: string;\n\n /**\n * The entity refs that the user claims ownership through\n */\n ent: string[];\n\n /**\n * A hard coded audience string\n */\n aud: typeof tokenTypes.user.audClaim;\n\n /**\n * Standard expiry in epoch seconds\n */\n exp: number;\n\n /**\n * Standard issue time in epoch seconds\n */\n iat: number;\n\n /**\n * A separate user identity proof that the auth service can convert to a limited user token\n */\n uip: string;\n\n /**\n * Any other custom claims that the adopter may have added\n */\n [claim: string]: JsonValue;\n}\n\ntype Options = {\n logger: LoggerService;\n /** Value of the issuer claim in issued tokens */\n issuer: string;\n /** Key store used for storing signing keys */\n keyStore: KeyStore;\n /** Expiration time of signing keys in seconds */\n keyDurationSeconds: number;\n /** JWS \"alg\" (Algorithm) Header Parameter value. Defaults to ES256.\n * Must match one of the algorithms defined for IdentityClient.\n * When setting a different algorithm, check if the `key` field\n * of the `signing_keys` table can fit the length of the generated keys.\n * If not, add a knex migration file in the migrations folder.\n * More info on supported algorithms: https://github.com/panva/jose */\n algorithm?: string;\n /**\n * A list of claims to omit from issued tokens and only store in the user info database\n */\n omitClaimsFromToken?: string[];\n};\n\n/**\n * A token issuer that is able to issue tokens in a distributed system\n * backed by a single database. Tokens are issued using lazily generated\n * signing keys, where each running instance of the auth service uses its own\n * signing key.\n *\n * The public parts of the keys are all stored in the shared key storage,\n * and any of the instances of the auth service will return the full list\n * of public keys that are currently in storage.\n *\n * Signing keys are automatically rotated at the same interval as the token\n * duration. Expired keys are kept in storage until there are no valid tokens\n * in circulation that could have been signed by that key.\n */\nexport class TokenFactory implements TokenIssuer {\n private readonly issuer: string;\n private readonly logger: LoggerService;\n private readonly keyStore: KeyStore;\n private readonly keyDurationSeconds: number;\n private readonly algorithm: string;\n private readonly omitClaimsFromToken?: string[];\n\n private keyExpiry?: Date;\n private privateKeyPromise?: Promise<JWK>;\n\n constructor(options: Options) {\n this.issuer = options.issuer;\n this.logger = options.logger;\n this.keyStore = options.keyStore;\n this.keyDurationSeconds = options.keyDurationSeconds;\n this.algorithm = options.algorithm ?? 'ES256';\n this.omitClaimsFromToken = options.omitClaimsFromToken;\n }\n\n async issueToken(\n params: TokenParams & { claims: { ent: string[] } },\n ): Promise<BackstageSignInResult> {\n const key = await this.getKey();\n\n return issueUserToken({\n issuer: this.issuer,\n key,\n keyDurationSeconds: this.keyDurationSeconds,\n logger: this.logger,\n omitClaimsFromToken: this.omitClaimsFromToken,\n params,\n });\n }\n\n // This will be called by other services that want to verify ID tokens.\n // It is important that it returns a list of all public keys that could\n // have been used to sign tokens that have not yet expired.\n async listPublicKeys(): Promise<{ keys: AnyJWK[] }> {\n const { items: keys } = await this.keyStore.listKeys();\n\n const validKeys = [];\n const expiredKeys = [];\n\n for (const key of keys) {\n // Allow for a grace period of another full key duration before we remove the keys from the database\n const expireAt = DateTime.fromJSDate(key.createdAt).plus({\n seconds: 3 * this.keyDurationSeconds,\n });\n if (expireAt < DateTime.local()) {\n expiredKeys.push(key);\n } else {\n validKeys.push(key);\n }\n }\n\n // Lazily prune expired keys. This may cause duplicate removals if we have concurrent callers, but w/e\n if (expiredKeys.length > 0) {\n const kids = expiredKeys.map(({ key }) => key.kid);\n\n this.logger.info(`Removing expired signing keys, '${kids.join(\"', '\")}'`);\n\n // We don't await this, just let it run in the background\n this.keyStore.removeKeys(kids).catch(error => {\n this.logger.error(`Failed to remove expired keys, ${error}`);\n });\n }\n\n // NOTE: we're currently only storing public keys, but if we start storing private keys we'd have to convert here\n return { keys: validKeys.map(({ key }) => key) };\n }\n\n private async getKey(): Promise<JWK> {\n // Make sure that we only generate one key at a time\n if (this.privateKeyPromise) {\n if (\n this.keyExpiry &&\n DateTime.fromJSDate(this.keyExpiry) > DateTime.local()\n ) {\n return this.privateKeyPromise;\n }\n this.logger.info(`Signing key has expired, generating new key`);\n delete this.privateKeyPromise;\n }\n\n this.keyExpiry = DateTime.utc()\n .plus({\n seconds: this.keyDurationSeconds,\n })\n .toJSDate();\n const promise = (async () => {\n // This generates a new signing key to be used to sign tokens until the next key rotation\n const key = await generateKeyPair(this.algorithm);\n const publicKey = await exportJWK(key.publicKey);\n const privateKey = await exportJWK(key.privateKey);\n publicKey.kid = privateKey.kid = uuid();\n publicKey.alg = privateKey.alg = this.algorithm;\n\n // We're not allowed to use the key until it has been successfully stored\n // TODO: some token verification implementations aggressively cache the list of keys, and\n // don't attempt to fetch new ones even if they encounter an unknown kid. Therefore we\n // may want to keep using the existing key for some period of time until we switch to\n // the new one. This also needs to be implemented cross-service though, meaning new services\n // that boot up need to be able to grab an existing key to use for signing.\n this.logger.info(`Created new signing key ${publicKey.kid}`);\n await this.keyStore.addKey(publicKey as AnyJWK);\n\n // At this point we are allowed to start using the new key\n return privateKey;\n })();\n\n this.privateKeyPromise = promise;\n\n try {\n // If we fail to generate a new key, we need to clear the state so that\n // the next caller will try to generate another key.\n await promise;\n } catch (error) {\n this.logger.error(`Failed to generate new signing key, ${error}`);\n delete this.keyExpiry;\n delete this.privateKeyPromise;\n }\n\n return promise;\n }\n}\n"],"names":["issueUserToken","DateTime","generateKeyPair","exportJWK","uuid"],"mappings":"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"}
@@ -14,10 +14,9 @@ async function issueUserToken({
14
14
  keyDurationSeconds,
15
15
  logger,
16
16
  omitClaimsFromToken,
17
- params,
18
- userInfoDatabaseHandler
17
+ params
19
18
  }) {
20
- const { sub, ent = [sub], ...additionalClaims } = params.claims;
19
+ const { sub, ent, ...additionalClaims } = params.claims;
21
20
  const aud = pluginAuthNode.tokenTypes.user.audClaim;
22
21
  const iat = Math.floor(Date.now() / MS_IN_S);
23
22
  const exp = iat + keyDurationSeconds;
@@ -65,9 +64,6 @@ async function issueUserToken({
65
64
  )}'`
66
65
  );
67
66
  }
68
- await userInfoDatabaseHandler.addUserInfo({
69
- claims: lodash.omit(claims, ["aud", "iat", "iss", "uip"])
70
- });
71
67
  return {
72
68
  token,
73
69
  identity: {
@@ -1 +1 @@
1
- {"version":3,"file":"issueUserToken.cjs.js","sources":["../../src/identity/issueUserToken.ts"],"sourcesContent":["/*\n * Copyright 2025 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 { parseEntityRef } from '@backstage/catalog-model';\nimport { AuthenticationError } from '@backstage/errors';\nimport {\n BackstageSignInResult,\n TokenParams,\n tokenTypes,\n} from '@backstage/plugin-auth-node';\nimport { omit } from 'lodash';\nimport { UserInfoDatabaseHandler } from './UserInfoDatabaseHandler';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { GeneralSign, importJWK, JWK, KeyLike, SignJWT } from 'jose';\nimport { BackstageTokenPayload } from './TokenFactory';\n\nconst MS_IN_S = 1000;\nconst MAX_TOKEN_LENGTH = 32768; // At 64 bytes per entity ref this still leaves room for about 500 entities\n\nexport async function issueUserToken({\n issuer,\n key,\n keyDurationSeconds,\n logger,\n omitClaimsFromToken,\n params,\n userInfoDatabaseHandler,\n}: {\n issuer: string;\n key: JWK;\n keyDurationSeconds: number;\n logger: LoggerService;\n omitClaimsFromToken?: string[];\n params: TokenParams;\n userInfoDatabaseHandler: UserInfoDatabaseHandler;\n}): Promise<BackstageSignInResult> {\n const { sub, ent = [sub], ...additionalClaims } = params.claims;\n const aud = tokenTypes.user.audClaim;\n const iat = Math.floor(Date.now() / MS_IN_S);\n const exp = iat + keyDurationSeconds;\n\n try {\n // The subject must be a valid entity ref\n parseEntityRef(sub);\n } catch (error) {\n throw new Error(\n '\"sub\" claim provided by the auth resolver is not a valid EntityRef.',\n );\n }\n\n if (!key.alg) {\n throw new AuthenticationError('No algorithm was provided in the key');\n }\n\n logger.info(`Issuing token for ${sub}, with entities ${ent}`);\n\n const signingKey = await importJWK(key);\n\n const uip = await createUserIdentityClaim({\n header: {\n typ: tokenTypes.limitedUser.typParam,\n alg: key.alg,\n kid: key.kid,\n },\n payload: { sub, iat, exp },\n key: signingKey,\n });\n\n const claims: BackstageTokenPayload = {\n ...additionalClaims,\n iss: issuer,\n sub,\n ent,\n aud,\n iat,\n exp,\n uip,\n };\n\n const tokenClaims = omitClaimsFromToken\n ? omit(claims, omitClaimsFromToken)\n : claims;\n const token = await new SignJWT(tokenClaims)\n .setProtectedHeader({\n typ: tokenTypes.user.typParam,\n alg: key.alg,\n kid: key.kid,\n })\n .sign(signingKey);\n\n if (token.length > MAX_TOKEN_LENGTH) {\n throw new Error(\n `Failed to issue a new user token. The resulting token is excessively large, with either too many ownership claims or too large custom claims. You likely have a bug either in the sign-in resolver or catalog data. The following claims were requested: '${JSON.stringify(\n tokenClaims,\n )}'`,\n );\n }\n\n // Store the user info in the database upon successful token\n // issuance so that it can be retrieved later by limited user tokens\n await userInfoDatabaseHandler.addUserInfo({\n claims: omit(claims, ['aud', 'iat', 'iss', 'uip']),\n });\n\n return {\n token,\n identity: {\n type: 'user',\n userEntityRef: sub,\n ownershipEntityRefs: ent,\n },\n };\n}\n\n/**\n * The payload contents of a valid Backstage user identity claim token\n *\n * @internal\n */\ninterface BackstageUserIdentityProofPayload {\n /**\n * The entity ref of the user\n */\n sub: string;\n\n /**\n * Standard expiry in epoch seconds\n */\n exp: number;\n\n /**\n * Standard issue time in epoch seconds\n */\n iat: number;\n}\n\n/**\n * Creates a string claim that can be used as part of reconstructing a limited\n * user token. The output of this function is only the signature part of a JWS.\n */\nasync function createUserIdentityClaim(options: {\n header: {\n typ: string;\n alg: string;\n kid?: string;\n };\n payload: BackstageUserIdentityProofPayload;\n key: KeyLike | Uint8Array;\n}): Promise<string> {\n // NOTE: We reconstruct the header and payload structures carefully to\n // perfectly guarantee ordering. The reason for this is that we store only\n // the signature part of these to reduce duplication within the Backstage\n // token. Anyone who wants to make an actual JWT based on all this must be\n // able to do the EXACT reconstruction of the header and payload parts, to\n // then append the signature.\n\n const header = {\n typ: options.header.typ,\n alg: options.header.alg,\n ...(options.header.kid ? { kid: options.header.kid } : {}),\n };\n\n const payload = {\n sub: options.payload.sub,\n iat: options.payload.iat,\n exp: options.payload.exp,\n };\n\n const jws = await new GeneralSign(\n new TextEncoder().encode(JSON.stringify(payload)),\n )\n .addSignature(options.key)\n .setProtectedHeader(header)\n .done()\n .sign();\n\n return jws.signatures[0].signature;\n}\n"],"names":["tokenTypes","parseEntityRef","AuthenticationError","importJWK","omit","SignJWT","GeneralSign"],"mappings":"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"}
1
+ {"version":3,"file":"issueUserToken.cjs.js","sources":["../../src/identity/issueUserToken.ts"],"sourcesContent":["/*\n * Copyright 2025 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 { parseEntityRef } from '@backstage/catalog-model';\nimport { AuthenticationError } from '@backstage/errors';\nimport {\n BackstageSignInResult,\n TokenParams,\n tokenTypes,\n} from '@backstage/plugin-auth-node';\nimport { omit } from 'lodash';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { GeneralSign, importJWK, JWK, KeyLike, SignJWT } from 'jose';\nimport { BackstageTokenPayload } from './TokenFactory';\n\nconst MS_IN_S = 1000;\nconst MAX_TOKEN_LENGTH = 32768; // At 64 bytes per entity ref this still leaves room for about 500 entities\n\nexport async function issueUserToken({\n issuer,\n key,\n keyDurationSeconds,\n logger,\n omitClaimsFromToken,\n params,\n}: {\n issuer: string;\n key: JWK;\n keyDurationSeconds: number;\n logger: LoggerService;\n omitClaimsFromToken?: string[];\n params: TokenParams & { claims: { ent: string[] } };\n}): Promise<BackstageSignInResult> {\n const { sub, ent, ...additionalClaims } = params.claims;\n const aud = tokenTypes.user.audClaim;\n const iat = Math.floor(Date.now() / MS_IN_S);\n const exp = iat + keyDurationSeconds;\n\n try {\n // The subject must be a valid entity ref\n parseEntityRef(sub);\n } catch (error) {\n throw new Error(\n '\"sub\" claim provided by the auth resolver is not a valid EntityRef.',\n );\n }\n\n if (!key.alg) {\n throw new AuthenticationError('No algorithm was provided in the key');\n }\n\n logger.info(`Issuing token for ${sub}, with entities ${ent}`);\n\n const signingKey = await importJWK(key);\n\n const uip = await createUserIdentityClaim({\n header: {\n typ: tokenTypes.limitedUser.typParam,\n alg: key.alg,\n kid: key.kid,\n },\n payload: { sub, iat, exp },\n key: signingKey,\n });\n\n const claims: BackstageTokenPayload = {\n ...additionalClaims,\n iss: issuer,\n sub,\n ent,\n aud,\n iat,\n exp,\n uip,\n };\n\n const tokenClaims = omitClaimsFromToken\n ? omit(claims, omitClaimsFromToken)\n : claims;\n const token = await new SignJWT(tokenClaims)\n .setProtectedHeader({\n typ: tokenTypes.user.typParam,\n alg: key.alg,\n kid: key.kid,\n })\n .sign(signingKey);\n\n if (token.length > MAX_TOKEN_LENGTH) {\n throw new Error(\n `Failed to issue a new user token. The resulting token is excessively large, with either too many ownership claims or too large custom claims. You likely have a bug either in the sign-in resolver or catalog data. The following claims were requested: '${JSON.stringify(\n tokenClaims,\n )}'`,\n );\n }\n\n return {\n token,\n identity: {\n type: 'user',\n userEntityRef: sub,\n ownershipEntityRefs: ent,\n },\n };\n}\n\n/**\n * The payload contents of a valid Backstage user identity claim token\n *\n * @internal\n */\ninterface BackstageUserIdentityProofPayload {\n /**\n * The entity ref of the user\n */\n sub: string;\n\n /**\n * Standard expiry in epoch seconds\n */\n exp: number;\n\n /**\n * Standard issue time in epoch seconds\n */\n iat: number;\n}\n\n/**\n * Creates a string claim that can be used as part of reconstructing a limited\n * user token. The output of this function is only the signature part of a JWS.\n */\nasync function createUserIdentityClaim(options: {\n header: {\n typ: string;\n alg: string;\n kid?: string;\n };\n payload: BackstageUserIdentityProofPayload;\n key: KeyLike | Uint8Array;\n}): Promise<string> {\n // NOTE: We reconstruct the header and payload structures carefully to\n // perfectly guarantee ordering. The reason for this is that we store only\n // the signature part of these to reduce duplication within the Backstage\n // token. Anyone who wants to make an actual JWT based on all this must be\n // able to do the EXACT reconstruction of the header and payload parts, to\n // then append the signature.\n\n const header = {\n typ: options.header.typ,\n alg: options.header.alg,\n ...(options.header.kid ? { kid: options.header.kid } : {}),\n };\n\n const payload = {\n sub: options.payload.sub,\n iat: options.payload.iat,\n exp: options.payload.exp,\n };\n\n const jws = await new GeneralSign(\n new TextEncoder().encode(JSON.stringify(payload)),\n )\n .addSignature(options.key)\n .setProtectedHeader(header)\n .done()\n .sign();\n\n return jws.signatures[0].signature;\n}\n"],"names":["tokenTypes","parseEntityRef","AuthenticationError","importJWK","omit","SignJWT","GeneralSign"],"mappings":";;;;;;;;AA4BA,MAAM,OAAU,GAAA,GAAA;AAChB,MAAM,gBAAmB,GAAA,KAAA;AAEzB,eAAsB,cAAe,CAAA;AAAA,EACnC,MAAA;AAAA,EACA,GAAA;AAAA,EACA,kBAAA;AAAA,EACA,MAAA;AAAA,EACA,mBAAA;AAAA,EACA;AACF,CAOmC,EAAA;AACjC,EAAA,MAAM,EAAE,GAAK,EAAA,GAAA,EAAK,GAAG,gBAAA,KAAqB,MAAO,CAAA,MAAA;AACjD,EAAM,MAAA,GAAA,GAAMA,0BAAW,IAAK,CAAA,QAAA;AAC5B,EAAA,MAAM,MAAM,IAAK,CAAA,KAAA,CAAM,IAAK,CAAA,GAAA,KAAQ,OAAO,CAAA;AAC3C,EAAA,MAAM,MAAM,GAAM,GAAA,kBAAA;AAElB,EAAI,IAAA;AAEF,IAAAC,2BAAA,CAAe,GAAG,CAAA;AAAA,WACX,KAAO,EAAA;AACd,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA;AAGF,EAAI,IAAA,CAAC,IAAI,GAAK,EAAA;AACZ,IAAM,MAAA,IAAIC,2BAAoB,sCAAsC,CAAA;AAAA;AAGtE,EAAA,MAAA,CAAO,IAAK,CAAA,CAAA,kBAAA,EAAqB,GAAG,CAAA,gBAAA,EAAmB,GAAG,CAAE,CAAA,CAAA;AAE5D,EAAM,MAAA,UAAA,GAAa,MAAMC,cAAA,CAAU,GAAG,CAAA;AAEtC,EAAM,MAAA,GAAA,GAAM,MAAM,uBAAwB,CAAA;AAAA,IACxC,MAAQ,EAAA;AAAA,MACN,GAAA,EAAKH,0BAAW,WAAY,CAAA,QAAA;AAAA,MAC5B,KAAK,GAAI,CAAA,GAAA;AAAA,MACT,KAAK,GAAI,CAAA;AAAA,KACX;AAAA,IACA,OAAS,EAAA,EAAE,GAAK,EAAA,GAAA,EAAK,GAAI,EAAA;AAAA,IACzB,GAAK,EAAA;AAAA,GACN,CAAA;AAED,EAAA,MAAM,MAAgC,GAAA;AAAA,IACpC,GAAG,gBAAA;AAAA,IACH,GAAK,EAAA,MAAA;AAAA,IACL,GAAA;AAAA,IACA,GAAA;AAAA,IACA,GAAA;AAAA,IACA,GAAA;AAAA,IACA,GAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,MAAM,WAAc,GAAA,mBAAA,GAChBI,WAAK,CAAA,MAAA,EAAQ,mBAAmB,CAChC,GAAA,MAAA;AACJ,EAAA,MAAM,QAAQ,MAAM,IAAIC,YAAQ,CAAA,WAAW,EACxC,kBAAmB,CAAA;AAAA,IAClB,GAAA,EAAKL,0BAAW,IAAK,CAAA,QAAA;AAAA,IACrB,KAAK,GAAI,CAAA,GAAA;AAAA,IACT,KAAK,GAAI,CAAA;AAAA,GACV,CACA,CAAA,IAAA,CAAK,UAAU,CAAA;AAElB,EAAI,IAAA,KAAA,CAAM,SAAS,gBAAkB,EAAA;AACnC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,6PAA6P,IAAK,CAAA,SAAA;AAAA,QAChQ;AAAA,OACD,CAAA,CAAA;AAAA,KACH;AAAA;AAGF,EAAO,OAAA;AAAA,IACL,KAAA;AAAA,IACA,QAAU,EAAA;AAAA,MACR,IAAM,EAAA,MAAA;AAAA,MACN,aAAe,EAAA,GAAA;AAAA,MACf,mBAAqB,EAAA;AAAA;AACvB,GACF;AACF;AA4BA,eAAe,wBAAwB,OAQnB,EAAA;AAQlB,EAAA,MAAM,MAAS,GAAA;AAAA,IACb,GAAA,EAAK,QAAQ,MAAO,CAAA,GAAA;AAAA,IACpB,GAAA,EAAK,QAAQ,MAAO,CAAA,GAAA;AAAA,IACpB,GAAI,OAAQ,CAAA,MAAA,CAAO,GAAM,GAAA,EAAE,KAAK,OAAQ,CAAA,MAAA,CAAO,GAAI,EAAA,GAAI;AAAC,GAC1D;AAEA,EAAA,MAAM,OAAU,GAAA;AAAA,IACd,GAAA,EAAK,QAAQ,OAAQ,CAAA,GAAA;AAAA,IACrB,GAAA,EAAK,QAAQ,OAAQ,CAAA,GAAA;AAAA,IACrB,GAAA,EAAK,QAAQ,OAAQ,CAAA;AAAA,GACvB;AAEA,EAAM,MAAA,GAAA,GAAM,MAAM,IAAIM,gBAAA;AAAA,IACpB,IAAI,WAAY,EAAA,CAAE,OAAO,IAAK,CAAA,SAAA,CAAU,OAAO,CAAC;AAAA,GAClD,CACG,YAAa,CAAA,OAAA,CAAQ,GAAG,CAAA,CACxB,mBAAmB,MAAM,CAAA,CACzB,IAAK,EAAA,CACL,IAAK,EAAA;AAER,EAAO,OAAA,GAAA,CAAI,UAAW,CAAA,CAAC,CAAE,CAAA,SAAA;AAC3B;;;;"}
@@ -11,12 +11,13 @@ function getDefaultOwnershipEntityRefs(entity) {
11
11
  return Array.from(/* @__PURE__ */ new Set([catalogModel.stringifyEntityRef(entity), ...membershipRefs]));
12
12
  }
13
13
  class CatalogAuthResolverContext {
14
- constructor(logger, tokenIssuer, catalogIdentityClient, catalog, auth, ownershipResolver) {
14
+ constructor(logger, tokenIssuer, catalogIdentityClient, catalog, auth, userInfo, ownershipResolver) {
15
15
  this.logger = logger;
16
16
  this.tokenIssuer = tokenIssuer;
17
17
  this.catalogIdentityClient = catalogIdentityClient;
18
18
  this.catalog = catalog;
19
19
  this.auth = auth;
20
+ this.userInfo = userInfo;
20
21
  this.ownershipResolver = ownershipResolver;
21
22
  }
22
23
  static create(options) {
@@ -30,11 +31,24 @@ class CatalogAuthResolverContext {
30
31
  catalogIdentityClient,
31
32
  options.catalog,
32
33
  options.auth,
34
+ options.userInfo,
33
35
  options.ownershipResolver
34
36
  );
35
37
  }
36
38
  async issueToken(params) {
37
- return await this.tokenIssuer.issueToken(params);
39
+ const { sub, ent = [sub], ...additionalClaims } = params.claims;
40
+ const claims = {
41
+ sub,
42
+ ent,
43
+ ...additionalClaims
44
+ };
45
+ const issuedToken = await this.tokenIssuer.issueToken({
46
+ claims
47
+ });
48
+ await this.userInfo.addUserInfo({
49
+ claims
50
+ });
51
+ return issuedToken;
38
52
  }
39
53
  async findCatalogUser(query) {
40
54
  let result = void 0;
@@ -95,7 +109,7 @@ class CatalogAuthResolverContext {
95
109
  const { ownershipEntityRefs } = await this.resolveOwnershipEntityRefs(
96
110
  entity
97
111
  );
98
- return await this.tokenIssuer.issueToken({
112
+ return await this.issueToken({
99
113
  claims: {
100
114
  sub: catalogModel.stringifyEntityRef(entity),
101
115
  ent: ownershipEntityRefs
@@ -111,7 +125,7 @@ class CatalogAuthResolverContext {
111
125
  defaultNamespace: catalogModel.DEFAULT_NAMESPACE
112
126
  })
113
127
  );
114
- return await this.tokenIssuer.issueToken({
128
+ return await this.issueToken({
115
129
  claims: {
116
130
  sub: userEntityRef,
117
131
  ent: [userEntityRef]
@@ -1 +1 @@
1
- {"version":3,"file":"CatalogAuthResolverContext.cjs.js","sources":["../../../src/lib/resolvers/CatalogAuthResolverContext.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\nimport {\n DEFAULT_NAMESPACE,\n Entity,\n parseEntityRef,\n RELATION_MEMBER_OF,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { ConflictError, InputError, NotFoundError } from '@backstage/errors';\nimport { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { TokenIssuer } from '../../identity/types';\nimport {\n AuthOwnershipResolver,\n AuthResolverCatalogUserQuery,\n AuthResolverContext,\n TokenParams,\n} from '@backstage/plugin-auth-node';\nimport { CatalogIdentityClient } from '../catalog/CatalogIdentityClient';\n\nfunction getDefaultOwnershipEntityRefs(entity: Entity) {\n const membershipRefs =\n entity.relations\n ?.filter(\n r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'),\n )\n .map(r => r.targetRef) ?? [];\n\n return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs]));\n}\n\nexport class CatalogAuthResolverContext implements AuthResolverContext {\n static create(options: {\n logger: LoggerService;\n catalog: CatalogService;\n tokenIssuer: TokenIssuer;\n auth: AuthService;\n ownershipResolver?: AuthOwnershipResolver;\n }): CatalogAuthResolverContext {\n const catalogIdentityClient = new CatalogIdentityClient({\n catalog: options.catalog,\n auth: options.auth,\n });\n\n return new CatalogAuthResolverContext(\n options.logger,\n options.tokenIssuer,\n catalogIdentityClient,\n options.catalog,\n options.auth,\n options.ownershipResolver,\n );\n }\n\n private constructor(\n public readonly logger: LoggerService,\n public readonly tokenIssuer: TokenIssuer,\n public readonly catalogIdentityClient: CatalogIdentityClient,\n private readonly catalog: CatalogService,\n private readonly auth: AuthService,\n private readonly ownershipResolver?: AuthOwnershipResolver,\n ) {}\n\n async issueToken(params: TokenParams) {\n return await this.tokenIssuer.issueToken(params);\n }\n\n async findCatalogUser(query: AuthResolverCatalogUserQuery) {\n let result: Entity[] | Entity | undefined = undefined;\n\n if ('entityRef' in query) {\n const entityRef = parseEntityRef(query.entityRef, {\n defaultKind: 'User',\n defaultNamespace: DEFAULT_NAMESPACE,\n });\n result = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n } else if ('annotations' in query) {\n const filter: Record<string, string> = {\n kind: 'user',\n };\n for (const [key, value] of Object.entries(query.annotations)) {\n filter[`metadata.annotations.${key}`] = value;\n }\n const res = await this.catalog.getEntities(\n { filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n result = res.items;\n } else if ('filter' in query) {\n const filter = [query.filter].flat().map(value => {\n if (\n !Object.keys(value).some(\n key => key.toLocaleLowerCase('en-US') === 'kind',\n )\n ) {\n return {\n ...value,\n kind: 'user',\n };\n }\n return value;\n });\n const res = await this.catalog.getEntities(\n { filter: filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n result = res.items;\n } else {\n throw new InputError('Invalid user lookup query');\n }\n\n if (Array.isArray(result)) {\n if (result.length > 1) {\n throw new ConflictError('User lookup resulted in multiple matches');\n }\n result = result[0];\n }\n if (!result) {\n throw new NotFoundError('User not found');\n }\n\n return { entity: result };\n }\n\n async signInWithCatalogUser(\n query: AuthResolverCatalogUserQuery,\n options?: {\n dangerousEntityRefFallback?: {\n entityRef:\n | string\n | {\n kind?: string;\n namespace?: string;\n name: string;\n };\n };\n },\n ) {\n try {\n const { entity } = await this.findCatalogUser(query);\n\n const { ownershipEntityRefs } = await this.resolveOwnershipEntityRefs(\n entity,\n );\n\n return await this.tokenIssuer.issueToken({\n claims: {\n sub: stringifyEntityRef(entity),\n ent: ownershipEntityRefs,\n },\n });\n } catch (error) {\n if (\n error?.name !== 'NotFoundError' ||\n !options?.dangerousEntityRefFallback\n ) {\n throw error;\n }\n const userEntityRef = stringifyEntityRef(\n parseEntityRef(options.dangerousEntityRefFallback.entityRef, {\n defaultKind: 'User',\n defaultNamespace: DEFAULT_NAMESPACE,\n }),\n );\n\n return await this.tokenIssuer.issueToken({\n claims: {\n sub: userEntityRef,\n ent: [userEntityRef],\n },\n });\n }\n }\n\n async resolveOwnershipEntityRefs(\n entity: Entity,\n ): Promise<{ ownershipEntityRefs: string[] }> {\n if (this.ownershipResolver) {\n return this.ownershipResolver.resolveOwnershipEntityRefs(entity);\n }\n return { ownershipEntityRefs: getDefaultOwnershipEntityRefs(entity) };\n }\n}\n"],"names":["RELATION_MEMBER_OF","stringifyEntityRef","CatalogIdentityClient","parseEntityRef","DEFAULT_NAMESPACE","InputError","ConflictError","NotFoundError"],"mappings":"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"}
1
+ {"version":3,"file":"CatalogAuthResolverContext.cjs.js","sources":["../../../src/lib/resolvers/CatalogAuthResolverContext.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\nimport {\n DEFAULT_NAMESPACE,\n Entity,\n parseEntityRef,\n RELATION_MEMBER_OF,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { ConflictError, InputError, NotFoundError } from '@backstage/errors';\nimport { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { TokenIssuer } from '../../identity/types';\nimport {\n AuthOwnershipResolver,\n AuthResolverCatalogUserQuery,\n AuthResolverContext,\n TokenParams,\n} from '@backstage/plugin-auth-node';\nimport { CatalogIdentityClient } from '../catalog/CatalogIdentityClient';\nimport { UserInfoDatabase } from '../../database/UserInfoDatabase';\n\nfunction getDefaultOwnershipEntityRefs(entity: Entity) {\n const membershipRefs =\n entity.relations\n ?.filter(\n r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'),\n )\n .map(r => r.targetRef) ?? [];\n\n return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs]));\n}\n\nexport class CatalogAuthResolverContext implements AuthResolverContext {\n static create(options: {\n logger: LoggerService;\n catalog: CatalogService;\n tokenIssuer: TokenIssuer;\n auth: AuthService;\n ownershipResolver?: AuthOwnershipResolver;\n userInfo: UserInfoDatabase;\n }): CatalogAuthResolverContext {\n const catalogIdentityClient = new CatalogIdentityClient({\n catalog: options.catalog,\n auth: options.auth,\n });\n\n return new CatalogAuthResolverContext(\n options.logger,\n options.tokenIssuer,\n catalogIdentityClient,\n options.catalog,\n options.auth,\n options.userInfo,\n options.ownershipResolver,\n );\n }\n\n private constructor(\n public readonly logger: LoggerService,\n public readonly tokenIssuer: TokenIssuer,\n public readonly catalogIdentityClient: CatalogIdentityClient,\n private readonly catalog: CatalogService,\n private readonly auth: AuthService,\n private readonly userInfo: UserInfoDatabase,\n private readonly ownershipResolver?: AuthOwnershipResolver,\n ) {}\n\n async issueToken(params: TokenParams) {\n const { sub, ent = [sub], ...additionalClaims } = params.claims;\n const claims = {\n sub,\n ent,\n ...additionalClaims,\n };\n\n const issuedToken = await this.tokenIssuer.issueToken({\n claims,\n });\n\n // Store the user info in the database upon successful token\n // issuance so that it can be retrieved later by limited user tokens\n await this.userInfo.addUserInfo({\n claims,\n });\n\n return issuedToken;\n }\n\n async findCatalogUser(query: AuthResolverCatalogUserQuery) {\n let result: Entity[] | Entity | undefined = undefined;\n\n if ('entityRef' in query) {\n const entityRef = parseEntityRef(query.entityRef, {\n defaultKind: 'User',\n defaultNamespace: DEFAULT_NAMESPACE,\n });\n result = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n } else if ('annotations' in query) {\n const filter: Record<string, string> = {\n kind: 'user',\n };\n for (const [key, value] of Object.entries(query.annotations)) {\n filter[`metadata.annotations.${key}`] = value;\n }\n const res = await this.catalog.getEntities(\n { filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n result = res.items;\n } else if ('filter' in query) {\n const filter = [query.filter].flat().map(value => {\n if (\n !Object.keys(value).some(\n key => key.toLocaleLowerCase('en-US') === 'kind',\n )\n ) {\n return {\n ...value,\n kind: 'user',\n };\n }\n return value;\n });\n const res = await this.catalog.getEntities(\n { filter: filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n result = res.items;\n } else {\n throw new InputError('Invalid user lookup query');\n }\n\n if (Array.isArray(result)) {\n if (result.length > 1) {\n throw new ConflictError('User lookup resulted in multiple matches');\n }\n result = result[0];\n }\n if (!result) {\n throw new NotFoundError('User not found');\n }\n\n return { entity: result };\n }\n\n async signInWithCatalogUser(\n query: AuthResolverCatalogUserQuery,\n options?: {\n dangerousEntityRefFallback?: {\n entityRef:\n | string\n | {\n kind?: string;\n namespace?: string;\n name: string;\n };\n };\n },\n ) {\n try {\n const { entity } = await this.findCatalogUser(query);\n\n const { ownershipEntityRefs } = await this.resolveOwnershipEntityRefs(\n entity,\n );\n\n return await this.issueToken({\n claims: {\n sub: stringifyEntityRef(entity),\n ent: ownershipEntityRefs,\n },\n });\n } catch (error) {\n if (\n error?.name !== 'NotFoundError' ||\n !options?.dangerousEntityRefFallback\n ) {\n throw error;\n }\n const userEntityRef = stringifyEntityRef(\n parseEntityRef(options.dangerousEntityRefFallback.entityRef, {\n defaultKind: 'User',\n defaultNamespace: DEFAULT_NAMESPACE,\n }),\n );\n\n return await this.issueToken({\n claims: {\n sub: userEntityRef,\n ent: [userEntityRef],\n },\n });\n }\n }\n\n async resolveOwnershipEntityRefs(\n entity: Entity,\n ): Promise<{ ownershipEntityRefs: string[] }> {\n if (this.ownershipResolver) {\n return this.ownershipResolver.resolveOwnershipEntityRefs(entity);\n }\n return { ownershipEntityRefs: getDefaultOwnershipEntityRefs(entity) };\n }\n}\n"],"names":["RELATION_MEMBER_OF","stringifyEntityRef","CatalogIdentityClient","parseEntityRef","DEFAULT_NAMESPACE","InputError","ConflictError","NotFoundError"],"mappings":"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"}
@@ -19,7 +19,8 @@ function bindProviderRouters(targetRouter, options) {
19
19
  auth,
20
20
  tokenIssuer,
21
21
  catalog,
22
- ownershipResolver
22
+ ownershipResolver,
23
+ userInfo
23
24
  } = options;
24
25
  const providersConfig = config.getOptionalConfig("auth.providers");
25
26
  const isOriginAllowed = createOriginFilter(config);
@@ -44,7 +45,8 @@ function bindProviderRouters(targetRouter, options) {
44
45
  catalog,
45
46
  tokenIssuer,
46
47
  auth,
47
- ownershipResolver
48
+ ownershipResolver,
49
+ userInfo
48
50
  })
49
51
  });
50
52
  const r = Router__default.default();
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../../src/providers/router.ts"],"sourcesContent":["/*\n * Copyright 2024 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 { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { assertError, NotFoundError } from '@backstage/errors';\nimport {\n AuthOwnershipResolver,\n AuthProviderFactory,\n} from '@backstage/plugin-auth-node';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { Minimatch } from 'minimatch';\nimport { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext';\nimport { TokenIssuer } from '../identity/types';\n\nexport type ProviderFactories = { [s: string]: AuthProviderFactory };\n\nexport function bindProviderRouters(\n targetRouter: express.Router,\n options: {\n providers: ProviderFactories;\n appUrl: string;\n baseUrl: string;\n config: Config;\n logger: LoggerService;\n auth: AuthService;\n tokenIssuer: TokenIssuer;\n ownershipResolver?: AuthOwnershipResolver;\n catalog: CatalogService;\n },\n) {\n const {\n providers,\n appUrl,\n baseUrl,\n config,\n logger,\n auth,\n tokenIssuer,\n catalog,\n ownershipResolver,\n } = options;\n\n const providersConfig = config.getOptionalConfig('auth.providers');\n\n const isOriginAllowed = createOriginFilter(config);\n\n for (const [providerId, providerFactory] of Object.entries(providers)) {\n if (providersConfig?.has(providerId)) {\n logger.info(`Configuring auth provider: ${providerId}`);\n try {\n const provider = providerFactory({\n providerId,\n appUrl,\n baseUrl,\n isOriginAllowed,\n globalConfig: {\n baseUrl,\n appUrl,\n isOriginAllowed,\n },\n config: providersConfig.getConfig(providerId),\n logger,\n resolverContext: CatalogAuthResolverContext.create({\n logger,\n catalog,\n tokenIssuer,\n auth,\n ownershipResolver,\n }),\n });\n\n const r = Router();\n\n r.get('/start', provider.start.bind(provider));\n r.get('/handler/frame', provider.frameHandler.bind(provider));\n r.post('/handler/frame', provider.frameHandler.bind(provider));\n if (provider.logout) {\n r.post('/logout', provider.logout.bind(provider));\n }\n if (provider.refresh) {\n r.get('/refresh', provider.refresh.bind(provider));\n r.post('/refresh', provider.refresh.bind(provider));\n }\n\n targetRouter.use(`/${providerId}`, r);\n } catch (e) {\n assertError(e);\n if (process.env.NODE_ENV !== 'development') {\n throw new Error(\n `Failed to initialize ${providerId} auth provider, ${e.message}`,\n );\n }\n\n logger.warn(`Skipping ${providerId} auth provider, ${e.message}`);\n\n targetRouter.use(`/${providerId}`, () => {\n // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found.\n throw new NotFoundError(\n `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` +\n `auth.providers.${providerId} are missing or the environment variables used are not defined. ` +\n `Check the auth backend plugin logs when the backend starts to see more details.`,\n );\n });\n }\n } else {\n targetRouter.use(`/${providerId}`, () => {\n throw new NotFoundError(\n `No auth provider registered for '${providerId}'`,\n );\n });\n }\n }\n}\n\nexport function createOriginFilter(\n config: Config,\n): (origin: string) => boolean {\n const appUrl = config.getString('app.baseUrl');\n const { origin: appOrigin } = new URL(appUrl);\n\n const allowedOrigins = config.getOptionalStringArray(\n 'auth.experimentalExtraAllowedOrigins',\n );\n\n const allowedOriginPatterns =\n allowedOrigins?.map(\n pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }),\n ) ?? [];\n\n return origin => {\n if (origin === appOrigin) {\n return true;\n }\n return allowedOriginPatterns.some(pattern => pattern.match(origin));\n };\n}\n"],"names":["CatalogAuthResolverContext","Router","assertError","NotFoundError","Minimatch"],"mappings":"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"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/providers/router.ts"],"sourcesContent":["/*\n * Copyright 2024 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 { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { assertError, NotFoundError } from '@backstage/errors';\nimport {\n AuthOwnershipResolver,\n AuthProviderFactory,\n} from '@backstage/plugin-auth-node';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { Minimatch } from 'minimatch';\nimport { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext';\nimport { TokenIssuer } from '../identity/types';\nimport { UserInfoDatabase } from '../database/UserInfoDatabase';\n\nexport type ProviderFactories = { [s: string]: AuthProviderFactory };\n\nexport function bindProviderRouters(\n targetRouter: express.Router,\n options: {\n providers: ProviderFactories;\n appUrl: string;\n baseUrl: string;\n config: Config;\n logger: LoggerService;\n auth: AuthService;\n tokenIssuer: TokenIssuer;\n userInfo: UserInfoDatabase;\n ownershipResolver?: AuthOwnershipResolver;\n catalog: CatalogService;\n },\n) {\n const {\n providers,\n appUrl,\n baseUrl,\n config,\n logger,\n auth,\n tokenIssuer,\n catalog,\n ownershipResolver,\n userInfo,\n } = options;\n\n const providersConfig = config.getOptionalConfig('auth.providers');\n\n const isOriginAllowed = createOriginFilter(config);\n\n for (const [providerId, providerFactory] of Object.entries(providers)) {\n if (providersConfig?.has(providerId)) {\n logger.info(`Configuring auth provider: ${providerId}`);\n try {\n const provider = providerFactory({\n providerId,\n appUrl,\n baseUrl,\n isOriginAllowed,\n globalConfig: {\n baseUrl,\n appUrl,\n isOriginAllowed,\n },\n config: providersConfig.getConfig(providerId),\n logger,\n resolverContext: CatalogAuthResolverContext.create({\n logger,\n catalog,\n tokenIssuer,\n auth,\n ownershipResolver,\n userInfo,\n }),\n });\n\n const r = Router();\n\n r.get('/start', provider.start.bind(provider));\n r.get('/handler/frame', provider.frameHandler.bind(provider));\n r.post('/handler/frame', provider.frameHandler.bind(provider));\n if (provider.logout) {\n r.post('/logout', provider.logout.bind(provider));\n }\n if (provider.refresh) {\n r.get('/refresh', provider.refresh.bind(provider));\n r.post('/refresh', provider.refresh.bind(provider));\n }\n\n targetRouter.use(`/${providerId}`, r);\n } catch (e) {\n assertError(e);\n if (process.env.NODE_ENV !== 'development') {\n throw new Error(\n `Failed to initialize ${providerId} auth provider, ${e.message}`,\n );\n }\n\n logger.warn(`Skipping ${providerId} auth provider, ${e.message}`);\n\n targetRouter.use(`/${providerId}`, () => {\n // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found.\n throw new NotFoundError(\n `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` +\n `auth.providers.${providerId} are missing or the environment variables used are not defined. ` +\n `Check the auth backend plugin logs when the backend starts to see more details.`,\n );\n });\n }\n } else {\n targetRouter.use(`/${providerId}`, () => {\n throw new NotFoundError(\n `No auth provider registered for '${providerId}'`,\n );\n });\n }\n }\n}\n\nexport function createOriginFilter(\n config: Config,\n): (origin: string) => boolean {\n const appUrl = config.getString('app.baseUrl');\n const { origin: appOrigin } = new URL(appUrl);\n\n const allowedOrigins = config.getOptionalStringArray(\n 'auth.experimentalExtraAllowedOrigins',\n );\n\n const allowedOriginPatterns =\n allowedOrigins?.map(\n pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }),\n ) ?? [];\n\n return origin => {\n if (origin === appOrigin) {\n return true;\n }\n return allowedOriginPatterns.some(pattern => pattern.match(origin));\n };\n}\n"],"names":["CatalogAuthResolverContext","Router","assertError","NotFoundError","Minimatch"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ var Router = require('express-promise-router');
4
+ var OidcService = require('./OidcService.cjs.js');
5
+ var errors = require('@backstage/errors');
6
+
7
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
+
9
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
10
+
11
+ class OidcRouter {
12
+ constructor(oidc) {
13
+ this.oidc = oidc;
14
+ }
15
+ static create(options) {
16
+ return new OidcRouter(OidcService.OidcService.create(options));
17
+ }
18
+ getRouter() {
19
+ const router = Router__default.default();
20
+ router.get("/.well-known/openid-configuration", (_req, res) => {
21
+ res.json(this.oidc.getConfiguration());
22
+ });
23
+ router.get("/.well-known/jwks.json", async (_req, res) => {
24
+ const { keys } = await this.oidc.listPublicKeys();
25
+ res.json({ keys });
26
+ });
27
+ router.get("/v1/token", (_req, res) => {
28
+ res.status(501).send("Not Implemented");
29
+ });
30
+ router.get("/v1/userinfo", async (req, res) => {
31
+ const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i);
32
+ const token = matches?.[1];
33
+ if (!token) {
34
+ throw new errors.AuthenticationError("No token provided");
35
+ }
36
+ const userInfo = await this.oidc.getUserInfo({ token });
37
+ if (!userInfo) {
38
+ res.status(404).send("User info not found");
39
+ return;
40
+ }
41
+ res.json(userInfo);
42
+ });
43
+ return router;
44
+ }
45
+ }
46
+
47
+ exports.OidcRouter = OidcRouter;
48
+ //# sourceMappingURL=OidcRouter.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OidcRouter.cjs.js","sources":["../../src/service/OidcRouter.ts"],"sourcesContent":["/*\n * Copyright 2025 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 */\nimport Router from 'express-promise-router';\nimport { OidcService } from './OidcService';\nimport { AuthenticationError } from '@backstage/errors';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { TokenIssuer } from '../identity/types';\nimport { UserInfoDatabase } from '../database/UserInfoDatabase';\n\nexport class OidcRouter {\n private constructor(private readonly oidc: OidcService) {}\n\n static create(options: {\n auth: AuthService;\n tokenIssuer: TokenIssuer;\n baseUrl: string;\n userInfo: UserInfoDatabase;\n }) {\n return new OidcRouter(OidcService.create(options));\n }\n\n public getRouter() {\n const router = Router();\n\n router.get('/.well-known/openid-configuration', (_req, res) => {\n res.json(this.oidc.getConfiguration());\n });\n\n router.get('/.well-known/jwks.json', async (_req, res) => {\n const { keys } = await this.oidc.listPublicKeys();\n res.json({ keys });\n });\n\n router.get('/v1/token', (_req, res) => {\n res.status(501).send('Not Implemented');\n });\n\n // This endpoint doesn't use the regular HttpAuthoidc, since the contract\n // is specifically for the header to be communicated in the Authorization\n // header, regardless of token type\n router.get('/v1/userinfo', async (req, res) => {\n const matches = req.headers.authorization?.match(/^Bearer[ ]+(\\S+)$/i);\n const token = matches?.[1];\n if (!token) {\n throw new AuthenticationError('No token provided');\n }\n\n const userInfo = await this.oidc.getUserInfo({ token });\n\n if (!userInfo) {\n res.status(404).send('User info not found');\n return;\n }\n\n res.json(userInfo);\n });\n\n return router;\n }\n}\n"],"names":["OidcService","Router","AuthenticationError"],"mappings":";;;;;;;;;;AAsBO,MAAM,UAAW,CAAA;AAAA,EACd,YAA6B,IAAmB,EAAA;AAAnB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA;AAAoB,EAEzD,OAAO,OAAO,OAKX,EAAA;AACD,IAAA,OAAO,IAAI,UAAA,CAAWA,uBAAY,CAAA,MAAA,CAAO,OAAO,CAAC,CAAA;AAAA;AACnD,EAEO,SAAY,GAAA;AACjB,IAAA,MAAM,SAASC,uBAAO,EAAA;AAEtB,IAAA,MAAA,CAAO,GAAI,CAAA,mCAAA,EAAqC,CAAC,IAAA,EAAM,GAAQ,KAAA;AAC7D,MAAA,GAAA,CAAI,IAAK,CAAA,IAAA,CAAK,IAAK,CAAA,gBAAA,EAAkB,CAAA;AAAA,KACtC,CAAA;AAED,IAAA,MAAA,CAAO,GAAI,CAAA,wBAAA,EAA0B,OAAO,IAAA,EAAM,GAAQ,KAAA;AACxD,MAAA,MAAM,EAAE,IAAK,EAAA,GAAI,MAAM,IAAA,CAAK,KAAK,cAAe,EAAA;AAChD,MAAI,GAAA,CAAA,IAAA,CAAK,EAAE,IAAA,EAAM,CAAA;AAAA,KAClB,CAAA;AAED,IAAA,MAAA,CAAO,GAAI,CAAA,WAAA,EAAa,CAAC,IAAA,EAAM,GAAQ,KAAA;AACrC,MAAA,GAAA,CAAI,MAAO,CAAA,GAAG,CAAE,CAAA,IAAA,CAAK,iBAAiB,CAAA;AAAA,KACvC,CAAA;AAKD,IAAA,MAAA,CAAO,GAAI,CAAA,cAAA,EAAgB,OAAO,GAAA,EAAK,GAAQ,KAAA;AAC7C,MAAA,MAAM,OAAU,GAAA,GAAA,CAAI,OAAQ,CAAA,aAAA,EAAe,MAAM,oBAAoB,CAAA;AACrE,MAAM,MAAA,KAAA,GAAQ,UAAU,CAAC,CAAA;AACzB,MAAA,IAAI,CAAC,KAAO,EAAA;AACV,QAAM,MAAA,IAAIC,2BAAoB,mBAAmB,CAAA;AAAA;AAGnD,MAAA,MAAM,WAAW,MAAM,IAAA,CAAK,KAAK,WAAY,CAAA,EAAE,OAAO,CAAA;AAEtD,MAAA,IAAI,CAAC,QAAU,EAAA;AACb,QAAA,GAAA,CAAI,MAAO,CAAA,GAAG,CAAE,CAAA,IAAA,CAAK,qBAAqB,CAAA;AAC1C,QAAA;AAAA;AAGF,MAAA,GAAA,CAAI,KAAK,QAAQ,CAAA;AAAA,KAClB,CAAA;AAED,IAAO,OAAA,MAAA;AAAA;AAEX;;;;"}
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ var errors = require('@backstage/errors');
4
+ var jose = require('jose');
5
+
6
+ class OidcService {
7
+ constructor(auth, tokenIssuer, baseUrl, userInfo) {
8
+ this.auth = auth;
9
+ this.tokenIssuer = tokenIssuer;
10
+ this.baseUrl = baseUrl;
11
+ this.userInfo = userInfo;
12
+ }
13
+ static create(options) {
14
+ return new OidcService(
15
+ options.auth,
16
+ options.tokenIssuer,
17
+ options.baseUrl,
18
+ options.userInfo
19
+ );
20
+ }
21
+ getConfiguration() {
22
+ return {
23
+ issuer: this.baseUrl,
24
+ token_endpoint: `${this.baseUrl}/v1/token`,
25
+ userinfo_endpoint: `${this.baseUrl}/v1/userinfo`,
26
+ jwks_uri: `${this.baseUrl}/.well-known/jwks.json`,
27
+ response_types_supported: ["id_token"],
28
+ subject_types_supported: ["public"],
29
+ id_token_signing_alg_values_supported: [
30
+ "RS256",
31
+ "RS384",
32
+ "RS512",
33
+ "ES256",
34
+ "ES384",
35
+ "ES512",
36
+ "PS256",
37
+ "PS384",
38
+ "PS512",
39
+ "EdDSA"
40
+ ],
41
+ scopes_supported: ["openid"],
42
+ token_endpoint_auth_methods_supported: [],
43
+ claims_supported: ["sub", "ent"],
44
+ grant_types_supported: []
45
+ };
46
+ }
47
+ async listPublicKeys() {
48
+ return await this.tokenIssuer.listPublicKeys();
49
+ }
50
+ async getUserInfo({ token }) {
51
+ const credentials = await this.auth.authenticate(token, {
52
+ allowLimitedAccess: true
53
+ });
54
+ if (!this.auth.isPrincipal(credentials, "user")) {
55
+ throw new errors.InputError(
56
+ "Userinfo endpoint must be called with a token that represents a user principal"
57
+ );
58
+ }
59
+ const { sub: userEntityRef } = jose.decodeJwt(token);
60
+ if (typeof userEntityRef !== "string") {
61
+ throw new Error("Invalid user token, user entity ref must be a string");
62
+ }
63
+ return await this.userInfo.getUserInfo(userEntityRef);
64
+ }
65
+ }
66
+
67
+ exports.OidcService = OidcService;
68
+ //# sourceMappingURL=OidcService.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OidcService.cjs.js","sources":["../../src/service/OidcService.ts"],"sourcesContent":["/*\n * Copyright 2025 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 */\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { TokenIssuer } from '../identity/types';\nimport { UserInfoDatabase } from '../database/UserInfoDatabase';\nimport { InputError } from '@backstage/errors';\nimport { decodeJwt } from 'jose';\n\nexport class OidcService {\n private constructor(\n private readonly auth: AuthService,\n private readonly tokenIssuer: TokenIssuer,\n private readonly baseUrl: string,\n private readonly userInfo: UserInfoDatabase,\n ) {}\n\n static create(options: {\n auth: AuthService;\n tokenIssuer: TokenIssuer;\n baseUrl: string;\n userInfo: UserInfoDatabase;\n }) {\n return new OidcService(\n options.auth,\n options.tokenIssuer,\n options.baseUrl,\n options.userInfo,\n );\n }\n\n public getConfiguration() {\n return {\n issuer: this.baseUrl,\n token_endpoint: `${this.baseUrl}/v1/token`,\n userinfo_endpoint: `${this.baseUrl}/v1/userinfo`,\n jwks_uri: `${this.baseUrl}/.well-known/jwks.json`,\n response_types_supported: ['id_token'],\n subject_types_supported: ['public'],\n id_token_signing_alg_values_supported: [\n 'RS256',\n 'RS384',\n 'RS512',\n 'ES256',\n 'ES384',\n 'ES512',\n 'PS256',\n 'PS384',\n 'PS512',\n 'EdDSA',\n ],\n scopes_supported: ['openid'],\n token_endpoint_auth_methods_supported: [],\n claims_supported: ['sub', 'ent'],\n grant_types_supported: [],\n };\n }\n\n public async listPublicKeys() {\n return await this.tokenIssuer.listPublicKeys();\n }\n\n public async getUserInfo({ token }: { token: string }) {\n const credentials = await this.auth.authenticate(token, {\n allowLimitedAccess: true,\n });\n if (!this.auth.isPrincipal(credentials, 'user')) {\n throw new InputError(\n 'Userinfo endpoint must be called with a token that represents a user principal',\n );\n }\n\n const { sub: userEntityRef } = decodeJwt(token);\n\n if (typeof userEntityRef !== 'string') {\n throw new Error('Invalid user token, user entity ref must be a string');\n }\n return await this.userInfo.getUserInfo(userEntityRef);\n }\n}\n"],"names":["InputError","decodeJwt"],"mappings":";;;;;AAqBO,MAAM,WAAY,CAAA;AAAA,EACf,WACW,CAAA,IAAA,EACA,WACA,EAAA,OAAA,EACA,QACjB,EAAA;AAJiB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AACA,IAAA,IAAA,CAAA,WAAA,GAAA,WAAA;AACA,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AACA,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA;AAChB,EAEH,OAAO,OAAO,OAKX,EAAA;AACD,IAAA,OAAO,IAAI,WAAA;AAAA,MACT,OAAQ,CAAA,IAAA;AAAA,MACR,OAAQ,CAAA,WAAA;AAAA,MACR,OAAQ,CAAA,OAAA;AAAA,MACR,OAAQ,CAAA;AAAA,KACV;AAAA;AACF,EAEO,gBAAmB,GAAA;AACxB,IAAO,OAAA;AAAA,MACL,QAAQ,IAAK,CAAA,OAAA;AAAA,MACb,cAAA,EAAgB,CAAG,EAAA,IAAA,CAAK,OAAO,CAAA,SAAA,CAAA;AAAA,MAC/B,iBAAA,EAAmB,CAAG,EAAA,IAAA,CAAK,OAAO,CAAA,YAAA,CAAA;AAAA,MAClC,QAAA,EAAU,CAAG,EAAA,IAAA,CAAK,OAAO,CAAA,sBAAA,CAAA;AAAA,MACzB,wBAAA,EAA0B,CAAC,UAAU,CAAA;AAAA,MACrC,uBAAA,EAAyB,CAAC,QAAQ,CAAA;AAAA,MAClC,qCAAuC,EAAA;AAAA,QACrC,OAAA;AAAA,QACA,OAAA;AAAA,QACA,OAAA;AAAA,QACA,OAAA;AAAA,QACA,OAAA;AAAA,QACA,OAAA;AAAA,QACA,OAAA;AAAA,QACA,OAAA;AAAA,QACA,OAAA;AAAA,QACA;AAAA,OACF;AAAA,MACA,gBAAA,EAAkB,CAAC,QAAQ,CAAA;AAAA,MAC3B,uCAAuC,EAAC;AAAA,MACxC,gBAAA,EAAkB,CAAC,KAAA,EAAO,KAAK,CAAA;AAAA,MAC/B,uBAAuB;AAAC,KAC1B;AAAA;AACF,EAEA,MAAa,cAAiB,GAAA;AAC5B,IAAO,OAAA,MAAM,IAAK,CAAA,WAAA,CAAY,cAAe,EAAA;AAAA;AAC/C,EAEA,MAAa,WAAA,CAAY,EAAE,KAAA,EAA4B,EAAA;AACrD,IAAA,MAAM,WAAc,GAAA,MAAM,IAAK,CAAA,IAAA,CAAK,aAAa,KAAO,EAAA;AAAA,MACtD,kBAAoB,EAAA;AAAA,KACrB,CAAA;AACD,IAAA,IAAI,CAAC,IAAK,CAAA,IAAA,CAAK,WAAY,CAAA,WAAA,EAAa,MAAM,CAAG,EAAA;AAC/C,MAAA,MAAM,IAAIA,iBAAA;AAAA,QACR;AAAA,OACF;AAAA;AAGF,IAAA,MAAM,EAAE,GAAA,EAAK,aAAc,EAAA,GAAIC,eAAU,KAAK,CAAA;AAE9C,IAAI,IAAA,OAAO,kBAAkB,QAAU,EAAA;AACrC,MAAM,MAAA,IAAI,MAAM,sDAAsD,CAAA;AAAA;AAExE,IAAA,OAAO,MAAM,IAAA,CAAK,QAAS,CAAA,WAAA,CAAY,aAAa,CAAA;AAAA;AAExD;;;;"}
@@ -4,10 +4,9 @@ var express = require('express');
4
4
  var Router = require('express-promise-router');
5
5
  var cookieParser = require('cookie-parser');
6
6
  var errors = require('@backstage/errors');
7
- var router$1 = require('../identity/router.cjs.js');
8
7
  var KeyStores = require('../identity/KeyStores.cjs.js');
9
8
  var TokenFactory = require('../identity/TokenFactory.cjs.js');
10
- var UserInfoDatabaseHandler = require('../identity/UserInfoDatabaseHandler.cjs.js');
9
+ var UserInfoDatabase = require('../database/UserInfoDatabase.cjs.js');
11
10
  var session = require('express-session');
12
11
  var connectSessionKnex = require('connect-session-knex');
13
12
  var passport = require('passport');
@@ -16,6 +15,7 @@ var readBackstageTokenExpiration = require('./readBackstageTokenExpiration.cjs.j
16
15
  var StaticTokenIssuer = require('../identity/StaticTokenIssuer.cjs.js');
17
16
  var StaticKeyStore = require('../identity/StaticKeyStore.cjs.js');
18
17
  var router = require('../providers/router.cjs.js');
18
+ var OidcRouter = require('./OidcRouter.cjs.js');
19
19
 
20
20
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
21
21
 
@@ -31,22 +31,22 @@ async function createRouter(options) {
31
31
  logger,
32
32
  config,
33
33
  discovery,
34
- database,
34
+ database: db,
35
35
  tokenFactoryAlgorithm,
36
36
  providerFactories = {}
37
37
  } = options;
38
- const router$2 = Router__default.default();
38
+ const router$1 = Router__default.default();
39
39
  const appUrl = config.getString("app.baseUrl");
40
40
  const authUrl = await discovery.getExternalBaseUrl("auth");
41
41
  const backstageTokenExpiration = readBackstageTokenExpiration.readBackstageTokenExpiration(config);
42
- const authDb = AuthDatabase.AuthDatabase.create(database);
42
+ const database = AuthDatabase.AuthDatabase.create(db);
43
43
  const keyStore = await KeyStores.KeyStores.fromConfig(config, {
44
44
  logger,
45
- database: authDb
45
+ database
46
+ });
47
+ const userInfo = await UserInfoDatabase.UserInfoDatabase.create({
48
+ database
46
49
  });
47
- const userInfoDatabaseHandler = new UserInfoDatabaseHandler.UserInfoDatabaseHandler(
48
- await authDb.get()
49
- );
50
50
  const omitClaimsFromToken = config.getOptionalBoolean(
51
51
  "auth.omitIdentityTokenOwnershipClaim"
52
52
  ) ? ["ent"] : [];
@@ -57,7 +57,6 @@ async function createRouter(options) {
57
57
  logger: logger.child({ component: "token-factory" }),
58
58
  issuer: authUrl,
59
59
  sessionExpirationSeconds: backstageTokenExpiration,
60
- userInfoDatabaseHandler,
61
60
  omitClaimsFromToken
62
61
  },
63
62
  keyStore
@@ -69,16 +68,15 @@ async function createRouter(options) {
69
68
  keyDurationSeconds: backstageTokenExpiration,
70
69
  logger: logger.child({ component: "token-factory" }),
71
70
  algorithm: tokenFactoryAlgorithm ?? config.getOptionalString("auth.identityTokenAlgorithm"),
72
- userInfoDatabaseHandler,
73
71
  omitClaimsFromToken
74
72
  });
75
73
  }
76
74
  const secret = config.getOptionalString("auth.session.secret");
77
75
  if (secret) {
78
- router$2.use(cookieParser__default.default(secret));
76
+ router$1.use(cookieParser__default.default(secret));
79
77
  const enforceCookieSSL = authUrl.startsWith("https");
80
78
  const KnexSessionStore = connectSessionKnex__default.default(session__default.default);
81
- router$2.use(
79
+ router$1.use(
82
80
  session__default.default({
83
81
  secret,
84
82
  saveUninitialized: false,
@@ -86,36 +84,38 @@ async function createRouter(options) {
86
84
  cookie: { secure: enforceCookieSSL ? "auto" : false },
87
85
  store: new KnexSessionStore({
88
86
  createtable: false,
89
- knex: await authDb.get()
87
+ knex: await database.get()
90
88
  })
91
89
  })
92
90
  );
93
- router$2.use(passport__default.default.initialize());
94
- router$2.use(passport__default.default.session());
91
+ router$1.use(passport__default.default.initialize());
92
+ router$1.use(passport__default.default.session());
95
93
  } else {
96
- router$2.use(cookieParser__default.default());
94
+ router$1.use(cookieParser__default.default());
97
95
  }
98
- router$2.use(express__default.default.urlencoded({ extended: false }));
99
- router$2.use(express__default.default.json());
100
- router.bindProviderRouters(router$2, {
96
+ router$1.use(express__default.default.urlencoded({ extended: false }));
97
+ router$1.use(express__default.default.json());
98
+ router.bindProviderRouters(router$1, {
101
99
  providers: providerFactories,
102
100
  appUrl,
103
101
  baseUrl: authUrl,
104
102
  tokenIssuer,
105
103
  ...options,
106
- auth: options.auth
104
+ auth: options.auth,
105
+ userInfo
107
106
  });
108
- router$1.bindOidcRouter(router$2, {
107
+ const oidcRouter = OidcRouter.OidcRouter.create({
109
108
  auth: options.auth,
110
109
  tokenIssuer,
111
110
  baseUrl: authUrl,
112
- userInfoDatabaseHandler
111
+ userInfo
113
112
  });
114
- router$2.use("/:provider/", (req) => {
113
+ router$1.use(oidcRouter.getRouter());
114
+ router$1.use("/:provider/", (req) => {
115
115
  const { provider } = req.params;
116
116
  throw new errors.NotFoundError(`Unknown auth provider '${provider}'`);
117
117
  });
118
- return router$2;
118
+ return router$1;
119
119
  }
120
120
 
121
121
  exports.createRouter = createRouter;
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\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 express from 'express';\nimport Router from 'express-promise-router';\nimport cookieParser from 'cookie-parser';\nimport {\n AuthService,\n DatabaseService,\n DiscoveryService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport { AuthOwnershipResolver } from '@backstage/plugin-auth-node';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { NotFoundError } from '@backstage/errors';\nimport { bindOidcRouter } from '../identity/router';\nimport { KeyStores } from '../identity/KeyStores';\nimport { TokenFactory } from '../identity/TokenFactory';\nimport { UserInfoDatabaseHandler } from '../identity/UserInfoDatabaseHandler';\nimport session from 'express-session';\nimport connectSessionKnex from 'connect-session-knex';\nimport passport from 'passport';\nimport { AuthDatabase } from '../database/AuthDatabase';\nimport { readBackstageTokenExpiration } from './readBackstageTokenExpiration';\nimport { TokenIssuer } from '../identity/types';\nimport { StaticTokenIssuer } from '../identity/StaticTokenIssuer';\nimport { StaticKeyStore } from '../identity/StaticKeyStore';\nimport { bindProviderRouters, ProviderFactories } from '../providers/router';\n\ninterface RouterOptions {\n logger: LoggerService;\n database: DatabaseService;\n config: RootConfigService;\n discovery: DiscoveryService;\n auth: AuthService;\n tokenFactoryAlgorithm?: string;\n providerFactories?: ProviderFactories;\n catalog: CatalogService;\n ownershipResolver?: AuthOwnershipResolver;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const {\n logger,\n config,\n discovery,\n database,\n tokenFactoryAlgorithm,\n providerFactories = {},\n } = options;\n\n const router = Router();\n\n const appUrl = config.getString('app.baseUrl');\n const authUrl = await discovery.getExternalBaseUrl('auth');\n const backstageTokenExpiration = readBackstageTokenExpiration(config);\n const authDb = AuthDatabase.create(database);\n\n const keyStore = await KeyStores.fromConfig(config, {\n logger,\n database: authDb,\n });\n\n const userInfoDatabaseHandler = new UserInfoDatabaseHandler(\n await authDb.get(),\n );\n\n const omitClaimsFromToken = config.getOptionalBoolean(\n 'auth.omitIdentityTokenOwnershipClaim',\n )\n ? ['ent']\n : [];\n\n let tokenIssuer: TokenIssuer;\n if (keyStore instanceof StaticKeyStore) {\n tokenIssuer = new StaticTokenIssuer(\n {\n logger: logger.child({ component: 'token-factory' }),\n issuer: authUrl,\n sessionExpirationSeconds: backstageTokenExpiration,\n userInfoDatabaseHandler,\n omitClaimsFromToken,\n },\n keyStore as StaticKeyStore,\n );\n } else {\n tokenIssuer = new TokenFactory({\n issuer: authUrl,\n keyStore,\n keyDurationSeconds: backstageTokenExpiration,\n logger: logger.child({ component: 'token-factory' }),\n algorithm:\n tokenFactoryAlgorithm ??\n config.getOptionalString('auth.identityTokenAlgorithm'),\n userInfoDatabaseHandler,\n omitClaimsFromToken,\n });\n }\n\n const secret = config.getOptionalString('auth.session.secret');\n if (secret) {\n router.use(cookieParser(secret));\n const enforceCookieSSL = authUrl.startsWith('https');\n const KnexSessionStore = connectSessionKnex(session);\n router.use(\n session({\n secret,\n saveUninitialized: false,\n resave: false,\n cookie: { secure: enforceCookieSSL ? 'auto' : false },\n store: new KnexSessionStore({\n createtable: false,\n knex: await authDb.get(),\n }),\n }),\n );\n router.use(passport.initialize());\n router.use(passport.session());\n } else {\n router.use(cookieParser());\n }\n\n router.use(express.urlencoded({ extended: false }));\n router.use(express.json());\n\n bindProviderRouters(router, {\n providers: providerFactories,\n appUrl,\n baseUrl: authUrl,\n tokenIssuer,\n ...options,\n auth: options.auth,\n });\n\n bindOidcRouter(router, {\n auth: options.auth,\n tokenIssuer,\n baseUrl: authUrl,\n userInfoDatabaseHandler,\n });\n\n // Gives a more helpful error message than a plain 404\n router.use('/:provider/', req => {\n const { provider } = req.params;\n throw new NotFoundError(`Unknown auth provider '${provider}'`);\n });\n\n return router;\n}\n"],"names":["router","Router","readBackstageTokenExpiration","AuthDatabase","KeyStores","UserInfoDatabaseHandler","StaticKeyStore","StaticTokenIssuer","TokenFactory","cookieParser","connectSessionKnex","session","passport","express","bindProviderRouters","bindOidcRouter","NotFoundError"],"mappings":"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"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\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 express from 'express';\nimport Router from 'express-promise-router';\nimport cookieParser from 'cookie-parser';\nimport {\n AuthService,\n DatabaseService,\n DiscoveryService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport { AuthOwnershipResolver } from '@backstage/plugin-auth-node';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { NotFoundError } from '@backstage/errors';\nimport { KeyStores } from '../identity/KeyStores';\nimport { TokenFactory } from '../identity/TokenFactory';\nimport { UserInfoDatabase } from '../database/UserInfoDatabase';\nimport session from 'express-session';\nimport connectSessionKnex from 'connect-session-knex';\nimport passport from 'passport';\nimport { AuthDatabase } from '../database/AuthDatabase';\nimport { readBackstageTokenExpiration } from './readBackstageTokenExpiration';\nimport { TokenIssuer } from '../identity/types';\nimport { StaticTokenIssuer } from '../identity/StaticTokenIssuer';\nimport { StaticKeyStore } from '../identity/StaticKeyStore';\nimport { bindProviderRouters, ProviderFactories } from '../providers/router';\nimport { OidcRouter } from './OidcRouter';\n\ninterface RouterOptions {\n logger: LoggerService;\n database: DatabaseService;\n config: RootConfigService;\n discovery: DiscoveryService;\n auth: AuthService;\n tokenFactoryAlgorithm?: string;\n providerFactories?: ProviderFactories;\n catalog: CatalogService;\n ownershipResolver?: AuthOwnershipResolver;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const {\n logger,\n config,\n discovery,\n database: db,\n tokenFactoryAlgorithm,\n providerFactories = {},\n } = options;\n\n const router = Router();\n\n const appUrl = config.getString('app.baseUrl');\n const authUrl = await discovery.getExternalBaseUrl('auth');\n const backstageTokenExpiration = readBackstageTokenExpiration(config);\n const database = AuthDatabase.create(db);\n\n const keyStore = await KeyStores.fromConfig(config, {\n logger,\n database,\n });\n\n const userInfo = await UserInfoDatabase.create({\n database,\n });\n\n const omitClaimsFromToken = config.getOptionalBoolean(\n 'auth.omitIdentityTokenOwnershipClaim',\n )\n ? ['ent']\n : [];\n\n let tokenIssuer: TokenIssuer;\n if (keyStore instanceof StaticKeyStore) {\n tokenIssuer = new StaticTokenIssuer(\n {\n logger: logger.child({ component: 'token-factory' }),\n issuer: authUrl,\n sessionExpirationSeconds: backstageTokenExpiration,\n omitClaimsFromToken,\n },\n keyStore as StaticKeyStore,\n );\n } else {\n tokenIssuer = new TokenFactory({\n issuer: authUrl,\n keyStore,\n keyDurationSeconds: backstageTokenExpiration,\n logger: logger.child({ component: 'token-factory' }),\n algorithm:\n tokenFactoryAlgorithm ??\n config.getOptionalString('auth.identityTokenAlgorithm'),\n omitClaimsFromToken,\n });\n }\n\n const secret = config.getOptionalString('auth.session.secret');\n if (secret) {\n router.use(cookieParser(secret));\n const enforceCookieSSL = authUrl.startsWith('https');\n const KnexSessionStore = connectSessionKnex(session);\n router.use(\n session({\n secret,\n saveUninitialized: false,\n resave: false,\n cookie: { secure: enforceCookieSSL ? 'auto' : false },\n store: new KnexSessionStore({\n createtable: false,\n knex: await database.get(),\n }),\n }),\n );\n router.use(passport.initialize());\n router.use(passport.session());\n } else {\n router.use(cookieParser());\n }\n\n router.use(express.urlencoded({ extended: false }));\n router.use(express.json());\n\n bindProviderRouters(router, {\n providers: providerFactories,\n appUrl,\n baseUrl: authUrl,\n tokenIssuer,\n ...options,\n auth: options.auth,\n userInfo,\n });\n\n const oidcRouter = OidcRouter.create({\n auth: options.auth,\n tokenIssuer,\n baseUrl: authUrl,\n userInfo,\n });\n\n router.use(oidcRouter.getRouter());\n\n // Gives a more helpful error message than a plain 404\n router.use('/:provider/', req => {\n const { provider } = req.params;\n throw new NotFoundError(`Unknown auth provider '${provider}'`);\n });\n\n return router;\n}\n"],"names":["router","Router","readBackstageTokenExpiration","AuthDatabase","KeyStores","UserInfoDatabase","StaticKeyStore","StaticTokenIssuer","TokenFactory","cookieParser","connectSessionKnex","session","passport","express","bindProviderRouters","OidcRouter","NotFoundError"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /*
2
+ * Copyright 2024 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ // @ts-check
18
+
19
+ /**
20
+ * @param {import('knex').Knex} knex
21
+ */
22
+ exports.up = async function up(knex) {
23
+ await knex.schema.alterTable('user_info', table => {
24
+ table.renameColumn('exp', 'updated_at');
25
+ table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
26
+ });
27
+ };
28
+
29
+ /**
30
+ * @param {import('knex').Knex} knex
31
+ */
32
+ exports.down = async function down(knex) {
33
+ await knex.schema.alterTable('user_info', table => {
34
+ table.dropColumn('created_at');
35
+ table.renameColumn('updated_at', 'exp');
36
+ });
37
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-auth-backend",
3
- "version": "0.25.1",
3
+ "version": "0.25.2-next.1",
4
4
  "description": "A Backstage backend plugin that handles authentication",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -46,13 +46,13 @@
46
46
  "test": "backstage-cli package test"
47
47
  },
48
48
  "dependencies": {
49
- "@backstage/backend-plugin-api": "^1.4.0",
50
- "@backstage/catalog-model": "^1.7.4",
51
- "@backstage/config": "^1.3.2",
52
- "@backstage/errors": "^1.2.7",
53
- "@backstage/plugin-auth-node": "^0.6.4",
54
- "@backstage/plugin-catalog-node": "^1.17.1",
55
- "@backstage/types": "^1.2.1",
49
+ "@backstage/backend-plugin-api": "1.4.1-next.0",
50
+ "@backstage/catalog-model": "1.7.5-next.0",
51
+ "@backstage/config": "1.3.3-next.0",
52
+ "@backstage/errors": "1.2.7",
53
+ "@backstage/plugin-auth-node": "0.6.5-next.0",
54
+ "@backstage/plugin-catalog-node": "1.17.2-next.0",
55
+ "@backstage/types": "1.2.1",
56
56
  "@google-cloud/firestore": "^7.0.0",
57
57
  "connect-session-knex": "^4.0.0",
58
58
  "cookie-parser": "^1.4.5",
@@ -68,11 +68,11 @@
68
68
  "uuid": "^11.0.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@backstage/backend-defaults": "^0.11.0",
72
- "@backstage/backend-test-utils": "^1.6.0",
73
- "@backstage/cli": "^0.33.0",
74
- "@backstage/plugin-auth-backend-module-google-provider": "^0.3.4",
75
- "@backstage/plugin-auth-backend-module-guest-provider": "^0.2.9",
71
+ "@backstage/backend-defaults": "0.11.1-next.1",
72
+ "@backstage/backend-test-utils": "1.7.0-next.1",
73
+ "@backstage/cli": "0.33.1-next.2",
74
+ "@backstage/plugin-auth-backend-module-google-provider": "0.3.5-next.0",
75
+ "@backstage/plugin-auth-backend-module-guest-provider": "0.2.10-next.0",
76
76
  "@types/cookie-parser": "^1.4.2",
77
77
  "@types/express": "^4.17.6",
78
78
  "@types/express-session": "^1.17.2",
@@ -1 +0,0 @@
1
- {"version":3,"file":"UserInfoDatabaseHandler.cjs.js","sources":["../../src/identity/UserInfoDatabaseHandler.ts"],"sourcesContent":["/*\n * Copyright 2024 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 { DateTime } from 'luxon';\nimport { Knex } from 'knex';\n\nimport { BackstageTokenPayload } from './TokenFactory';\n\nconst TABLE = 'user_info';\n\ntype Row = {\n user_entity_ref: string;\n user_info: string;\n exp: string;\n};\n\ntype UserInfo = {\n claims: Omit<BackstageTokenPayload, 'aud' | 'iat' | 'iss' | 'uip'>;\n};\n\nexport class UserInfoDatabaseHandler {\n constructor(private readonly client: Knex) {}\n\n async addUserInfo(userInfo: UserInfo): Promise<void> {\n await this.client<Row>(TABLE)\n .insert({\n user_entity_ref: userInfo.claims.sub as string,\n user_info: JSON.stringify(userInfo),\n exp: DateTime.fromSeconds(userInfo.claims.exp as number, {\n zone: 'utc',\n }).toSQL({ includeOffset: false }),\n })\n .onConflict('user_entity_ref')\n .merge();\n }\n\n async getUserInfo(userEntityRef: string): Promise<UserInfo | undefined> {\n const info = await this.client<Row>(TABLE)\n .where({ user_entity_ref: userEntityRef })\n .first();\n\n if (!info) {\n return undefined;\n }\n\n const userInfo = JSON.parse(info.user_info);\n return userInfo;\n }\n}\n"],"names":["DateTime"],"mappings":";;;;AAqBA,MAAM,KAAQ,GAAA,WAAA;AAYP,MAAM,uBAAwB,CAAA;AAAA,EACnC,YAA6B,MAAc,EAAA;AAAd,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA;AAAe,EAE5C,MAAM,YAAY,QAAmC,EAAA;AACnD,IAAA,MAAM,IAAK,CAAA,MAAA,CAAY,KAAK,CAAA,CACzB,MAAO,CAAA;AAAA,MACN,eAAA,EAAiB,SAAS,MAAO,CAAA,GAAA;AAAA,MACjC,SAAA,EAAW,IAAK,CAAA,SAAA,CAAU,QAAQ,CAAA;AAAA,MAClC,GAAK,EAAAA,cAAA,CAAS,WAAY,CAAA,QAAA,CAAS,OAAO,GAAe,EAAA;AAAA,QACvD,IAAM,EAAA;AAAA,OACP,CAAE,CAAA,KAAA,CAAM,EAAE,aAAA,EAAe,OAAO;AAAA,KAClC,CAAA,CACA,UAAW,CAAA,iBAAiB,EAC5B,KAAM,EAAA;AAAA;AACX,EAEA,MAAM,YAAY,aAAsD,EAAA;AACtE,IAAA,MAAM,IAAO,GAAA,MAAM,IAAK,CAAA,MAAA,CAAY,KAAK,CAAA,CACtC,KAAM,CAAA,EAAE,eAAiB,EAAA,aAAA,EAAe,CAAA,CACxC,KAAM,EAAA;AAET,IAAA,IAAI,CAAC,IAAM,EAAA;AACT,MAAO,OAAA,KAAA,CAAA;AAAA;AAGT,IAAA,MAAM,QAAW,GAAA,IAAA,CAAK,KAAM,CAAA,IAAA,CAAK,SAAS,CAAA;AAC1C,IAAO,OAAA,QAAA;AAAA;AAEX;;;;"}
@@ -1,77 +0,0 @@
1
- 'use strict';
2
-
3
- var Router = require('express-promise-router');
4
- var jose = require('jose');
5
- var errors = require('@backstage/errors');
6
-
7
- function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
-
9
- var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
10
-
11
- function bindOidcRouter(targetRouter, options) {
12
- const { baseUrl, auth, tokenIssuer, userInfoDatabaseHandler } = options;
13
- const router = Router__default.default();
14
- targetRouter.use(router);
15
- const config = {
16
- issuer: baseUrl,
17
- token_endpoint: `${baseUrl}/v1/token`,
18
- userinfo_endpoint: `${baseUrl}/v1/userinfo`,
19
- jwks_uri: `${baseUrl}/.well-known/jwks.json`,
20
- response_types_supported: ["id_token"],
21
- subject_types_supported: ["public"],
22
- id_token_signing_alg_values_supported: [
23
- "RS256",
24
- "RS384",
25
- "RS512",
26
- "ES256",
27
- "ES384",
28
- "ES512",
29
- "PS256",
30
- "PS384",
31
- "PS512",
32
- "EdDSA"
33
- ],
34
- scopes_supported: ["openid"],
35
- token_endpoint_auth_methods_supported: [],
36
- claims_supported: ["sub", "ent"],
37
- grant_types_supported: []
38
- };
39
- router.get("/.well-known/openid-configuration", (_req, res) => {
40
- res.json(config);
41
- });
42
- router.get("/.well-known/jwks.json", async (_req, res) => {
43
- const { keys } = await tokenIssuer.listPublicKeys();
44
- res.json({ keys });
45
- });
46
- router.get("/v1/token", (_req, res) => {
47
- res.status(501).send("Not Implemented");
48
- });
49
- router.get("/v1/userinfo", async (req, res) => {
50
- const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i);
51
- const token = matches?.[1];
52
- if (!token) {
53
- throw new errors.AuthenticationError("No token provided");
54
- }
55
- const credentials = await auth.authenticate(token, {
56
- allowLimitedAccess: true
57
- });
58
- if (!auth.isPrincipal(credentials, "user")) {
59
- throw new errors.InputError(
60
- "Userinfo endpoint must be called with a token that represents a user principal"
61
- );
62
- }
63
- const { sub: userEntityRef } = jose.decodeJwt(token);
64
- if (typeof userEntityRef !== "string") {
65
- throw new Error("Invalid user token, user entity ref must be a string");
66
- }
67
- const userInfo = await userInfoDatabaseHandler.getUserInfo(userEntityRef);
68
- if (!userInfo) {
69
- res.status(404).send("User info not found");
70
- return;
71
- }
72
- res.json(userInfo);
73
- });
74
- }
75
-
76
- exports.bindOidcRouter = bindOidcRouter;
77
- //# sourceMappingURL=router.cjs.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../../src/identity/router.ts"],"sourcesContent":["/*\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 express from 'express';\nimport Router from 'express-promise-router';\nimport { TokenIssuer } from './types';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { decodeJwt } from 'jose';\nimport { AuthenticationError, InputError } from '@backstage/errors';\nimport { UserInfoDatabaseHandler } from './UserInfoDatabaseHandler';\n\nexport function bindOidcRouter(\n targetRouter: express.Router,\n options: {\n baseUrl: string;\n auth: AuthService;\n tokenIssuer: TokenIssuer;\n userInfoDatabaseHandler: UserInfoDatabaseHandler;\n },\n) {\n const { baseUrl, auth, tokenIssuer, userInfoDatabaseHandler } = options;\n\n const router = Router();\n targetRouter.use(router);\n\n const config = {\n issuer: baseUrl,\n token_endpoint: `${baseUrl}/v1/token`,\n userinfo_endpoint: `${baseUrl}/v1/userinfo`,\n jwks_uri: `${baseUrl}/.well-known/jwks.json`,\n response_types_supported: ['id_token'],\n subject_types_supported: ['public'],\n id_token_signing_alg_values_supported: [\n 'RS256',\n 'RS384',\n 'RS512',\n 'ES256',\n 'ES384',\n 'ES512',\n 'PS256',\n 'PS384',\n 'PS512',\n 'EdDSA',\n ],\n scopes_supported: ['openid'],\n token_endpoint_auth_methods_supported: [],\n claims_supported: ['sub', 'ent'],\n grant_types_supported: [],\n };\n\n router.get('/.well-known/openid-configuration', (_req, res) => {\n res.json(config);\n });\n\n router.get('/.well-known/jwks.json', async (_req, res) => {\n const { keys } = await tokenIssuer.listPublicKeys();\n res.json({ keys });\n });\n\n router.get('/v1/token', (_req, res) => {\n res.status(501).send('Not Implemented');\n });\n\n // This endpoint doesn't use the regular HttpAuthService, since the contract\n // is specifically for the header to be communicated in the Authorization\n // header, regardless of token type\n router.get('/v1/userinfo', async (req, res) => {\n const matches = req.headers.authorization?.match(/^Bearer[ ]+(\\S+)$/i);\n const token = matches?.[1];\n if (!token) {\n throw new AuthenticationError('No token provided');\n }\n\n const credentials = await auth.authenticate(token, {\n allowLimitedAccess: true,\n });\n if (!auth.isPrincipal(credentials, 'user')) {\n throw new InputError(\n 'Userinfo endpoint must be called with a token that represents a user principal',\n );\n }\n\n const { sub: userEntityRef } = decodeJwt(token);\n\n if (typeof userEntityRef !== 'string') {\n throw new Error('Invalid user token, user entity ref must be a string');\n }\n\n const userInfo = await userInfoDatabaseHandler.getUserInfo(userEntityRef);\n if (!userInfo) {\n res.status(404).send('User info not found');\n return;\n }\n\n res.json(userInfo);\n });\n}\n"],"names":["Router","AuthenticationError","InputError","decodeJwt"],"mappings":"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"}