@backstage/plugin-auth-backend 0.30.1-next.2 → 0.30.2-next.0

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/actions/createWhoAmIAction.cjs.js.map +1 -1
  3. package/dist/actions/index.cjs.js.map +1 -1
  4. package/dist/authPlugin.cjs.js.map +1 -1
  5. package/dist/database/AuthDatabase.cjs.js.map +1 -1
  6. package/dist/database/OfflineSessionDatabase.cjs.js.map +1 -1
  7. package/dist/database/OidcDatabase.cjs.js +1 -0
  8. package/dist/database/OidcDatabase.cjs.js.map +1 -1
  9. package/dist/database/UserInfoDatabase.cjs.js +1 -0
  10. package/dist/database/UserInfoDatabase.cjs.js.map +1 -1
  11. package/dist/identity/DatabaseKeyStore.cjs.js +1 -0
  12. package/dist/identity/DatabaseKeyStore.cjs.js.map +1 -1
  13. package/dist/identity/FirestoreKeyStore.cjs.js.map +1 -1
  14. package/dist/identity/KeyStores.cjs.js.map +1 -1
  15. package/dist/identity/MemoryKeyStore.cjs.js.map +1 -1
  16. package/dist/identity/StaticKeyStore.cjs.js.map +1 -1
  17. package/dist/identity/StaticTokenIssuer.cjs.js.map +1 -1
  18. package/dist/identity/TokenFactory.cjs.js.map +1 -1
  19. package/dist/identity/issueUserToken.cjs.js.map +1 -1
  20. package/dist/lib/catalog/CatalogIdentityClient.cjs.js +1 -1
  21. package/dist/lib/catalog/CatalogIdentityClient.cjs.js.map +1 -1
  22. package/dist/lib/refreshToken.cjs.js.map +1 -1
  23. package/dist/lib/resolvers/CatalogAuthResolverContext.cjs.js +1 -3
  24. package/dist/lib/resolvers/CatalogAuthResolverContext.cjs.js.map +1 -1
  25. package/dist/providers/router.cjs.js.map +1 -1
  26. package/dist/service/CimdClient.cjs.js.map +1 -1
  27. package/dist/service/OfflineAccessService.cjs.js.map +1 -1
  28. package/dist/service/OidcError.cjs.js.map +1 -1
  29. package/dist/service/OidcRouter.cjs.js.map +1 -1
  30. package/dist/service/OidcService.cjs.js.map +1 -1
  31. package/dist/service/readTokenExpiration.cjs.js.map +1 -1
  32. package/dist/service/router.cjs.js.map +1 -1
  33. package/package.json +12 -12
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @backstage/plugin-auth-backend
2
2
 
3
+ ## 0.30.2-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @backstage/config@1.3.10-next.0
9
+ - @backstage/errors@1.3.2-next.0
10
+ - @backstage/backend-plugin-api@1.10.2-next.0
11
+ - @backstage/plugin-auth-node@0.7.7-next.0
12
+ - @backstage/catalog-model@1.10.2-next.0
13
+ - @backstage/plugin-catalog-node@2.2.6-next.0
14
+
15
+ ## 0.30.1
16
+
17
+ ### Patch Changes
18
+
19
+ - 736d84e: Use locale-insensitive Unicode casing for consistent string handling across environments.
20
+ - 08c5d9b: Fixed inconsistent URL pattern matching in token revocation.
21
+ - Updated dependencies
22
+ - @backstage/catalog-model@1.10.1
23
+ - @backstage/plugin-auth-node@0.7.6
24
+ - @backstage/config@1.3.9
25
+ - @backstage/backend-plugin-api@1.10.1
26
+ - @backstage/plugin-catalog-node@2.2.5
27
+
3
28
  ## 0.30.1-next.2
4
29
 
5
30
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"createWhoAmIAction.cjs.js","sources":["../../src/actions/createWhoAmIAction.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, UserInfoService } from '@backstage/backend-plugin-api';\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { NotAllowedError, NotFoundError } from '@backstage/errors';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\n\nexport const createWhoAmIAction = ({\n auth,\n catalog,\n userInfo,\n actionsRegistry,\n}: {\n auth: AuthService;\n catalog: CatalogService;\n userInfo: UserInfoService;\n actionsRegistry: ActionsRegistryService;\n}) => {\n actionsRegistry.register({\n name: 'who-am-i',\n title: 'Who Am I',\n attributes: {\n destructive: false,\n readOnly: true,\n idempotent: true,\n },\n description:\n 'Returns the catalog entity and user info for the currently authenticated user. This action requires user credentials and cannot be used with service or unauthenticated credentials.',\n schema: {\n input: z => z.object({}),\n output: z =>\n z.object({\n entity: z\n .object({})\n .passthrough()\n .describe('The full catalog entity for the authenticated user'),\n userInfo: z\n .object({\n userEntityRef: z\n .string()\n .describe(\n 'The entity ref of the user, e.g. user:default/jane.doe',\n ),\n ownershipEntityRefs: z\n .array(z.string())\n .describe('Entity refs that the user claims ownership through'),\n })\n .describe(\n 'User identity information extracted from the authentication token',\n ),\n }),\n },\n action: async ({ credentials }) => {\n if (!auth.isPrincipal(credentials, 'user')) {\n throw new NotAllowedError('This action requires user credentials');\n }\n\n const { userEntityRef } = credentials.principal;\n\n const [entity, info] = await Promise.all([\n catalog.getEntityByRef(userEntityRef, { credentials }),\n userInfo.getUserInfo(credentials),\n ]);\n\n if (!entity) {\n throw new NotFoundError(\n `User entity not found in the catalog for \"${userEntityRef}\"`,\n );\n }\n\n return {\n output: {\n entity,\n userInfo: info,\n },\n };\n },\n });\n};\n"],"names":["NotAllowedError","NotFoundError"],"mappings":";;;;AAoBO,MAAM,qBAAqB,CAAC;AAAA,EACjC,IAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,KAKM;AACJ,EAAA,eAAA,CAAgB,QAAA,CAAS;AAAA,IACvB,IAAA,EAAM,UAAA;AAAA,IACN,KAAA,EAAO,UAAA;AAAA,IACP,UAAA,EAAY;AAAA,MACV,WAAA,EAAa,KAAA;AAAA,MACb,QAAA,EAAU,IAAA;AAAA,MACV,UAAA,EAAY;AAAA,KACd;AAAA,IACA,WAAA,EACE,sLAAA;AAAA,IACF,MAAA,EAAQ;AAAA,MACN,KAAA,EAAO,CAAA,CAAA,KAAK,CAAA,CAAE,MAAA,CAAO,EAAE,CAAA;AAAA,MACvB,MAAA,EAAQ,CAAA,CAAA,KACN,CAAA,CAAE,MAAA,CAAO;AAAA,QACP,MAAA,EAAQ,EACL,MAAA,CAAO,EAAE,CAAA,CACT,WAAA,EAAY,CACZ,QAAA,CAAS,oDAAoD,CAAA;AAAA,QAChE,QAAA,EAAU,EACP,MAAA,CAAO;AAAA,UACN,aAAA,EAAe,CAAA,CACZ,MAAA,EAAO,CACP,QAAA;AAAA,YACC;AAAA,WACF;AAAA,UACF,mBAAA,EAAqB,EAClB,KAAA,CAAM,CAAA,CAAE,QAAQ,CAAA,CAChB,SAAS,oDAAoD;AAAA,SACjE,CAAA,CACA,QAAA;AAAA,UACC;AAAA;AACF,OACH;AAAA,KACL;AAAA,IACA,MAAA,EAAQ,OAAO,EAAE,WAAA,EAAY,KAAM;AACjC,MAAA,IAAI,CAAC,IAAA,CAAK,WAAA,CAAY,WAAA,EAAa,MAAM,CAAA,EAAG;AAC1C,QAAA,MAAM,IAAIA,uBAAgB,uCAAuC,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,EAAE,aAAA,EAAc,GAAI,WAAA,CAAY,SAAA;AAEtC,MAAA,MAAM,CAAC,MAAA,EAAQ,IAAI,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,QACvC,OAAA,CAAQ,cAAA,CAAe,aAAA,EAAe,EAAE,aAAa,CAAA;AAAA,QACrD,QAAA,CAAS,YAAY,WAAW;AAAA,OACjC,CAAA;AAED,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,MAAM,IAAIC,oBAAA;AAAA,UACR,6CAA6C,aAAa,CAAA,CAAA;AAAA,SAC5D;AAAA,MACF;AAEA,MAAA,OAAO;AAAA,QACL,MAAA,EAAQ;AAAA,UACN,MAAA;AAAA,UACA,QAAA,EAAU;AAAA;AACZ,OACF;AAAA,IACF;AAAA,GACD,CAAA;AACH;;;;"}
1
+ {"version":3,"file":"createWhoAmIAction.cjs.js","sources":["../../src/actions/createWhoAmIAction.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, UserInfoService } from '@backstage/backend-plugin-api';\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { NotAllowedError, NotFoundError } from '@backstage/errors';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\n\nexport const createWhoAmIAction = ({\n auth,\n catalog,\n userInfo,\n actionsRegistry,\n}: {\n auth: AuthService;\n catalog: CatalogService;\n userInfo: UserInfoService;\n actionsRegistry: ActionsRegistryService;\n}) => {\n actionsRegistry.register({\n name: 'who-am-i',\n title: 'Who Am I',\n attributes: {\n destructive: false,\n readOnly: true,\n idempotent: true,\n },\n description:\n 'Returns the catalog entity and user info for the currently authenticated user. This action requires user credentials and cannot be used with service or unauthenticated credentials.',\n schema: {\n input: z => z.object({}),\n output: z =>\n z.object({\n entity: z\n .object({})\n .passthrough()\n .describe('The full catalog entity for the authenticated user'),\n userInfo: z\n .object({\n userEntityRef: z\n .string()\n .describe(\n 'The entity ref of the user, e.g. user:default/jane.doe',\n ),\n ownershipEntityRefs: z\n .array(z.string())\n .describe('Entity refs that the user claims ownership through'),\n })\n .describe(\n 'User identity information extracted from the authentication token',\n ),\n }),\n },\n action: async ({ credentials }) => {\n if (!auth.isPrincipal(credentials, 'user')) {\n throw new NotAllowedError('This action requires user credentials');\n }\n\n const { userEntityRef } = credentials.principal;\n\n const [entity, info] = await Promise.all([\n catalog.getEntityByRef(userEntityRef, { credentials }),\n userInfo.getUserInfo(credentials),\n ]);\n\n if (!entity) {\n throw new NotFoundError(\n `User entity not found in the catalog for \"${userEntityRef}\"`,\n );\n }\n\n return {\n output: {\n entity,\n userInfo: info,\n },\n };\n },\n });\n};\n"],"names":["NotAllowedError","NotFoundError"],"mappings":";;;;AAoBO,MAAM,qBAAqB,CAAC;AAAA,EACjC,IAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,KAKM;AACJ,EAAA,eAAA,CAAgB,QAAA,CAAS;AAAA,IACvB,IAAA,EAAM,UAAA;AAAA,IACN,KAAA,EAAO,UAAA;AAAA,IACP,UAAA,EAAY;AAAA,MACV,WAAA,EAAa,KAAA;AAAA,MACb,QAAA,EAAU,IAAA;AAAA,MACV,UAAA,EAAY;AAAA,KACd;AAAA,IACA,WAAA,EACE,sLAAA;AAAA,IACF,MAAA,EAAQ;AAAA,MACN,KAAA,EAAO,CAAA,CAAA,KAAK,CAAA,CAAE,MAAA,CAAO,EAAE,CAAA;AAAA,MACvB,MAAA,EAAQ,CAAA,CAAA,KACN,CAAA,CAAE,MAAA,CAAO;AAAA,QACP,MAAA,EAAQ,EACL,MAAA,CAAO,EAAE,CAAA,CACT,WAAA,EAAY,CACZ,QAAA,CAAS,oDAAoD,CAAA;AAAA,QAChE,QAAA,EAAU,EACP,MAAA,CAAO;AAAA,UACN,aAAA,EAAe,CAAA,CACZ,MAAA,EAAO,CACP,QAAA;AAAA,YACC;AAAA,WACF;AAAA,UACF,mBAAA,EAAqB,EAClB,KAAA,CAAM,CAAA,CAAE,QAAQ,CAAA,CAChB,SAAS,oDAAoD;AAAA,SACjE,CAAA,CACA,QAAA;AAAA,UACC;AAAA;AACF,OACH;AAAA,KACL;AAAA,IACA,MAAA,EAAQ,OAAO,EAAE,WAAA,EAAY,KAAM;AACjC,MAAA,IAAI,CAAC,IAAA,CAAK,WAAA,CAAY,WAAA,EAAa,MAAM,CAAA,EAAG;AAC1C,QAAA,MAAM,IAAIA,uBAAgB,uCAAuC,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,EAAE,aAAA,EAAc,GAAI,WAAA,CAAY,SAAA;AAEtC,MAAA,MAAM,CAAC,MAAA,EAAQ,IAAI,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,QACvC,OAAA,CAAQ,cAAA,CAAe,aAAA,EAAe,EAAE,aAAa,CAAA;AAAA,QACrD,QAAA,CAAS,YAAY,WAAW;AAAA,OACjC,CAAA;AAED,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,MAAM,IAAIC,oBAAA;AAAA,UACR,6CAA6C,aAAa,CAAA,CAAA;AAAA,SAC5D;AAAA,MACF;AAEA,MAAA,OAAO;AAAA,QACL,MAAA,EAAQ;AAAA,UACN,MAAA;AAAA,UACA,QAAA,EAAU;AAAA;AACZ,OACF;AAAA,IACF;AAAA,GACD,CAAA;AACH;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":["../../src/actions/index.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, UserInfoService } from '@backstage/backend-plugin-api';\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { createWhoAmIAction } from './createWhoAmIAction';\n\nexport const createAuthActions = (options: {\n auth: AuthService;\n actionsRegistry: ActionsRegistryService;\n catalog: CatalogService;\n userInfo: UserInfoService;\n}) => {\n createWhoAmIAction(options);\n};\n"],"names":["createWhoAmIAction"],"mappings":";;;;AAoBO,MAAM,iBAAA,GAAoB,CAAC,OAAA,KAK5B;AACJ,EAAAA,qCAAA,CAAmB,OAAO,CAAA;AAC5B;;;;"}
1
+ {"version":3,"file":"index.cjs.js","sources":["../../src/actions/index.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, UserInfoService } from '@backstage/backend-plugin-api';\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { createWhoAmIAction } from './createWhoAmIAction';\n\nexport const createAuthActions = (options: {\n auth: AuthService;\n actionsRegistry: ActionsRegistryService;\n catalog: CatalogService;\n userInfo: UserInfoService;\n}) => {\n createWhoAmIAction(options);\n};\n"],"names":["createWhoAmIAction"],"mappings":";;;;AAoBO,MAAM,iBAAA,GAAoB,CAAC,OAAA,KAK5B;AACJ,EAAAA,qCAAA,CAAmB,OAAO,CAAA;AAC5B;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"authPlugin.cjs.js","sources":["../src/authPlugin.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 {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport {\n authOwnershipResolutionExtensionPoint,\n AuthOwnershipResolver,\n AuthProviderFactory,\n authProvidersExtensionPoint,\n} from '@backstage/plugin-auth-node';\nimport { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport { createAuthActions } from './actions';\nimport { createRouter } from './service/router';\nimport { OfflineAccessService } from './service/OfflineAccessService';\n\n/**\n * Auth plugin\n *\n * @public\n */\nexport const authPlugin = createBackendPlugin({\n pluginId: 'auth',\n register(reg) {\n const providers = new Map<string, AuthProviderFactory>();\n let ownershipResolver: AuthOwnershipResolver | undefined = undefined;\n\n reg.registerExtensionPoint(authProvidersExtensionPoint, {\n registerProvider({ providerId, factory }) {\n if (providers.has(providerId)) {\n throw new Error(\n `Auth provider '${providerId}' was already registered`,\n );\n }\n providers.set(providerId, factory);\n },\n });\n\n reg.registerExtensionPoint(authOwnershipResolutionExtensionPoint, {\n setAuthOwnershipResolver(resolver) {\n if (ownershipResolver) {\n throw new Error('Auth ownership resolver is already set');\n }\n ownershipResolver = resolver;\n },\n });\n\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n database: coreServices.database,\n discovery: coreServices.discovery,\n auth: coreServices.auth,\n httpAuth: coreServices.httpAuth,\n lifecycle: coreServices.lifecycle,\n catalog: catalogServiceRef,\n actionsRegistry: actionsRegistryServiceRef,\n userInfo: coreServices.userInfo,\n },\n async init({\n httpRouter,\n logger,\n config,\n database,\n discovery,\n auth,\n httpAuth,\n lifecycle,\n catalog,\n actionsRegistry,\n userInfo,\n }) {\n const refreshTokensEnabled = config.getOptionalBoolean(\n 'auth.experimentalRefreshToken.enabled',\n );\n\n const offlineAccess = refreshTokensEnabled\n ? await OfflineAccessService.create({\n config,\n database,\n logger,\n lifecycle,\n catalog,\n auth,\n })\n : undefined;\n\n const router = await createRouter({\n logger,\n config,\n database,\n discovery,\n auth,\n catalog,\n providerFactories: Object.fromEntries(providers),\n ownershipResolver,\n httpAuth,\n offlineAccess,\n });\n httpRouter.addAuthPolicy({\n path: '/',\n allow: 'unauthenticated',\n });\n httpRouter.use(router);\n\n createAuthActions({ auth, catalog, userInfo, actionsRegistry });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","authProvidersExtensionPoint","authOwnershipResolutionExtensionPoint","coreServices","catalogServiceRef","actionsRegistryServiceRef","OfflineAccessService","router","createRouter","createAuthActions"],"mappings":";;;;;;;;;;AAqCO,MAAM,aAAaA,oCAAA,CAAoB;AAAA,EAC5C,QAAA,EAAU,MAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,SAAA,uBAAgB,GAAA,EAAiC;AACvD,IAAA,IAAI,iBAAA,GAAuD,MAAA;AAE3D,IAAA,GAAA,CAAI,uBAAuBC,0CAAA,EAA6B;AAAA,MACtD,gBAAA,CAAiB,EAAE,UAAA,EAAY,OAAA,EAAQ,EAAG;AACxC,QAAA,IAAI,SAAA,CAAU,GAAA,CAAI,UAAU,CAAA,EAAG;AAC7B,UAAA,MAAM,IAAI,KAAA;AAAA,YACR,kBAAkB,UAAU,CAAA,wBAAA;AAAA,WAC9B;AAAA,QACF;AACA,QAAA,SAAA,CAAU,GAAA,CAAI,YAAY,OAAO,CAAA;AAAA,MACnC;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,uBAAuBC,oDAAA,EAAuC;AAAA,MAChE,yBAAyB,QAAA,EAAU;AACjC,QAAA,IAAI,iBAAA,EAAmB;AACrB,UAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAAA,QAC1D;AACA,QAAA,iBAAA,GAAoB,QAAA;AAAA,MACtB;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,OAAA,EAASC,mCAAA;AAAA,QACT,eAAA,EAAiBC,+BAAA;AAAA,QACjB,UAAUF,6BAAA,CAAa;AAAA,OACzB;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,UAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,IAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,OAAA;AAAA,QACA,eAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAM,uBAAuB,MAAA,CAAO,kBAAA;AAAA,UAClC;AAAA,SACF;AAEA,QAAA,MAAM,aAAA,GAAgB,oBAAA,GAClB,MAAMG,yCAAA,CAAqB,MAAA,CAAO;AAAA,UAChC,MAAA;AAAA,UACA,QAAA;AAAA,UACA,MAAA;AAAA,UACA,SAAA;AAAA,UACA,OAAA;AAAA,UACA;AAAA,SACD,CAAA,GACD,MAAA;AAEJ,QAAA,MAAMC,QAAA,GAAS,MAAMC,mBAAA,CAAa;AAAA,UAChC,MAAA;AAAA,UACA,MAAA;AAAA,UACA,QAAA;AAAA,UACA,SAAA;AAAA,UACA,IAAA;AAAA,UACA,OAAA;AAAA,UACA,iBAAA,EAAmB,MAAA,CAAO,WAAA,CAAY,SAAS,CAAA;AAAA,UAC/C,iBAAA;AAAA,UACA,QAAA;AAAA,UACA;AAAA,SACD,CAAA;AACD,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,GAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA;AAErB,QAAAE,uBAAA,CAAkB,EAAE,IAAA,EAAM,OAAA,EAAS,QAAA,EAAU,iBAAiB,CAAA;AAAA,MAChE;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"authPlugin.cjs.js","sources":["../src/authPlugin.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 {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport {\n authOwnershipResolutionExtensionPoint,\n AuthOwnershipResolver,\n AuthProviderFactory,\n authProvidersExtensionPoint,\n} from '@backstage/plugin-auth-node';\nimport { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport { createAuthActions } from './actions';\nimport { createRouter } from './service/router';\nimport { OfflineAccessService } from './service/OfflineAccessService';\n\n/**\n * Auth plugin\n *\n * @public\n */\nexport const authPlugin = createBackendPlugin({\n pluginId: 'auth',\n register(reg) {\n const providers = new Map<string, AuthProviderFactory>();\n let ownershipResolver: AuthOwnershipResolver | undefined = undefined;\n\n reg.registerExtensionPoint(authProvidersExtensionPoint, {\n registerProvider({ providerId, factory }) {\n if (providers.has(providerId)) {\n throw new Error(\n `Auth provider '${providerId}' was already registered`,\n );\n }\n providers.set(providerId, factory);\n },\n });\n\n reg.registerExtensionPoint(authOwnershipResolutionExtensionPoint, {\n setAuthOwnershipResolver(resolver) {\n if (ownershipResolver) {\n throw new Error('Auth ownership resolver is already set');\n }\n ownershipResolver = resolver;\n },\n });\n\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n database: coreServices.database,\n discovery: coreServices.discovery,\n auth: coreServices.auth,\n httpAuth: coreServices.httpAuth,\n lifecycle: coreServices.lifecycle,\n catalog: catalogServiceRef,\n actionsRegistry: actionsRegistryServiceRef,\n userInfo: coreServices.userInfo,\n },\n async init({\n httpRouter,\n logger,\n config,\n database,\n discovery,\n auth,\n httpAuth,\n lifecycle,\n catalog,\n actionsRegistry,\n userInfo,\n }) {\n const refreshTokensEnabled = config.getOptionalBoolean(\n 'auth.experimentalRefreshToken.enabled',\n );\n\n const offlineAccess = refreshTokensEnabled\n ? await OfflineAccessService.create({\n config,\n database,\n logger,\n lifecycle,\n catalog,\n auth,\n })\n : undefined;\n\n const router = await createRouter({\n logger,\n config,\n database,\n discovery,\n auth,\n catalog,\n providerFactories: Object.fromEntries(providers),\n ownershipResolver,\n httpAuth,\n offlineAccess,\n });\n httpRouter.addAuthPolicy({\n path: '/',\n allow: 'unauthenticated',\n });\n httpRouter.use(router);\n\n createAuthActions({ auth, catalog, userInfo, actionsRegistry });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","authProvidersExtensionPoint","authOwnershipResolutionExtensionPoint","coreServices","catalogServiceRef","actionsRegistryServiceRef","OfflineAccessService","router","createRouter","createAuthActions"],"mappings":";;;;;;;;;;AAqCO,MAAM,aAAaA,oCAAA,CAAoB;AAAA,EAC5C,QAAA,EAAU,MAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,SAAA,uBAAgB,GAAA,EAAiC;AACvD,IAAA,IAAI,iBAAA,GAAuD,MAAA;AAE3D,IAAA,GAAA,CAAI,uBAAuBC,0CAAA,EAA6B;AAAA,MACtD,gBAAA,CAAiB,EAAE,UAAA,EAAY,OAAA,EAAQ,EAAG;AACxC,QAAA,IAAI,SAAA,CAAU,GAAA,CAAI,UAAU,CAAA,EAAG;AAC7B,UAAA,MAAM,IAAI,KAAA;AAAA,YACR,kBAAkB,UAAU,CAAA,wBAAA;AAAA,WAC9B;AAAA,QACF;AACA,QAAA,SAAA,CAAU,GAAA,CAAI,YAAY,OAAO,CAAA;AAAA,MACnC;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,uBAAuBC,oDAAA,EAAuC;AAAA,MAChE,yBAAyB,QAAA,EAAU;AACjC,QAAA,IAAI,iBAAA,EAAmB;AACrB,UAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAAA,QAC1D;AACA,QAAA,iBAAA,GAAoB,QAAA;AAAA,MACtB;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,OAAA,EAASC,mCAAA;AAAA,QACT,eAAA,EAAiBC,+BAAA;AAAA,QACjB,UAAUF,6BAAA,CAAa;AAAA,OACzB;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,UAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,IAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,OAAA;AAAA,QACA,eAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAM,uBAAuB,MAAA,CAAO,kBAAA;AAAA,UAClC;AAAA,SACF;AAEA,QAAA,MAAM,aAAA,GAAgB,oBAAA,GAClB,MAAMG,yCAAA,CAAqB,MAAA,CAAO;AAAA,UAChC,MAAA;AAAA,UACA,QAAA;AAAA,UACA,MAAA;AAAA,UACA,SAAA;AAAA,UACA,OAAA;AAAA,UACA;AAAA,SACD,CAAA,GACD,MAAA;AAEJ,QAAA,MAAMC,QAAA,GAAS,MAAMC,mBAAA,CAAa;AAAA,UAChC,MAAA;AAAA,UACA,MAAA;AAAA,UACA,QAAA;AAAA,UACA,SAAA;AAAA,UACA,IAAA;AAAA,UACA,OAAA;AAAA,UACA,iBAAA,EAAmB,MAAA,CAAO,WAAA,CAAY,SAAS,CAAA;AAAA,UAC/C,iBAAA;AAAA,UACA,QAAA;AAAA,UACA;AAAA,SACD,CAAA;AACD,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,GAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA;AAErB,QAAAE,uBAAA,CAAkB,EAAE,IAAA,EAAM,OAAA,EAAS,QAAA,EAAU,iBAAiB,CAAA;AAAA,MAChE;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"AuthDatabase.cjs.js","sources":["../../src/database/AuthDatabase.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 {\n DatabaseService,\n resolvePackagePath,\n} from '@backstage/backend-plugin-api';\nimport { Knex } from 'knex';\n\nconst migrationsDir = resolvePackagePath(\n '@backstage/plugin-auth-backend',\n 'migrations',\n);\n\n/**\n * Ensures that a database connection is established exactly once and only when\n * asked for, and runs migrations.\n */\nexport class AuthDatabase {\n readonly #database: DatabaseService;\n #promise: Promise<Knex> | undefined;\n\n static create(database: DatabaseService): AuthDatabase {\n return new AuthDatabase(database);\n }\n\n static async runMigrations(knex: Knex): Promise<void> {\n await knex.migrate.latest({\n directory: migrationsDir,\n });\n }\n\n private constructor(database: DatabaseService) {\n this.#database = database;\n }\n\n get(): Promise<Knex> {\n this.#promise ??= this.#database.getClient().then(async client => {\n if (!this.#database.migrations?.skip) {\n await AuthDatabase.runMigrations(client);\n }\n return client;\n });\n\n return this.#promise;\n }\n}\n"],"names":["resolvePackagePath"],"mappings":";;;;AAsBA,MAAM,aAAA,GAAgBA,mCAAA;AAAA,EACpB,gCAAA;AAAA,EACA;AACF,CAAA;AAMO,MAAM,YAAA,CAAa;AAAA,EACf,SAAA;AAAA,EACT,QAAA;AAAA,EAEA,OAAO,OAAO,QAAA,EAAyC;AACrD,IAAA,OAAO,IAAI,aAAa,QAAQ,CAAA;AAAA,EAClC;AAAA,EAEA,aAAa,cAAc,IAAA,EAA2B;AACpD,IAAA,MAAM,IAAA,CAAK,QAAQ,MAAA,CAAO;AAAA,MACxB,SAAA,EAAW;AAAA,KACZ,CAAA;AAAA,EACH;AAAA,EAEQ,YAAY,QAAA,EAA2B;AAC7C,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,EACnB;AAAA,EAEA,GAAA,GAAqB;AACnB,IAAA,IAAA,CAAK,aAAa,IAAA,CAAK,SAAA,CAAU,WAAU,CAAE,IAAA,CAAK,OAAM,MAAA,KAAU;AAChE,MAAA,IAAI,CAAC,IAAA,CAAK,SAAA,CAAU,UAAA,EAAY,IAAA,EAAM;AACpC,QAAA,MAAM,YAAA,CAAa,cAAc,MAAM,CAAA;AAAA,MACzC;AACA,MAAA,OAAO,MAAA;AAAA,IACT,CAAC,CAAA;AAED,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AACF;;;;"}
1
+ {"version":3,"file":"AuthDatabase.cjs.js","sources":["../../src/database/AuthDatabase.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 {\n DatabaseService,\n resolvePackagePath,\n} from '@backstage/backend-plugin-api';\nimport { Knex } from 'knex';\n\nconst migrationsDir = resolvePackagePath(\n '@backstage/plugin-auth-backend',\n 'migrations',\n);\n\n/**\n * Ensures that a database connection is established exactly once and only when\n * asked for, and runs migrations.\n */\nexport class AuthDatabase {\n readonly #database: DatabaseService;\n #promise: Promise<Knex> | undefined;\n\n static create(database: DatabaseService): AuthDatabase {\n return new AuthDatabase(database);\n }\n\n static async runMigrations(knex: Knex): Promise<void> {\n await knex.migrate.latest({\n directory: migrationsDir,\n });\n }\n\n private constructor(database: DatabaseService) {\n this.#database = database;\n }\n\n get(): Promise<Knex> {\n this.#promise ??= this.#database.getClient().then(async client => {\n if (!this.#database.migrations?.skip) {\n await AuthDatabase.runMigrations(client);\n }\n return client;\n });\n\n return this.#promise;\n }\n}\n"],"names":["resolvePackagePath"],"mappings":";;;;AAsBA,MAAM,aAAA,GAAgBA,mCAAA;AAAA,EACpB,gCAAA;AAAA,EACA;AACF,CAAA;AAMO,MAAM,YAAA,CAAa;AAAA,EACf,SAAA;AAAA,EACT,QAAA;AAAA,EAEA,OAAO,OAAO,QAAA,EAAyC;AACrD,IAAA,OAAO,IAAI,aAAa,QAAQ,CAAA;AAAA,EAClC;AAAA,EAEA,aAAa,cAAc,IAAA,EAA2B;AACpD,IAAA,MAAM,IAAA,CAAK,QAAQ,MAAA,CAAO;AAAA,MACxB,SAAA,EAAW;AAAA,KACZ,CAAA;AAAA,EACH;AAAA,EAEQ,YAAY,QAAA,EAA2B;AAC7C,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,EACnB;AAAA,EAEA,GAAA,GAAqB;AACnB,IAAA,IAAA,CAAK,aAAa,IAAA,CAAK,SAAA,CAAU,WAAU,CAAE,IAAA,CAAK,OAAM,MAAA,KAAU;AAChE,MAAA,IAAI,CAAC,IAAA,CAAK,SAAA,CAAU,UAAA,EAAY,IAAA,EAAM;AACpC,QAAA,MAAM,YAAA,CAAa,cAAc,MAAM,CAAA;AAAA,MACzC;AACA,MAAA,OAAO,MAAA;AAAA,IACT,CAAC,CAAA;AAED,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AACF;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineSessionDatabase.cjs.js","sources":["../../src/database/OfflineSessionDatabase.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 { Knex } from 'knex';\nimport { DateTime } from 'luxon';\n\nconst TABLE_NAME = 'offline_sessions';\n\ntype DbOfflineSessionRow = {\n id: string;\n user_entity_ref: string;\n oidc_client_id: string | null;\n token_hash: string;\n created_at: Date;\n last_used_at: Date;\n};\n\n/**\n * Represents an offline session for refresh tokens\n * @internal\n */\nexport type OfflineSession = {\n id: string;\n userEntityRef: string;\n oidcClientId: string | null;\n tokenHash: string;\n createdAt: Date;\n lastUsedAt: Date;\n};\n\n/**\n * Options for creating a new offline session\n * @internal\n */\nexport type CreateOfflineSessionOptions = {\n id: string;\n userEntityRef: string;\n oidcClientId?: string;\n tokenHash: string;\n};\n\n/**\n * Database layer for managing offline sessions (refresh tokens)\n * @internal\n */\nexport class OfflineSessionDatabase {\n readonly #knex: Knex;\n readonly #tokenLifetimeSeconds: number;\n readonly #maxRotationLifetimeSeconds: number;\n readonly #maxTokensPerUser: number;\n\n static create(options: {\n knex: Knex;\n tokenLifetimeSeconds: number;\n maxRotationLifetimeSeconds: number;\n maxTokensPerUser: number;\n }) {\n return new OfflineSessionDatabase(\n options.knex,\n options.tokenLifetimeSeconds,\n options.maxRotationLifetimeSeconds,\n options.maxTokensPerUser,\n );\n }\n\n private constructor(\n knex: Knex,\n tokenLifetimeSeconds: number,\n maxRotationLifetimeSeconds: number,\n maxTokensPerUser: number,\n ) {\n this.#knex = knex;\n this.#tokenLifetimeSeconds = tokenLifetimeSeconds;\n this.#maxRotationLifetimeSeconds = maxRotationLifetimeSeconds;\n this.#maxTokensPerUser = maxTokensPerUser;\n }\n\n /**\n * Create a new offline session\n * Automatically enforces per-user and per-client limits\n */\n async createSession(\n options: CreateOfflineSessionOptions,\n ): Promise<OfflineSession> {\n const { id, userEntityRef, oidcClientId, tokenHash } = options;\n\n await this.#knex.transaction(async trx => {\n // Delete existing session for same user and OIDC client\n if (oidcClientId) {\n await trx<DbOfflineSessionRow>(TABLE_NAME)\n .where('oidc_client_id', oidcClientId)\n .andWhere('user_entity_ref', userEntityRef)\n .delete();\n }\n\n // Enforce per-user limit\n const userSessions = await trx<DbOfflineSessionRow>(TABLE_NAME)\n .where('user_entity_ref', userEntityRef)\n .select('id', 'last_used_at')\n .orderBy('last_used_at', 'asc')\n .orderBy('id', 'asc');\n\n const tokensToDelete = userSessions.length - (this.#maxTokensPerUser - 1);\n\n if (tokensToDelete > 0) {\n const idsToDelete = userSessions\n .slice(0, tokensToDelete)\n .map(s => s.id);\n\n await trx(TABLE_NAME).whereIn('id', idsToDelete).delete();\n }\n\n // Insert new session\n await trx<DbOfflineSessionRow>(TABLE_NAME).insert({\n id,\n user_entity_ref: userEntityRef,\n oidc_client_id: oidcClientId ?? null,\n token_hash: tokenHash,\n created_at: trx.fn.now(),\n last_used_at: trx.fn.now(),\n });\n });\n\n const session = await this.getSessionById(id);\n if (!session) {\n throw new Error('Failed to create session');\n }\n return session;\n }\n\n /**\n * Get a session by its ID without expiration filtering\n * Used for internal operations that need to check expiration explicitly\n */\n async getSessionById(id: string): Promise<OfflineSession | undefined> {\n const row = await this.#knex<DbOfflineSessionRow>(TABLE_NAME)\n .where('id', id)\n .first();\n\n if (!row) {\n return undefined;\n }\n\n return this.#mapRow(row);\n }\n\n /**\n * Get a session and rotate its token atomically\n * This prevents race conditions where multiple refresh requests use the same token\n */\n async getAndRotateToken(\n id: string,\n expectedTokenHash: string,\n newTokenHash: string,\n ): Promise<OfflineSession | undefined> {\n const now = DateTime.utc();\n const tokenLifetimeThreshold = now\n .minus({ seconds: this.#tokenLifetimeSeconds })\n .toJSDate();\n const maxRotationThreshold = now\n .minus({ seconds: this.#maxRotationLifetimeSeconds })\n .toJSDate();\n\n return await this.#knex.transaction(async trx => {\n // Lock the row and verify token hash matches\n const row = await trx<DbOfflineSessionRow>(TABLE_NAME)\n .where('id', id)\n .where('token_hash', expectedTokenHash)\n .where('last_used_at', '>=', tokenLifetimeThreshold)\n .where('created_at', '>=', maxRotationThreshold)\n .forUpdate()\n .first();\n\n if (!row) {\n return undefined;\n }\n\n // Update token hash atomically\n await trx<DbOfflineSessionRow>(TABLE_NAME).where('id', id).update({\n token_hash: newTokenHash,\n last_used_at: trx.fn.now(),\n });\n\n return this.#mapRow(row);\n });\n }\n\n /**\n * Delete a session by ID\n */\n async deleteSession(id: string): Promise<number> {\n return await this.#knex<DbOfflineSessionRow>(TABLE_NAME)\n .where('id', id)\n .delete();\n }\n\n /**\n * Delete all sessions for a user entity ref\n */\n async deleteSessionsByUserEntityRef(userEntityRef: string): Promise<number> {\n return await this.#knex<DbOfflineSessionRow>(TABLE_NAME)\n .where('user_entity_ref', userEntityRef)\n .delete();\n }\n\n /**\n * Cleanup expired sessions based on both time windows\n * - Short window: last_used_at + tokenLifetime\n * - Long window: created_at + maxRotationLifetime\n */\n async cleanupExpiredSessions(): Promise<number> {\n const now = DateTime.utc();\n const tokenLifetimeThreshold = now\n .minus({ seconds: this.#tokenLifetimeSeconds })\n .toJSDate();\n const maxRotationThreshold = now\n .minus({ seconds: this.#maxRotationLifetimeSeconds })\n .toJSDate();\n\n return await this.#knex<DbOfflineSessionRow>(TABLE_NAME)\n .where('last_used_at', '<', tokenLifetimeThreshold)\n .orWhere('created_at', '<', maxRotationThreshold)\n .delete();\n }\n\n /**\n * Check if a session is expired based on both time windows\n */\n isSessionExpired(session: OfflineSession): boolean {\n const now = DateTime.utc();\n const lastUsedExpiry = DateTime.fromJSDate(session.lastUsedAt).plus({\n seconds: this.#tokenLifetimeSeconds,\n });\n const createdExpiry = DateTime.fromJSDate(session.createdAt).plus({\n seconds: this.#maxRotationLifetimeSeconds,\n });\n\n return now > lastUsedExpiry || now > createdExpiry;\n }\n\n #mapRow(row: DbOfflineSessionRow): OfflineSession {\n return {\n id: row.id,\n userEntityRef: row.user_entity_ref,\n oidcClientId: row.oidc_client_id,\n tokenHash: row.token_hash,\n createdAt: new Date(row.created_at),\n lastUsedAt: new Date(row.last_used_at),\n };\n }\n}\n"],"names":["DateTime"],"mappings":";;;;AAmBA,MAAM,UAAA,GAAa,kBAAA;AAuCZ,MAAM,sBAAA,CAAuB;AAAA,EACzB,KAAA;AAAA,EACA,qBAAA;AAAA,EACA,2BAAA;AAAA,EACA,iBAAA;AAAA,EAET,OAAO,OAAO,OAAA,EAKX;AACD,IAAA,OAAO,IAAI,sBAAA;AAAA,MACT,OAAA,CAAQ,IAAA;AAAA,MACR,OAAA,CAAQ,oBAAA;AAAA,MACR,OAAA,CAAQ,0BAAA;AAAA,MACR,OAAA,CAAQ;AAAA,KACV;AAAA,EACF;AAAA,EAEQ,WAAA,CACN,IAAA,EACA,oBAAA,EACA,0BAAA,EACA,gBAAA,EACA;AACA,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AACb,IAAA,IAAA,CAAK,qBAAA,GAAwB,oBAAA;AAC7B,IAAA,IAAA,CAAK,2BAAA,GAA8B,0BAAA;AACnC,IAAA,IAAA,CAAK,iBAAA,GAAoB,gBAAA;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cACJ,OAAA,EACyB;AACzB,IAAA,MAAM,EAAE,EAAA,EAAI,aAAA,EAAe,YAAA,EAAc,WAAU,GAAI,OAAA;AAEvD,IAAA,MAAM,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,OAAM,GAAA,KAAO;AAExC,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,GAAA,CAAyB,UAAU,CAAA,CACtC,KAAA,CAAM,gBAAA,EAAkB,YAAY,CAAA,CACpC,QAAA,CAAS,iBAAA,EAAmB,aAAa,CAAA,CACzC,MAAA,EAAO;AAAA,MACZ;AAGA,MAAA,MAAM,eAAe,MAAM,GAAA,CAAyB,UAAU,CAAA,CAC3D,KAAA,CAAM,mBAAmB,aAAa,CAAA,CACtC,OAAO,IAAA,EAAM,cAAc,EAC3B,OAAA,CAAQ,cAAA,EAAgB,KAAK,CAAA,CAC7B,OAAA,CAAQ,MAAM,KAAK,CAAA;AAEtB,MAAA,MAAM,cAAA,GAAiB,YAAA,CAAa,MAAA,IAAU,IAAA,CAAK,iBAAA,GAAoB,CAAA,CAAA;AAEvE,MAAA,IAAI,iBAAiB,CAAA,EAAG;AACtB,QAAA,MAAM,WAAA,GAAc,aACjB,KAAA,CAAM,CAAA,EAAG,cAAc,CAAA,CACvB,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,EAAE,CAAA;AAEhB,QAAA,MAAM,IAAI,UAAU,CAAA,CAAE,QAAQ,IAAA,EAAM,WAAW,EAAE,MAAA,EAAO;AAAA,MAC1D;AAGA,MAAA,MAAM,GAAA,CAAyB,UAAU,CAAA,CAAE,MAAA,CAAO;AAAA,QAChD,EAAA;AAAA,QACA,eAAA,EAAiB,aAAA;AAAA,QACjB,gBAAgB,YAAA,IAAgB,IAAA;AAAA,QAChC,UAAA,EAAY,SAAA;AAAA,QACZ,UAAA,EAAY,GAAA,CAAI,EAAA,CAAG,GAAA,EAAI;AAAA,QACvB,YAAA,EAAc,GAAA,CAAI,EAAA,CAAG,GAAA;AAAI,OAC1B,CAAA;AAAA,IACH,CAAC,CAAA;AAED,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,cAAA,CAAe,EAAE,CAAA;AAC5C,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,MAAM,IAAI,MAAM,0BAA0B,CAAA;AAAA,IAC5C;AACA,IAAA,OAAO,OAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAe,EAAA,EAAiD;AACpE,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAA2B,UAAU,EACzD,KAAA,CAAM,IAAA,EAAM,EAAE,CAAA,CACd,KAAA,EAAM;AAET,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,OAAO,IAAA,CAAK,QAAQ,GAAG,CAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,iBAAA,CACJ,EAAA,EACA,iBAAA,EACA,YAAA,EACqC;AACrC,IAAA,MAAM,GAAA,GAAMA,eAAS,GAAA,EAAI;AACzB,IAAA,MAAM,sBAAA,GAAyB,IAC5B,KAAA,CAAM,EAAE,SAAS,IAAA,CAAK,qBAAA,EAAuB,CAAA,CAC7C,QAAA,EAAS;AACZ,IAAA,MAAM,oBAAA,GAAuB,IAC1B,KAAA,CAAM,EAAE,SAAS,IAAA,CAAK,2BAAA,EAA6B,CAAA,CACnD,QAAA,EAAS;AAEZ,IAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,OAAM,GAAA,KAAO;AAE/C,MAAA,MAAM,GAAA,GAAM,MAAM,GAAA,CAAyB,UAAU,CAAA,CAClD,MAAM,IAAA,EAAM,EAAE,CAAA,CACd,KAAA,CAAM,YAAA,EAAc,iBAAiB,EACrC,KAAA,CAAM,cAAA,EAAgB,IAAA,EAAM,sBAAsB,CAAA,CAClD,KAAA,CAAM,YAAA,EAAc,IAAA,EAAM,oBAAoB,CAAA,CAC9C,SAAA,EAAU,CACV,KAAA,EAAM;AAET,MAAA,IAAI,CAAC,GAAA,EAAK;AACR,QAAA,OAAO,MAAA;AAAA,MACT;AAGA,MAAA,MAAM,IAAyB,UAAU,CAAA,CAAE,MAAM,IAAA,EAAM,EAAE,EAAE,MAAA,CAAO;AAAA,QAChE,UAAA,EAAY,YAAA;AAAA,QACZ,YAAA,EAAc,GAAA,CAAI,EAAA,CAAG,GAAA;AAAI,OAC1B,CAAA;AAED,MAAA,OAAO,IAAA,CAAK,QAAQ,GAAG,CAAA;AAAA,IACzB,CAAC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,cAAc,EAAA,EAA6B;AAC/C,IAAA,OAAO,MAAM,KAAK,KAAA,CAA2B,UAAU,EACpD,KAAA,CAAM,IAAA,EAAM,EAAE,CAAA,CACd,MAAA,EAAO;AAAA,EACZ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,8BAA8B,aAAA,EAAwC;AAC1E,IAAA,OAAO,MAAM,KAAK,KAAA,CAA2B,UAAU,EACpD,KAAA,CAAM,iBAAA,EAAmB,aAAa,CAAA,CACtC,MAAA,EAAO;AAAA,EACZ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,sBAAA,GAA0C;AAC9C,IAAA,MAAM,GAAA,GAAMA,eAAS,GAAA,EAAI;AACzB,IAAA,MAAM,sBAAA,GAAyB,IAC5B,KAAA,CAAM,EAAE,SAAS,IAAA,CAAK,qBAAA,EAAuB,CAAA,CAC7C,QAAA,EAAS;AACZ,IAAA,MAAM,oBAAA,GAAuB,IAC1B,KAAA,CAAM,EAAE,SAAS,IAAA,CAAK,2BAAA,EAA6B,CAAA,CACnD,QAAA,EAAS;AAEZ,IAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAA2B,UAAU,EACpD,KAAA,CAAM,cAAA,EAAgB,GAAA,EAAK,sBAAsB,EACjD,OAAA,CAAQ,YAAA,EAAc,GAAA,EAAK,oBAAoB,EAC/C,MAAA,EAAO;AAAA,EACZ;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,OAAA,EAAkC;AACjD,IAAA,MAAM,GAAA,GAAMA,eAAS,GAAA,EAAI;AACzB,IAAA,MAAM,iBAAiBA,cAAA,CAAS,UAAA,CAAW,OAAA,CAAQ,UAAU,EAAE,IAAA,CAAK;AAAA,MAClE,SAAS,IAAA,CAAK;AAAA,KACf,CAAA;AACD,IAAA,MAAM,gBAAgBA,cAAA,CAAS,UAAA,CAAW,OAAA,CAAQ,SAAS,EAAE,IAAA,CAAK;AAAA,MAChE,SAAS,IAAA,CAAK;AAAA,KACf,CAAA;AAED,IAAA,OAAO,GAAA,GAAM,kBAAkB,GAAA,GAAM,aAAA;AAAA,EACvC;AAAA,EAEA,QAAQ,GAAA,EAA0C;AAChD,IAAA,OAAO;AAAA,MACL,IAAI,GAAA,CAAI,EAAA;AAAA,MACR,eAAe,GAAA,CAAI,eAAA;AAAA,MACnB,cAAc,GAAA,CAAI,cAAA;AAAA,MAClB,WAAW,GAAA,CAAI,UAAA;AAAA,MACf,SAAA,EAAW,IAAI,IAAA,CAAK,GAAA,CAAI,UAAU,CAAA;AAAA,MAClC,UAAA,EAAY,IAAI,IAAA,CAAK,GAAA,CAAI,YAAY;AAAA,KACvC;AAAA,EACF;AACF;;;;"}
1
+ {"version":3,"file":"OfflineSessionDatabase.cjs.js","sources":["../../src/database/OfflineSessionDatabase.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 { Knex } from 'knex';\nimport { DateTime } from 'luxon';\n\nconst TABLE_NAME = 'offline_sessions';\n\ntype DbOfflineSessionRow = {\n id: string;\n user_entity_ref: string;\n oidc_client_id: string | null;\n token_hash: string;\n created_at: Date;\n last_used_at: Date;\n};\n\n/**\n * Represents an offline session for refresh tokens\n * @internal\n */\nexport type OfflineSession = {\n id: string;\n userEntityRef: string;\n oidcClientId: string | null;\n tokenHash: string;\n createdAt: Date;\n lastUsedAt: Date;\n};\n\n/**\n * Options for creating a new offline session\n * @internal\n */\nexport type CreateOfflineSessionOptions = {\n id: string;\n userEntityRef: string;\n oidcClientId?: string;\n tokenHash: string;\n};\n\n/**\n * Database layer for managing offline sessions (refresh tokens)\n * @internal\n */\nexport class OfflineSessionDatabase {\n readonly #knex: Knex;\n readonly #tokenLifetimeSeconds: number;\n readonly #maxRotationLifetimeSeconds: number;\n readonly #maxTokensPerUser: number;\n\n static create(options: {\n knex: Knex;\n tokenLifetimeSeconds: number;\n maxRotationLifetimeSeconds: number;\n maxTokensPerUser: number;\n }) {\n return new OfflineSessionDatabase(\n options.knex,\n options.tokenLifetimeSeconds,\n options.maxRotationLifetimeSeconds,\n options.maxTokensPerUser,\n );\n }\n\n private constructor(\n knex: Knex,\n tokenLifetimeSeconds: number,\n maxRotationLifetimeSeconds: number,\n maxTokensPerUser: number,\n ) {\n this.#knex = knex;\n this.#tokenLifetimeSeconds = tokenLifetimeSeconds;\n this.#maxRotationLifetimeSeconds = maxRotationLifetimeSeconds;\n this.#maxTokensPerUser = maxTokensPerUser;\n }\n\n /**\n * Create a new offline session\n * Automatically enforces per-user and per-client limits\n */\n async createSession(\n options: CreateOfflineSessionOptions,\n ): Promise<OfflineSession> {\n const { id, userEntityRef, oidcClientId, tokenHash } = options;\n\n await this.#knex.transaction(async trx => {\n // Delete existing session for same user and OIDC client\n if (oidcClientId) {\n await trx<DbOfflineSessionRow>(TABLE_NAME)\n .where('oidc_client_id', oidcClientId)\n .andWhere('user_entity_ref', userEntityRef)\n .delete();\n }\n\n // Enforce per-user limit\n const userSessions = await trx<DbOfflineSessionRow>(TABLE_NAME)\n .where('user_entity_ref', userEntityRef)\n .select('id', 'last_used_at')\n .orderBy('last_used_at', 'asc')\n .orderBy('id', 'asc');\n\n const tokensToDelete = userSessions.length - (this.#maxTokensPerUser - 1);\n\n if (tokensToDelete > 0) {\n const idsToDelete = userSessions\n .slice(0, tokensToDelete)\n .map(s => s.id);\n\n await trx(TABLE_NAME).whereIn('id', idsToDelete).delete();\n }\n\n // Insert new session\n await trx<DbOfflineSessionRow>(TABLE_NAME).insert({\n id,\n user_entity_ref: userEntityRef,\n oidc_client_id: oidcClientId ?? null,\n token_hash: tokenHash,\n created_at: trx.fn.now(),\n last_used_at: trx.fn.now(),\n });\n });\n\n const session = await this.getSessionById(id);\n if (!session) {\n throw new Error('Failed to create session');\n }\n return session;\n }\n\n /**\n * Get a session by its ID without expiration filtering\n * Used for internal operations that need to check expiration explicitly\n */\n async getSessionById(id: string): Promise<OfflineSession | undefined> {\n const row = await this.#knex<DbOfflineSessionRow>(TABLE_NAME)\n .where('id', id)\n .first();\n\n if (!row) {\n return undefined;\n }\n\n return this.#mapRow(row);\n }\n\n /**\n * Get a session and rotate its token atomically\n * This prevents race conditions where multiple refresh requests use the same token\n */\n async getAndRotateToken(\n id: string,\n expectedTokenHash: string,\n newTokenHash: string,\n ): Promise<OfflineSession | undefined> {\n const now = DateTime.utc();\n const tokenLifetimeThreshold = now\n .minus({ seconds: this.#tokenLifetimeSeconds })\n .toJSDate();\n const maxRotationThreshold = now\n .minus({ seconds: this.#maxRotationLifetimeSeconds })\n .toJSDate();\n\n return await this.#knex.transaction(async trx => {\n // Lock the row and verify token hash matches\n const row = await trx<DbOfflineSessionRow>(TABLE_NAME)\n .where('id', id)\n .where('token_hash', expectedTokenHash)\n .where('last_used_at', '>=', tokenLifetimeThreshold)\n .where('created_at', '>=', maxRotationThreshold)\n .forUpdate()\n .first();\n\n if (!row) {\n return undefined;\n }\n\n // Update token hash atomically\n await trx<DbOfflineSessionRow>(TABLE_NAME).where('id', id).update({\n token_hash: newTokenHash,\n last_used_at: trx.fn.now(),\n });\n\n return this.#mapRow(row);\n });\n }\n\n /**\n * Delete a session by ID\n */\n async deleteSession(id: string): Promise<number> {\n return await this.#knex<DbOfflineSessionRow>(TABLE_NAME)\n .where('id', id)\n .delete();\n }\n\n /**\n * Delete all sessions for a user entity ref\n */\n async deleteSessionsByUserEntityRef(userEntityRef: string): Promise<number> {\n return await this.#knex<DbOfflineSessionRow>(TABLE_NAME)\n .where('user_entity_ref', userEntityRef)\n .delete();\n }\n\n /**\n * Cleanup expired sessions based on both time windows\n * - Short window: last_used_at + tokenLifetime\n * - Long window: created_at + maxRotationLifetime\n */\n async cleanupExpiredSessions(): Promise<number> {\n const now = DateTime.utc();\n const tokenLifetimeThreshold = now\n .minus({ seconds: this.#tokenLifetimeSeconds })\n .toJSDate();\n const maxRotationThreshold = now\n .minus({ seconds: this.#maxRotationLifetimeSeconds })\n .toJSDate();\n\n return await this.#knex<DbOfflineSessionRow>(TABLE_NAME)\n .where('last_used_at', '<', tokenLifetimeThreshold)\n .orWhere('created_at', '<', maxRotationThreshold)\n .delete();\n }\n\n /**\n * Check if a session is expired based on both time windows\n */\n isSessionExpired(session: OfflineSession): boolean {\n const now = DateTime.utc();\n const lastUsedExpiry = DateTime.fromJSDate(session.lastUsedAt).plus({\n seconds: this.#tokenLifetimeSeconds,\n });\n const createdExpiry = DateTime.fromJSDate(session.createdAt).plus({\n seconds: this.#maxRotationLifetimeSeconds,\n });\n\n return now > lastUsedExpiry || now > createdExpiry;\n }\n\n #mapRow(row: DbOfflineSessionRow): OfflineSession {\n return {\n id: row.id,\n userEntityRef: row.user_entity_ref,\n oidcClientId: row.oidc_client_id,\n tokenHash: row.token_hash,\n createdAt: new Date(row.created_at),\n lastUsedAt: new Date(row.last_used_at),\n };\n }\n}\n"],"names":["DateTime"],"mappings":"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"}
@@ -10,6 +10,7 @@ class OidcDatabase {
10
10
  constructor(db) {
11
11
  this.db = db;
12
12
  }
13
+ db;
13
14
  static async create(options) {
14
15
  const client = await options.database.get();
15
16
  return new OidcDatabase(client);
@@ -1 +1 @@
1
- {"version":3,"file":"OidcDatabase.cjs.js","sources":["../../src/database/OidcDatabase.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 { Knex } from 'knex';\nimport { AuthDatabase } from './AuthDatabase';\n\nfunction toDate(value?: Date | string | number): Date | undefined {\n if (!value) {\n return undefined;\n }\n\n return typeof value === 'string' || typeof value === 'number'\n ? new Date(value)\n : value;\n}\ntype OidcClientRow = {\n client_id: string;\n client_secret: string;\n client_name: string;\n response_types: string;\n grant_types: string;\n redirect_uris: string;\n scope: string | null;\n metadata: string | null;\n};\n\ntype OAuthAuthorizationSessionRow = {\n id: string;\n client_id: string;\n user_entity_ref: string | null;\n redirect_uri: string;\n scope: string | null;\n state: string | null;\n response_type: string;\n code_challenge: string | null;\n code_challenge_method: string | null;\n nonce: string | null;\n status: 'pending' | 'approved' | 'rejected' | 'expired';\n expires_at: Date | string;\n};\n\ntype OidcAuthorizationCodeRow = {\n code: string;\n session_id: string;\n expires_at: Date | string;\n used: boolean;\n};\n\nexport type Client = {\n clientId: string;\n clientName: string;\n clientSecret: string;\n redirectUris: string[];\n responseTypes: string[];\n grantTypes: string[];\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\nexport type AuthorizationSession = {\n id: string;\n clientId: string;\n userEntityRef?: string;\n redirectUri: string;\n scope?: string;\n state?: string;\n responseType: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n nonce?: string;\n status: 'pending' | 'approved' | 'rejected' | 'expired';\n expiresAt: Date;\n};\n\nexport type ConsentRequest = {\n id: string;\n sessionId: string;\n expiresAt: Date;\n};\n\nexport type AuthorizationCode = {\n code: string;\n sessionId: string;\n expiresAt: Date;\n used: boolean;\n};\n\nexport type AccessToken = {\n tokenId: string;\n sessionId: string;\n expiresAt: Date;\n};\n\n/**\n * This class provides database operations for OpenID Connect (OIDC) authentication flows.\n * It manages OIDC clients, authorization codes, and access tokens in the database.\n */\nexport class OidcDatabase {\n private constructor(private readonly db: Knex) {}\n\n static async create(options: { database: AuthDatabase }) {\n const client = await options.database.get();\n return new OidcDatabase(client);\n }\n\n async createClient(client: Client) {\n await this.db<OidcClientRow>('oidc_clients').insert({\n client_id: client.clientId,\n client_secret: client.clientSecret,\n client_name: client.clientName,\n response_types: JSON.stringify(client.responseTypes),\n grant_types: JSON.stringify(client.grantTypes),\n redirect_uris: JSON.stringify(client.redirectUris),\n scope: client.scope,\n metadata: JSON.stringify(client.metadata),\n });\n\n return client;\n }\n\n async getClient({ clientId }: { clientId: string }) {\n const client = await this.db<OidcClientRow>('oidc_clients')\n .where('client_id', clientId)\n .first();\n\n if (!client) {\n return null;\n }\n\n return this.rowToClient(client) as Client;\n }\n\n async createAuthorizationSession(\n session: Omit<AuthorizationSession, 'status'>,\n ) {\n await this.db<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n ).insert({\n id: session.id,\n client_id: session.clientId,\n user_entity_ref: session.userEntityRef,\n redirect_uri: session.redirectUri,\n scope: session.scope,\n state: session.state,\n response_type: session.responseType,\n code_challenge: session.codeChallenge,\n code_challenge_method: session.codeChallengeMethod,\n nonce: session.nonce,\n status: 'pending',\n expires_at: session.expiresAt,\n });\n\n return {\n ...session,\n status: 'pending',\n };\n }\n\n async updateAuthorizationSession(\n session: Partial<AuthorizationSession> & { id: string },\n ) {\n const row = this.authorizationSessionToRow(session);\n const updatedFields = Object.fromEntries(\n Object.entries(row).filter(([_, value]) => value !== undefined),\n );\n\n // MySQL and SQLite3 don't support RETURNING\n if (\n this.db.client.config.client.includes('sqlite3') ||\n this.db.client.config.client.includes('mysql')\n ) {\n return await this.db.transaction(async trx => {\n await trx<OAuthAuthorizationSessionRow>('oauth_authorization_sessions')\n .where('id', session.id)\n .update(updatedFields);\n\n const updated = await trx<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n )\n .where('id', session.id)\n .first();\n\n if (!updated) {\n throw new Error(\n `Failed to retrieve updated authorization session with id ${session.id}`,\n );\n }\n\n return this.rowToAuthorizationSession(updated) as AuthorizationSession;\n });\n }\n\n const returnedRows = await this.db<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n )\n .where('id', session.id)\n .update(updatedFields)\n .returning('*');\n\n if (returnedRows.length !== 1) {\n throw new Error(\n `Failed to retrieve updated authorization session with id ${session.id}`,\n );\n }\n\n const [returnedSession] = returnedRows;\n\n return this.rowToAuthorizationSession(\n returnedSession,\n ) as AuthorizationSession;\n }\n\n async getAuthorizationSession({ id }: { id: string }) {\n const session = await this.db<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n )\n .where('id', id)\n .first();\n\n if (!session) {\n return null;\n }\n\n return this.rowToAuthorizationSession(session) as AuthorizationSession;\n }\n\n async createAuthorizationCode(\n authorizationCode: Omit<AuthorizationCode, 'used'>,\n ) {\n await this.db<OidcAuthorizationCodeRow>('oidc_authorization_codes').insert({\n code: authorizationCode.code,\n session_id: authorizationCode.sessionId,\n expires_at: authorizationCode.expiresAt,\n used: false,\n });\n\n return {\n ...authorizationCode,\n used: false,\n };\n }\n\n async getAuthorizationCode({ code }: { code: string }) {\n const authCode = await this.db<OidcAuthorizationCodeRow>(\n 'oidc_authorization_codes',\n )\n .where('code', code)\n .first();\n\n if (!authCode) {\n return null;\n }\n\n return this.rowToAuthorizationCode(authCode) as AuthorizationCode;\n }\n\n async updateAuthorizationCode(\n authorizationCode: Partial<AuthorizationCode> & { code: string },\n ) {\n const row = this.authorizationCodeToRow(authorizationCode);\n const updatedFields = Object.fromEntries(\n Object.entries(row).filter(([_, value]) => value !== undefined),\n );\n\n // MySQL and SQLite3 don't support RETURNING\n if (\n this.db.client.config.client.includes('sqlite3') ||\n this.db.client.config.client.includes('mysql')\n ) {\n return await this.db.transaction(async trx => {\n await trx<OidcAuthorizationCodeRow>('oidc_authorization_codes')\n .where('code', authorizationCode.code)\n .update(updatedFields);\n\n const updated = await trx<OidcAuthorizationCodeRow>(\n 'oidc_authorization_codes',\n )\n .where('code', authorizationCode.code)\n .first();\n\n if (!updated) {\n throw new Error(\n `Failed to retrieve updated authorization code with code ${authorizationCode.code}`,\n );\n }\n\n return this.rowToAuthorizationCode(updated) as AuthorizationCode;\n });\n }\n\n const returnedRows = await this.db<OidcAuthorizationCodeRow>(\n 'oidc_authorization_codes',\n )\n .where('code', authorizationCode.code)\n .update(updatedFields)\n .returning('*');\n\n if (returnedRows.length !== 1) {\n throw new Error(\n `Failed to retrieve updated authorization code with code ${authorizationCode.code}`,\n );\n }\n\n const [returnedCode] = returnedRows;\n\n return this.rowToAuthorizationCode(returnedCode) as AuthorizationCode;\n }\n\n private rowToClient(row: OidcClientRow): Client {\n return {\n clientId: row.client_id,\n clientName: row.client_name,\n clientSecret: row.client_secret,\n redirectUris: row.redirect_uris\n ? JSON.parse(row.redirect_uris)\n : undefined,\n responseTypes: row.response_types\n ? JSON.parse(row.response_types)\n : undefined,\n grantTypes: row.grant_types ? JSON.parse(row.grant_types) : undefined,\n scope: row.scope ?? undefined,\n metadata: row.metadata ? JSON.parse(row.metadata) : undefined,\n };\n }\n\n private authorizationSessionToRow(\n session: Partial<AuthorizationSession>,\n ): Partial<OAuthAuthorizationSessionRow> {\n return {\n id: session.id,\n client_id: session.clientId,\n user_entity_ref: session.userEntityRef,\n redirect_uri: session.redirectUri,\n scope: session.scope,\n state: session.state,\n response_type: session.responseType,\n code_challenge: session.codeChallenge,\n code_challenge_method: session.codeChallengeMethod,\n nonce: session.nonce,\n status: session.status,\n expires_at: toDate(session.expiresAt),\n };\n }\n\n private rowToAuthorizationSession(\n row: OAuthAuthorizationSessionRow,\n ): Partial<AuthorizationSession> {\n return {\n id: row.id,\n clientId: row.client_id,\n userEntityRef: row.user_entity_ref ?? undefined,\n redirectUri: row.redirect_uri,\n scope: row.scope ?? undefined,\n state: row.state ?? undefined,\n responseType: row.response_type,\n codeChallenge: row.code_challenge ?? undefined,\n codeChallengeMethod: row.code_challenge_method ?? undefined,\n nonce: row.nonce ?? undefined,\n status: row.status,\n expiresAt: toDate(row.expires_at),\n };\n }\n\n private authorizationCodeToRow(\n authorizationCode: Partial<AuthorizationCode>,\n ): Partial<OidcAuthorizationCodeRow> {\n return {\n code: authorizationCode.code,\n session_id: authorizationCode.sessionId,\n expires_at: toDate(authorizationCode.expiresAt),\n used: authorizationCode.used,\n };\n }\n\n private rowToAuthorizationCode(\n row: OidcAuthorizationCodeRow,\n ): Partial<AuthorizationCode> {\n return {\n code: row.code,\n sessionId: row.session_id,\n expiresAt: toDate(row.expires_at),\n used: Boolean(row.used),\n };\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"OidcDatabase.cjs.js","sources":["../../src/database/OidcDatabase.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 { Knex } from 'knex';\nimport { AuthDatabase } from './AuthDatabase';\n\nfunction toDate(value?: Date | string | number): Date | undefined {\n if (!value) {\n return undefined;\n }\n\n return typeof value === 'string' || typeof value === 'number'\n ? new Date(value)\n : value;\n}\ntype OidcClientRow = {\n client_id: string;\n client_secret: string;\n client_name: string;\n response_types: string;\n grant_types: string;\n redirect_uris: string;\n scope: string | null;\n metadata: string | null;\n};\n\ntype OAuthAuthorizationSessionRow = {\n id: string;\n client_id: string;\n user_entity_ref: string | null;\n redirect_uri: string;\n scope: string | null;\n state: string | null;\n response_type: string;\n code_challenge: string | null;\n code_challenge_method: string | null;\n nonce: string | null;\n status: 'pending' | 'approved' | 'rejected' | 'expired';\n expires_at: Date | string;\n};\n\ntype OidcAuthorizationCodeRow = {\n code: string;\n session_id: string;\n expires_at: Date | string;\n used: boolean;\n};\n\nexport type Client = {\n clientId: string;\n clientName: string;\n clientSecret: string;\n redirectUris: string[];\n responseTypes: string[];\n grantTypes: string[];\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\nexport type AuthorizationSession = {\n id: string;\n clientId: string;\n userEntityRef?: string;\n redirectUri: string;\n scope?: string;\n state?: string;\n responseType: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n nonce?: string;\n status: 'pending' | 'approved' | 'rejected' | 'expired';\n expiresAt: Date;\n};\n\nexport type ConsentRequest = {\n id: string;\n sessionId: string;\n expiresAt: Date;\n};\n\nexport type AuthorizationCode = {\n code: string;\n sessionId: string;\n expiresAt: Date;\n used: boolean;\n};\n\nexport type AccessToken = {\n tokenId: string;\n sessionId: string;\n expiresAt: Date;\n};\n\n/**\n * This class provides database operations for OpenID Connect (OIDC) authentication flows.\n * It manages OIDC clients, authorization codes, and access tokens in the database.\n */\nexport class OidcDatabase {\n private constructor(private readonly db: Knex) {}\n\n static async create(options: { database: AuthDatabase }) {\n const client = await options.database.get();\n return new OidcDatabase(client);\n }\n\n async createClient(client: Client) {\n await this.db<OidcClientRow>('oidc_clients').insert({\n client_id: client.clientId,\n client_secret: client.clientSecret,\n client_name: client.clientName,\n response_types: JSON.stringify(client.responseTypes),\n grant_types: JSON.stringify(client.grantTypes),\n redirect_uris: JSON.stringify(client.redirectUris),\n scope: client.scope,\n metadata: JSON.stringify(client.metadata),\n });\n\n return client;\n }\n\n async getClient({ clientId }: { clientId: string }) {\n const client = await this.db<OidcClientRow>('oidc_clients')\n .where('client_id', clientId)\n .first();\n\n if (!client) {\n return null;\n }\n\n return this.rowToClient(client) as Client;\n }\n\n async createAuthorizationSession(\n session: Omit<AuthorizationSession, 'status'>,\n ) {\n await this.db<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n ).insert({\n id: session.id,\n client_id: session.clientId,\n user_entity_ref: session.userEntityRef,\n redirect_uri: session.redirectUri,\n scope: session.scope,\n state: session.state,\n response_type: session.responseType,\n code_challenge: session.codeChallenge,\n code_challenge_method: session.codeChallengeMethod,\n nonce: session.nonce,\n status: 'pending',\n expires_at: session.expiresAt,\n });\n\n return {\n ...session,\n status: 'pending',\n };\n }\n\n async updateAuthorizationSession(\n session: Partial<AuthorizationSession> & { id: string },\n ) {\n const row = this.authorizationSessionToRow(session);\n const updatedFields = Object.fromEntries(\n Object.entries(row).filter(([_, value]) => value !== undefined),\n );\n\n // MySQL and SQLite3 don't support RETURNING\n if (\n this.db.client.config.client.includes('sqlite3') ||\n this.db.client.config.client.includes('mysql')\n ) {\n return await this.db.transaction(async trx => {\n await trx<OAuthAuthorizationSessionRow>('oauth_authorization_sessions')\n .where('id', session.id)\n .update(updatedFields);\n\n const updated = await trx<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n )\n .where('id', session.id)\n .first();\n\n if (!updated) {\n throw new Error(\n `Failed to retrieve updated authorization session with id ${session.id}`,\n );\n }\n\n return this.rowToAuthorizationSession(updated) as AuthorizationSession;\n });\n }\n\n const returnedRows = await this.db<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n )\n .where('id', session.id)\n .update(updatedFields)\n .returning('*');\n\n if (returnedRows.length !== 1) {\n throw new Error(\n `Failed to retrieve updated authorization session with id ${session.id}`,\n );\n }\n\n const [returnedSession] = returnedRows;\n\n return this.rowToAuthorizationSession(\n returnedSession,\n ) as AuthorizationSession;\n }\n\n async getAuthorizationSession({ id }: { id: string }) {\n const session = await this.db<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n )\n .where('id', id)\n .first();\n\n if (!session) {\n return null;\n }\n\n return this.rowToAuthorizationSession(session) as AuthorizationSession;\n }\n\n async createAuthorizationCode(\n authorizationCode: Omit<AuthorizationCode, 'used'>,\n ) {\n await this.db<OidcAuthorizationCodeRow>('oidc_authorization_codes').insert({\n code: authorizationCode.code,\n session_id: authorizationCode.sessionId,\n expires_at: authorizationCode.expiresAt,\n used: false,\n });\n\n return {\n ...authorizationCode,\n used: false,\n };\n }\n\n async getAuthorizationCode({ code }: { code: string }) {\n const authCode = await this.db<OidcAuthorizationCodeRow>(\n 'oidc_authorization_codes',\n )\n .where('code', code)\n .first();\n\n if (!authCode) {\n return null;\n }\n\n return this.rowToAuthorizationCode(authCode) as AuthorizationCode;\n }\n\n async updateAuthorizationCode(\n authorizationCode: Partial<AuthorizationCode> & { code: string },\n ) {\n const row = this.authorizationCodeToRow(authorizationCode);\n const updatedFields = Object.fromEntries(\n Object.entries(row).filter(([_, value]) => value !== undefined),\n );\n\n // MySQL and SQLite3 don't support RETURNING\n if (\n this.db.client.config.client.includes('sqlite3') ||\n this.db.client.config.client.includes('mysql')\n ) {\n return await this.db.transaction(async trx => {\n await trx<OidcAuthorizationCodeRow>('oidc_authorization_codes')\n .where('code', authorizationCode.code)\n .update(updatedFields);\n\n const updated = await trx<OidcAuthorizationCodeRow>(\n 'oidc_authorization_codes',\n )\n .where('code', authorizationCode.code)\n .first();\n\n if (!updated) {\n throw new Error(\n `Failed to retrieve updated authorization code with code ${authorizationCode.code}`,\n );\n }\n\n return this.rowToAuthorizationCode(updated) as AuthorizationCode;\n });\n }\n\n const returnedRows = await this.db<OidcAuthorizationCodeRow>(\n 'oidc_authorization_codes',\n )\n .where('code', authorizationCode.code)\n .update(updatedFields)\n .returning('*');\n\n if (returnedRows.length !== 1) {\n throw new Error(\n `Failed to retrieve updated authorization code with code ${authorizationCode.code}`,\n );\n }\n\n const [returnedCode] = returnedRows;\n\n return this.rowToAuthorizationCode(returnedCode) as AuthorizationCode;\n }\n\n private rowToClient(row: OidcClientRow): Client {\n return {\n clientId: row.client_id,\n clientName: row.client_name,\n clientSecret: row.client_secret,\n redirectUris: row.redirect_uris\n ? JSON.parse(row.redirect_uris)\n : undefined,\n responseTypes: row.response_types\n ? JSON.parse(row.response_types)\n : undefined,\n grantTypes: row.grant_types ? JSON.parse(row.grant_types) : undefined,\n scope: row.scope ?? undefined,\n metadata: row.metadata ? JSON.parse(row.metadata) : undefined,\n };\n }\n\n private authorizationSessionToRow(\n session: Partial<AuthorizationSession>,\n ): Partial<OAuthAuthorizationSessionRow> {\n return {\n id: session.id,\n client_id: session.clientId,\n user_entity_ref: session.userEntityRef,\n redirect_uri: session.redirectUri,\n scope: session.scope,\n state: session.state,\n response_type: session.responseType,\n code_challenge: session.codeChallenge,\n code_challenge_method: session.codeChallengeMethod,\n nonce: session.nonce,\n status: session.status,\n expires_at: toDate(session.expiresAt),\n };\n }\n\n private rowToAuthorizationSession(\n row: OAuthAuthorizationSessionRow,\n ): Partial<AuthorizationSession> {\n return {\n id: row.id,\n clientId: row.client_id,\n userEntityRef: row.user_entity_ref ?? undefined,\n redirectUri: row.redirect_uri,\n scope: row.scope ?? undefined,\n state: row.state ?? undefined,\n responseType: row.response_type,\n codeChallenge: row.code_challenge ?? undefined,\n codeChallengeMethod: row.code_challenge_method ?? undefined,\n nonce: row.nonce ?? undefined,\n status: row.status,\n expiresAt: toDate(row.expires_at),\n };\n }\n\n private authorizationCodeToRow(\n authorizationCode: Partial<AuthorizationCode>,\n ): Partial<OidcAuthorizationCodeRow> {\n return {\n code: authorizationCode.code,\n session_id: authorizationCode.sessionId,\n expires_at: toDate(authorizationCode.expiresAt),\n used: authorizationCode.used,\n };\n }\n\n private rowToAuthorizationCode(\n row: OidcAuthorizationCodeRow,\n ): Partial<AuthorizationCode> {\n return {\n code: row.code,\n sessionId: row.session_id,\n expiresAt: toDate(row.expires_at),\n used: Boolean(row.used),\n };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ class UserInfoDatabase {
7
7
  constructor(client) {
8
8
  this.client = client;
9
9
  }
10
+ client;
10
11
  async addUserInfo(userInfo) {
11
12
  await this.client(TABLE).insert({
12
13
  user_entity_ref: userInfo.claims.sub,
@@ -1 +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,KAAA,GAAQ,WAAA;AAYP,MAAM,gBAAA,CAAiB;AAAA,EACpB,YAA6B,MAAA,EAAc;AAAd,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAAe;AAAA,EAEpD,MAAM,YAAY,QAAA,EAAmC;AACnD,IAAA,MAAM,IAAA,CAAK,MAAA,CAAY,KAAK,CAAA,CACzB,MAAA,CAAO;AAAA,MACN,eAAA,EAAiB,SAAS,MAAA,CAAO,GAAA;AAAA,MACjC,SAAA,EAAW,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAA;AAAA,MAClC,UAAA,EAAYA,eAAS,GAAA,EAAI,CAAE,MAAM,EAAE,aAAA,EAAe,OAAO;AAAA,KAC1D,CAAA,CACA,UAAA,CAAW,iBAAiB,EAC5B,KAAA,EAAM;AAAA,EACX;AAAA,EAEA,MAAM,YAAY,aAAA,EAAsD;AACtE,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,MAAA,CAAY,KAAK,CAAA,CACtC,KAAA,CAAM,EAAE,eAAA,EAAiB,aAAA,EAAe,CAAA,CACxC,KAAA,EAAM;AAET,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,SAAS,CAAA;AAC1C,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,aAAa,OAAO,OAAA,EAAqC;AACvD,IAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,QAAA,CAAS,GAAA,EAAI;AAC1C,IAAA,OAAO,IAAI,iBAAiB,MAAM,CAAA;AAAA,EACpC;AACF;;;;"}
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,KAAA,GAAQ,WAAA;AAYP,MAAM,gBAAA,CAAiB;AAAA,EACpB,YAA6B,MAAA,EAAc;AAAd,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAAe;AAAA,EAAf,MAAA;AAAA,EAErC,MAAM,YAAY,QAAA,EAAmC;AACnD,IAAA,MAAM,IAAA,CAAK,MAAA,CAAY,KAAK,CAAA,CACzB,MAAA,CAAO;AAAA,MACN,eAAA,EAAiB,SAAS,MAAA,CAAO,GAAA;AAAA,MACjC,SAAA,EAAW,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAA;AAAA,MAClC,UAAA,EAAYA,eAAS,GAAA,EAAI,CAAE,MAAM,EAAE,aAAA,EAAe,OAAO;AAAA,KAC1D,CAAA,CACA,UAAA,CAAW,iBAAiB,EAC5B,KAAA,EAAM;AAAA,EACX;AAAA,EAEA,MAAM,YAAY,aAAA,EAAsD;AACtE,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,MAAA,CAAY,KAAK,CAAA,CACtC,KAAA,CAAM,EAAE,eAAA,EAAiB,aAAA,EAAe,CAAA,CACxC,KAAA,EAAM;AAET,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,SAAS,CAAA;AAC1C,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,aAAa,OAAO,OAAA,EAAqC;AACvD,IAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,QAAA,CAAS,GAAA,EAAI;AAC1C,IAAA,OAAO,IAAI,iBAAiB,MAAM,CAAA;AAAA,EACpC;AACF;;"}
@@ -16,6 +16,7 @@ class DatabaseKeyStore {
16
16
  constructor(client) {
17
17
  this.client = client;
18
18
  }
19
+ client;
19
20
  async addKey(key) {
20
21
  await this.client(TABLE).insert({
21
22
  kid: key.kid,
@@ -1 +1 @@
1
- {"version":3,"file":"DatabaseKeyStore.cjs.js","sources":["../../src/identity/DatabaseKeyStore.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 { Knex } from 'knex';\nimport { DateTime } from 'luxon';\nimport { AnyJWK, KeyStore, StoredKey } from './types';\n\nconst TABLE = 'signing_keys';\n\ntype Row = {\n created_at: Date; // row.created_at is a string after being returned from the database\n kid: string;\n key: string;\n};\n\nconst parseDate = (date: string | Date) => {\n const parsedDate =\n typeof date === 'string'\n ? DateTime.fromSQL(date, { zone: 'UTC' })\n : DateTime.fromJSDate(date);\n\n if (!parsedDate.isValid) {\n throw new Error(\n `Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`,\n );\n }\n\n return parsedDate.toJSDate();\n};\n\nexport class DatabaseKeyStore implements KeyStore {\n constructor(private readonly client: Knex) {}\n\n async addKey(key: AnyJWK): Promise<void> {\n await this.client<Row>(TABLE).insert({\n kid: key.kid,\n key: JSON.stringify(key),\n });\n }\n\n async listKeys(): Promise<{ items: StoredKey[] }> {\n const rows = await this.client<Row>(TABLE).select();\n\n return {\n items: rows.map(row => ({\n key: JSON.parse(row.key),\n createdAt: parseDate(row.created_at),\n })),\n };\n }\n\n async removeKeys(kids: string[]): Promise<void> {\n await this.client(TABLE).delete().whereIn('kid', kids);\n }\n}\n"],"names":["DateTime"],"mappings":";;;;AAoBA,MAAM,KAAA,GAAQ,cAAA;AAQd,MAAM,SAAA,GAAY,CAAC,IAAA,KAAwB;AACzC,EAAA,MAAM,UAAA,GACJ,OAAO,IAAA,KAAS,QAAA,GACZA,eAAS,OAAA,CAAQ,IAAA,EAAM,EAAE,IAAA,EAAM,KAAA,EAAO,CAAA,GACtCA,cAAA,CAAS,WAAW,IAAI,CAAA;AAE9B,EAAA,IAAI,CAAC,WAAW,OAAA,EAAS;AACvB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,8BAAA,EAAiC,UAAA,CAAW,aAAa,CAAA,eAAA,EAAkB,WAAW,kBAAkB,CAAA;AAAA,KAC1G;AAAA,EACF;AAEA,EAAA,OAAO,WAAW,QAAA,EAAS;AAC7B,CAAA;AAEO,MAAM,gBAAA,CAAqC;AAAA,EAChD,YAA6B,MAAA,EAAc;AAAd,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAAe;AAAA,EAE5C,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,MAAM,IAAA,CAAK,MAAA,CAAY,KAAK,CAAA,CAAE,MAAA,CAAO;AAAA,MACnC,KAAK,GAAA,CAAI,GAAA;AAAA,MACT,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,GAAG;AAAA,KACxB,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,QAAA,GAA4C;AAChD,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,MAAA,CAAY,KAAK,EAAE,MAAA,EAAO;AAElD,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,GAAA,MAAQ;AAAA,QACtB,GAAA,EAAK,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAAA,QACvB,SAAA,EAAW,SAAA,CAAU,GAAA,CAAI,UAAU;AAAA,OACrC,CAAE;AAAA,KACJ;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,IAAA,EAA+B;AAC9C,IAAA,MAAM,IAAA,CAAK,OAAO,KAAK,CAAA,CAAE,QAAO,CAAE,OAAA,CAAQ,OAAO,IAAI,CAAA;AAAA,EACvD;AACF;;;;"}
1
+ {"version":3,"file":"DatabaseKeyStore.cjs.js","sources":["../../src/identity/DatabaseKeyStore.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 { Knex } from 'knex';\nimport { DateTime } from 'luxon';\nimport { AnyJWK, KeyStore, StoredKey } from './types';\n\nconst TABLE = 'signing_keys';\n\ntype Row = {\n created_at: Date; // row.created_at is a string after being returned from the database\n kid: string;\n key: string;\n};\n\nconst parseDate = (date: string | Date) => {\n const parsedDate =\n typeof date === 'string'\n ? DateTime.fromSQL(date, { zone: 'UTC' })\n : DateTime.fromJSDate(date);\n\n if (!parsedDate.isValid) {\n throw new Error(\n `Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`,\n );\n }\n\n return parsedDate.toJSDate();\n};\n\nexport class DatabaseKeyStore implements KeyStore {\n constructor(private readonly client: Knex) {}\n\n async addKey(key: AnyJWK): Promise<void> {\n await this.client<Row>(TABLE).insert({\n kid: key.kid,\n key: JSON.stringify(key),\n });\n }\n\n async listKeys(): Promise<{ items: StoredKey[] }> {\n const rows = await this.client<Row>(TABLE).select();\n\n return {\n items: rows.map(row => ({\n key: JSON.parse(row.key),\n createdAt: parseDate(row.created_at),\n })),\n };\n }\n\n async removeKeys(kids: string[]): Promise<void> {\n await this.client(TABLE).delete().whereIn('kid', kids);\n }\n}\n"],"names":["DateTime"],"mappings":";;;;AAoBA,MAAM,KAAA,GAAQ,cAAA;AAQd,MAAM,SAAA,GAAY,CAAC,IAAA,KAAwB;AACzC,EAAA,MAAM,UAAA,GACJ,OAAO,IAAA,KAAS,QAAA,GACZA,eAAS,OAAA,CAAQ,IAAA,EAAM,EAAE,IAAA,EAAM,KAAA,EAAO,CAAA,GACtCA,cAAA,CAAS,WAAW,IAAI,CAAA;AAE9B,EAAA,IAAI,CAAC,WAAW,OAAA,EAAS;AACvB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,8BAAA,EAAiC,UAAA,CAAW,aAAa,CAAA,eAAA,EAAkB,WAAW,kBAAkB,CAAA;AAAA,KAC1G;AAAA,EACF;AAEA,EAAA,OAAO,WAAW,QAAA,EAAS;AAC7B,CAAA;AAEO,MAAM,gBAAA,CAAqC;AAAA,EAChD,YAA6B,MAAA,EAAc;AAAd,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAAe;AAAA,EAAf,MAAA;AAAA,EAE7B,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,MAAM,IAAA,CAAK,MAAA,CAAY,KAAK,CAAA,CAAE,MAAA,CAAO;AAAA,MACnC,KAAK,GAAA,CAAI,GAAA;AAAA,MACT,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,GAAG;AAAA,KACxB,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,QAAA,GAA4C;AAChD,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,MAAA,CAAY,KAAK,EAAE,MAAA,EAAO;AAElD,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,GAAA,MAAQ;AAAA,QACtB,GAAA,EAAK,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAAA,QACvB,SAAA,EAAW,SAAA,CAAU,GAAA,CAAI,UAAU;AAAA,OACrC,CAAE;AAAA,KACJ;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,IAAA,EAA+B;AAC9C,IAAA,MAAM,IAAA,CAAK,OAAO,KAAK,CAAA,CAAE,QAAO,CAAE,OAAA,CAAQ,OAAO,IAAI,CAAA;AAAA,EACvD;AACF;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"FirestoreKeyStore.cjs.js","sources":["../../src/identity/FirestoreKeyStore.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { LoggerService } from '@backstage/backend-plugin-api';\nimport {\n DocumentData,\n Firestore,\n QuerySnapshot,\n Settings,\n WriteResult,\n} from '@google-cloud/firestore';\n\nimport { toError } from '@backstage/errors';\nimport { AnyJWK, KeyStore, StoredKey } from './types';\n\nexport type FirestoreKeyStoreSettings = Settings & Options;\n\ntype Options = {\n path?: string;\n timeout?: number;\n};\n\nexport const DEFAULT_TIMEOUT_MS = 10000;\nexport const DEFAULT_DOCUMENT_PATH = 'sessions';\n\nexport class FirestoreKeyStore implements KeyStore {\n static async create(\n settings?: FirestoreKeyStoreSettings,\n ): Promise<FirestoreKeyStore> {\n const { path, timeout, ...firestoreSettings } = settings ?? {};\n const database = new Firestore(firestoreSettings);\n\n return new FirestoreKeyStore(\n database,\n path ?? DEFAULT_DOCUMENT_PATH,\n timeout ?? DEFAULT_TIMEOUT_MS,\n );\n }\n\n private readonly database: Firestore;\n private readonly path: string;\n private readonly timeout: number;\n\n private constructor(database: Firestore, path: string, timeout: number) {\n this.database = database;\n this.path = path;\n this.timeout = timeout;\n }\n\n static async verifyConnection(\n keyStore: FirestoreKeyStore,\n logger?: LoggerService,\n ): Promise<void> {\n try {\n await keyStore.verify();\n } catch (error) {\n const err = toError(error);\n if (process.env.NODE_ENV !== 'development') {\n throw new Error(`Failed to connect to database: ${err.message}`);\n }\n logger?.warn(`Failed to connect to database: ${err.message}`);\n }\n }\n\n async addKey(key: AnyJWK): Promise<void> {\n await this.withTimeout<WriteResult>(\n this.database.collection(this.path).doc(key.kid).set({\n kid: key.kid,\n key: key,\n }),\n );\n }\n\n async listKeys(): Promise<{ items: StoredKey[] }> {\n const keys = await this.withTimeout<QuerySnapshot<DocumentData>>(\n this.database.collection(this.path).get(),\n );\n\n return {\n items: keys.docs.map(doc => {\n const { key } = doc.data();\n\n return {\n createdAt: doc.createTime.toDate(),\n key: typeof key === 'string' ? JSON.parse(key) : key,\n };\n }),\n };\n }\n\n async removeKeys(kids: string[]): Promise<void> {\n // This is probably really slow, but it's done async in the background\n for (const kid of kids) {\n await this.withTimeout<WriteResult>(\n this.database.collection(this.path).doc(kid).delete(),\n );\n }\n\n /**\n * This could be achieved with batching but there's a couple of limitations with that:\n *\n * - A batched write can contain a maximum of 500 operations\n * https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes\n *\n * - The \"in\" operator can combine a maximum of 10 equality clauses\n * https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any\n *\n * Example:\n *\n * const batch = this.database.batch();\n * const docs = await this.database\n * .collection(this.path)\n * .where('kid', 'in', kids)\n * .get();\n * docs.forEach(doc => {\n * batch.delete(doc.ref);\n * });\n * await batch.commit();\n *\n */\n }\n\n /**\n * Helper function to allow us to modify the timeout used when\n * performing Firestore database operations.\n *\n * The reason for this is that it seems that there's no other\n * practical solution to change the default timeout of 10mins\n * that Firestore has.\n *\n */\n private async withTimeout<T>(operation: Promise<T>): Promise<T> {\n const timer = new Promise<never>((_, reject) =>\n setTimeout(() => {\n reject(new Error(`Operation timed out after ${this.timeout}ms`));\n }, this.timeout),\n );\n return Promise.race<T>([operation, timer]);\n }\n\n /**\n * Used to verify that the database is reachable.\n */\n private async verify(): Promise<void> {\n await this.withTimeout(this.database.collection(this.path).limit(1).get());\n }\n}\n"],"names":["Firestore","toError"],"mappings":";;;;;AAmCO,MAAM,kBAAA,GAAqB;AAC3B,MAAM,qBAAA,GAAwB;AAE9B,MAAM,iBAAA,CAAsC;AAAA,EACjD,aAAa,OACX,QAAA,EAC4B;AAC5B,IAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAS,GAAG,iBAAA,EAAkB,GAAI,YAAY,EAAC;AAC7D,IAAA,MAAM,QAAA,GAAW,IAAIA,mBAAA,CAAU,iBAAiB,CAAA;AAEhD,IAAA,OAAO,IAAI,iBAAA;AAAA,MACT,QAAA;AAAA,MACA,IAAA,IAAQ,qBAAA;AAAA,MACR,OAAA,IAAW;AAAA,KACb;AAAA,EACF;AAAA,EAEiB,QAAA;AAAA,EACA,IAAA;AAAA,EACA,OAAA;AAAA,EAET,WAAA,CAAY,QAAA,EAAqB,IAAA,EAAc,OAAA,EAAiB;AACtE,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AAAA,EACjB;AAAA,EAEA,aAAa,gBAAA,CACX,QAAA,EACA,MAAA,EACe;AACf,IAAA,IAAI;AACF,MAAA,MAAM,SAAS,MAAA,EAAO;AAAA,IACxB,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,GAAA,GAAMC,eAAQ,KAAK,CAAA;AACzB,MAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa,aAAA,EAAe;AAC1C,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,+BAAA,EAAkC,GAAA,CAAI,OAAO,CAAA,CAAE,CAAA;AAAA,MACjE;AACA,MAAA,MAAA,EAAQ,IAAA,CAAK,CAAA,+BAAA,EAAkC,GAAA,CAAI,OAAO,CAAA,CAAE,CAAA;AAAA,IAC9D;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,MAAM,IAAA,CAAK,WAAA;AAAA,MACT,IAAA,CAAK,QAAA,CAAS,UAAA,CAAW,IAAA,CAAK,IAAI,EAAE,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA,CAAE,GAAA,CAAI;AAAA,QACnD,KAAK,GAAA,CAAI,GAAA;AAAA,QACT;AAAA,OACD;AAAA,KACH;AAAA,EACF;AAAA,EAEA,MAAM,QAAA,GAA4C;AAChD,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,WAAA;AAAA,MACtB,KAAK,QAAA,CAAS,UAAA,CAAW,IAAA,CAAK,IAAI,EAAE,GAAA;AAAI,KAC1C;AAEA,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,CAAA,GAAA,KAAO;AAC1B,QAAA,MAAM,EAAE,GAAA,EAAI,GAAI,GAAA,CAAI,IAAA,EAAK;AAEzB,QAAA,OAAO;AAAA,UACL,SAAA,EAAW,GAAA,CAAI,UAAA,CAAW,MAAA,EAAO;AAAA,UACjC,KAAK,OAAO,GAAA,KAAQ,WAAW,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,GAAI;AAAA,SACnD;AAAA,MACF,CAAC;AAAA,KACH;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,IAAA,EAA+B;AAE9C,IAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,MAAA,MAAM,IAAA,CAAK,WAAA;AAAA,QACT,IAAA,CAAK,SAAS,UAAA,CAAW,IAAA,CAAK,IAAI,CAAA,CAAE,GAAA,CAAI,GAAG,CAAA,CAAE,MAAA;AAAO,OACtD;AAAA,IACF;AAAA,EAwBF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAc,YAAe,SAAA,EAAmC;AAC9D,IAAA,MAAM,QAAQ,IAAI,OAAA;AAAA,MAAe,CAAC,CAAA,EAAG,MAAA,KACnC,UAAA,CAAW,MAAM;AACf,QAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,IAAA,CAAK,OAAO,IAAI,CAAC,CAAA;AAAA,MACjE,CAAA,EAAG,KAAK,OAAO;AAAA,KACjB;AACA,IAAA,OAAO,OAAA,CAAQ,IAAA,CAAQ,CAAC,SAAA,EAAW,KAAK,CAAC,CAAA;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,MAAA,GAAwB;AACpC,IAAA,MAAM,IAAA,CAAK,WAAA,CAAY,IAAA,CAAK,QAAA,CAAS,UAAA,CAAW,IAAA,CAAK,IAAI,CAAA,CAAE,KAAA,CAAM,CAAC,CAAA,CAAE,GAAA,EAAK,CAAA;AAAA,EAC3E;AACF;;;;;;"}
1
+ {"version":3,"file":"FirestoreKeyStore.cjs.js","sources":["../../src/identity/FirestoreKeyStore.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { LoggerService } from '@backstage/backend-plugin-api';\nimport {\n DocumentData,\n Firestore,\n QuerySnapshot,\n Settings,\n WriteResult,\n} from '@google-cloud/firestore';\n\nimport { toError } from '@backstage/errors';\nimport { AnyJWK, KeyStore, StoredKey } from './types';\n\nexport type FirestoreKeyStoreSettings = Settings & Options;\n\ntype Options = {\n path?: string;\n timeout?: number;\n};\n\nexport const DEFAULT_TIMEOUT_MS = 10000;\nexport const DEFAULT_DOCUMENT_PATH = 'sessions';\n\nexport class FirestoreKeyStore implements KeyStore {\n static async create(\n settings?: FirestoreKeyStoreSettings,\n ): Promise<FirestoreKeyStore> {\n const { path, timeout, ...firestoreSettings } = settings ?? {};\n const database = new Firestore(firestoreSettings);\n\n return new FirestoreKeyStore(\n database,\n path ?? DEFAULT_DOCUMENT_PATH,\n timeout ?? DEFAULT_TIMEOUT_MS,\n );\n }\n\n private readonly database: Firestore;\n private readonly path: string;\n private readonly timeout: number;\n\n private constructor(database: Firestore, path: string, timeout: number) {\n this.database = database;\n this.path = path;\n this.timeout = timeout;\n }\n\n static async verifyConnection(\n keyStore: FirestoreKeyStore,\n logger?: LoggerService,\n ): Promise<void> {\n try {\n await keyStore.verify();\n } catch (error) {\n const err = toError(error);\n if (process.env.NODE_ENV !== 'development') {\n throw new Error(`Failed to connect to database: ${err.message}`);\n }\n logger?.warn(`Failed to connect to database: ${err.message}`);\n }\n }\n\n async addKey(key: AnyJWK): Promise<void> {\n await this.withTimeout<WriteResult>(\n this.database.collection(this.path).doc(key.kid).set({\n kid: key.kid,\n key: key,\n }),\n );\n }\n\n async listKeys(): Promise<{ items: StoredKey[] }> {\n const keys = await this.withTimeout<QuerySnapshot<DocumentData>>(\n this.database.collection(this.path).get(),\n );\n\n return {\n items: keys.docs.map(doc => {\n const { key } = doc.data();\n\n return {\n createdAt: doc.createTime.toDate(),\n key: typeof key === 'string' ? JSON.parse(key) : key,\n };\n }),\n };\n }\n\n async removeKeys(kids: string[]): Promise<void> {\n // This is probably really slow, but it's done async in the background\n for (const kid of kids) {\n await this.withTimeout<WriteResult>(\n this.database.collection(this.path).doc(kid).delete(),\n );\n }\n\n /**\n * This could be achieved with batching but there's a couple of limitations with that:\n *\n * - A batched write can contain a maximum of 500 operations\n * https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes\n *\n * - The \"in\" operator can combine a maximum of 10 equality clauses\n * https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any\n *\n * Example:\n *\n * const batch = this.database.batch();\n * const docs = await this.database\n * .collection(this.path)\n * .where('kid', 'in', kids)\n * .get();\n * docs.forEach(doc => {\n * batch.delete(doc.ref);\n * });\n * await batch.commit();\n *\n */\n }\n\n /**\n * Helper function to allow us to modify the timeout used when\n * performing Firestore database operations.\n *\n * The reason for this is that it seems that there's no other\n * practical solution to change the default timeout of 10mins\n * that Firestore has.\n *\n */\n private async withTimeout<T>(operation: Promise<T>): Promise<T> {\n const timer = new Promise<never>((_, reject) =>\n setTimeout(() => {\n reject(new Error(`Operation timed out after ${this.timeout}ms`));\n }, this.timeout),\n );\n return Promise.race<T>([operation, timer]);\n }\n\n /**\n * Used to verify that the database is reachable.\n */\n private async verify(): Promise<void> {\n await this.withTimeout(this.database.collection(this.path).limit(1).get());\n }\n}\n"],"names":["Firestore","toError"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"KeyStores.cjs.js","sources":["../../src/identity/KeyStores.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { pickBy } from 'lodash';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nimport { Config } from '@backstage/config';\nimport { AuthDatabase } from '../database/AuthDatabase';\nimport { DatabaseKeyStore } from './DatabaseKeyStore';\nimport { FirestoreKeyStore } from './FirestoreKeyStore';\nimport { MemoryKeyStore } from './MemoryKeyStore';\nimport { KeyStore } from './types';\nimport { StaticKeyStore } from './StaticKeyStore';\n\ntype Options = {\n logger: LoggerService;\n database: AuthDatabase;\n};\n\nexport class KeyStores {\n /**\n * Looks at the `auth.keyStore` section in the application configuration\n * and returns a KeyStore store. Defaults to `database`\n *\n * @returns a KeyStore store\n */\n static async fromConfig(config: Config, options: Options): Promise<KeyStore> {\n const { logger, database } = options;\n\n const ks = config.getOptionalConfig('auth.keyStore');\n const provider = ks?.getOptionalString('provider') ?? 'database';\n\n logger.info(`Configuring \"${provider}\" as KeyStore provider`);\n\n if (provider === 'database') {\n return new DatabaseKeyStore(await database.get());\n }\n\n if (provider === 'memory') {\n return new MemoryKeyStore();\n }\n\n if (provider === 'firestore') {\n const settings = ks?.getConfig(provider);\n\n const keyStore = await FirestoreKeyStore.create(\n pickBy(\n {\n projectId: settings?.getOptionalString('projectId'),\n keyFilename: settings?.getOptionalString('keyFilename'),\n host: settings?.getOptionalString('host'),\n port: settings?.getOptionalNumber('port'),\n ssl: settings?.getOptionalBoolean('ssl'),\n path: settings?.getOptionalString('path'),\n timeout: settings?.getOptionalNumber('timeout'),\n },\n value => value !== undefined,\n ),\n );\n await FirestoreKeyStore.verifyConnection(keyStore, logger);\n\n return keyStore;\n }\n\n if (provider === 'static') {\n return await StaticKeyStore.fromConfig(config);\n }\n\n throw new Error(`Unknown KeyStore provider: ${provider}`);\n }\n}\n"],"names":["DatabaseKeyStore","MemoryKeyStore","FirestoreKeyStore","pickBy","StaticKeyStore"],"mappings":";;;;;;;;AAgCO,MAAM,SAAA,CAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrB,aAAa,UAAA,CAAW,MAAA,EAAgB,OAAA,EAAqC;AAC3E,IAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAS,GAAI,OAAA;AAE7B,IAAA,MAAM,EAAA,GAAK,MAAA,CAAO,iBAAA,CAAkB,eAAe,CAAA;AACnD,IAAA,MAAM,QAAA,GAAW,EAAA,EAAI,iBAAA,CAAkB,UAAU,CAAA,IAAK,UAAA;AAEtD,IAAA,MAAA,CAAO,IAAA,CAAK,CAAA,aAAA,EAAgB,QAAQ,CAAA,sBAAA,CAAwB,CAAA;AAE5D,IAAA,IAAI,aAAa,UAAA,EAAY;AAC3B,MAAA,OAAO,IAAIA,iCAAA,CAAiB,MAAM,QAAA,CAAS,KAAK,CAAA;AAAA,IAClD;AAEA,IAAA,IAAI,aAAa,QAAA,EAAU;AACzB,MAAA,OAAO,IAAIC,6BAAA,EAAe;AAAA,IAC5B;AAEA,IAAA,IAAI,aAAa,WAAA,EAAa;AAC5B,MAAA,MAAM,QAAA,GAAW,EAAA,EAAI,SAAA,CAAU,QAAQ,CAAA;AAEvC,MAAA,MAAM,QAAA,GAAW,MAAMC,mCAAA,CAAkB,MAAA;AAAA,QACvCC,aAAA;AAAA,UACE;AAAA,YACE,SAAA,EAAW,QAAA,EAAU,iBAAA,CAAkB,WAAW,CAAA;AAAA,YAClD,WAAA,EAAa,QAAA,EAAU,iBAAA,CAAkB,aAAa,CAAA;AAAA,YACtD,IAAA,EAAM,QAAA,EAAU,iBAAA,CAAkB,MAAM,CAAA;AAAA,YACxC,IAAA,EAAM,QAAA,EAAU,iBAAA,CAAkB,MAAM,CAAA;AAAA,YACxC,GAAA,EAAK,QAAA,EAAU,kBAAA,CAAmB,KAAK,CAAA;AAAA,YACvC,IAAA,EAAM,QAAA,EAAU,iBAAA,CAAkB,MAAM,CAAA;AAAA,YACxC,OAAA,EAAS,QAAA,EAAU,iBAAA,CAAkB,SAAS;AAAA,WAChD;AAAA,UACA,WAAS,KAAA,KAAU;AAAA;AACrB,OACF;AACA,MAAA,MAAMD,mCAAA,CAAkB,gBAAA,CAAiB,QAAA,EAAU,MAAM,CAAA;AAEzD,MAAA,OAAO,QAAA;AAAA,IACT;AAEA,IAAA,IAAI,aAAa,QAAA,EAAU;AACzB,MAAA,OAAO,MAAME,6BAAA,CAAe,UAAA,CAAW,MAAM,CAAA;AAAA,IAC/C;AAEA,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,2BAAA,EAA8B,QAAQ,CAAA,CAAE,CAAA;AAAA,EAC1D;AACF;;;;"}
1
+ {"version":3,"file":"KeyStores.cjs.js","sources":["../../src/identity/KeyStores.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { pickBy } from 'lodash';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nimport { Config } from '@backstage/config';\nimport { AuthDatabase } from '../database/AuthDatabase';\nimport { DatabaseKeyStore } from './DatabaseKeyStore';\nimport { FirestoreKeyStore } from './FirestoreKeyStore';\nimport { MemoryKeyStore } from './MemoryKeyStore';\nimport { KeyStore } from './types';\nimport { StaticKeyStore } from './StaticKeyStore';\n\ntype Options = {\n logger: LoggerService;\n database: AuthDatabase;\n};\n\nexport class KeyStores {\n /**\n * Looks at the `auth.keyStore` section in the application configuration\n * and returns a KeyStore store. Defaults to `database`\n *\n * @returns a KeyStore store\n */\n static async fromConfig(config: Config, options: Options): Promise<KeyStore> {\n const { logger, database } = options;\n\n const ks = config.getOptionalConfig('auth.keyStore');\n const provider = ks?.getOptionalString('provider') ?? 'database';\n\n logger.info(`Configuring \"${provider}\" as KeyStore provider`);\n\n if (provider === 'database') {\n return new DatabaseKeyStore(await database.get());\n }\n\n if (provider === 'memory') {\n return new MemoryKeyStore();\n }\n\n if (provider === 'firestore') {\n const settings = ks?.getConfig(provider);\n\n const keyStore = await FirestoreKeyStore.create(\n pickBy(\n {\n projectId: settings?.getOptionalString('projectId'),\n keyFilename: settings?.getOptionalString('keyFilename'),\n host: settings?.getOptionalString('host'),\n port: settings?.getOptionalNumber('port'),\n ssl: settings?.getOptionalBoolean('ssl'),\n path: settings?.getOptionalString('path'),\n timeout: settings?.getOptionalNumber('timeout'),\n },\n value => value !== undefined,\n ),\n );\n await FirestoreKeyStore.verifyConnection(keyStore, logger);\n\n return keyStore;\n }\n\n if (provider === 'static') {\n return await StaticKeyStore.fromConfig(config);\n }\n\n throw new Error(`Unknown KeyStore provider: ${provider}`);\n }\n}\n"],"names":["DatabaseKeyStore","MemoryKeyStore","FirestoreKeyStore","pickBy","StaticKeyStore"],"mappings":";;;;;;;;AAgCO,MAAM,SAAA,CAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrB,aAAa,UAAA,CAAW,MAAA,EAAgB,OAAA,EAAqC;AAC3E,IAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAS,GAAI,OAAA;AAE7B,IAAA,MAAM,EAAA,GAAK,MAAA,CAAO,iBAAA,CAAkB,eAAe,CAAA;AACnD,IAAA,MAAM,QAAA,GAAW,EAAA,EAAI,iBAAA,CAAkB,UAAU,CAAA,IAAK,UAAA;AAEtD,IAAA,MAAA,CAAO,IAAA,CAAK,CAAA,aAAA,EAAgB,QAAQ,CAAA,sBAAA,CAAwB,CAAA;AAE5D,IAAA,IAAI,aAAa,UAAA,EAAY;AAC3B,MAAA,OAAO,IAAIA,iCAAA,CAAiB,MAAM,QAAA,CAAS,KAAK,CAAA;AAAA,IAClD;AAEA,IAAA,IAAI,aAAa,QAAA,EAAU;AACzB,MAAA,OAAO,IAAIC,6BAAA,EAAe;AAAA,IAC5B;AAEA,IAAA,IAAI,aAAa,WAAA,EAAa;AAC5B,MAAA,MAAM,QAAA,GAAW,EAAA,EAAI,SAAA,CAAU,QAAQ,CAAA;AAEvC,MAAA,MAAM,QAAA,GAAW,MAAMC,mCAAA,CAAkB,MAAA;AAAA,QACvCC,aAAA;AAAA,UACE;AAAA,YACE,SAAA,EAAW,QAAA,EAAU,iBAAA,CAAkB,WAAW,CAAA;AAAA,YAClD,WAAA,EAAa,QAAA,EAAU,iBAAA,CAAkB,aAAa,CAAA;AAAA,YACtD,IAAA,EAAM,QAAA,EAAU,iBAAA,CAAkB,MAAM,CAAA;AAAA,YACxC,IAAA,EAAM,QAAA,EAAU,iBAAA,CAAkB,MAAM,CAAA;AAAA,YACxC,GAAA,EAAK,QAAA,EAAU,kBAAA,CAAmB,KAAK,CAAA;AAAA,YACvC,IAAA,EAAM,QAAA,EAAU,iBAAA,CAAkB,MAAM,CAAA;AAAA,YACxC,OAAA,EAAS,QAAA,EAAU,iBAAA,CAAkB,SAAS;AAAA,WAChD;AAAA,UACA,WAAS,KAAA,KAAU;AAAA;AACrB,OACF;AACA,MAAA,MAAMD,mCAAA,CAAkB,gBAAA,CAAiB,QAAA,EAAU,MAAM,CAAA;AAEzD,MAAA,OAAO,QAAA;AAAA,IACT;AAEA,IAAA,IAAI,aAAa,QAAA,EAAU;AACzB,MAAA,OAAO,MAAME,6BAAA,CAAe,UAAA,CAAW,MAAM,CAAA;AAAA,IAC/C;AAEA,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,2BAAA,EAA8B,QAAQ,CAAA,CAAE,CAAA;AAAA,EAC1D;AACF;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"MemoryKeyStore.cjs.js","sources":["../../src/identity/MemoryKeyStore.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 { KeyStore, AnyJWK, StoredKey } from './types';\nimport { DateTime } from 'luxon';\n\nexport class MemoryKeyStore implements KeyStore {\n private readonly keys = new Map<string, { createdAt: Date; key: string }>();\n\n async addKey(key: AnyJWK): Promise<void> {\n this.keys.set(key.kid, {\n createdAt: DateTime.utc().toJSDate(),\n key: JSON.stringify(key),\n });\n }\n\n async removeKeys(kids: string[]): Promise<void> {\n for (const kid of kids) {\n this.keys.delete(kid);\n }\n }\n\n async listKeys(): Promise<{ items: StoredKey[] }> {\n return {\n items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({\n createdAt,\n key: JSON.parse(keyStr),\n })),\n };\n }\n}\n"],"names":["DateTime"],"mappings":";;;;AAmBO,MAAM,cAAA,CAAmC;AAAA,EAC7B,IAAA,uBAAW,GAAA,EAA8C;AAAA,EAE1E,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK;AAAA,MACrB,SAAA,EAAWA,cAAA,CAAS,GAAA,EAAI,CAAE,QAAA,EAAS;AAAA,MACnC,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,GAAG;AAAA,KACxB,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,WAAW,IAAA,EAA+B;AAC9C,IAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,MAAA,IAAA,CAAK,IAAA,CAAK,OAAO,GAAG,CAAA;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAM,QAAA,GAA4C;AAChD,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA,CAAE,GAAA,CAAI,CAAC,GAAG,EAAE,SAAA,EAAW,GAAA,EAAK,MAAA,EAAQ,CAAA,MAAO;AAAA,QACpE,SAAA;AAAA,QACA,GAAA,EAAK,IAAA,CAAK,KAAA,CAAM,MAAM;AAAA,OACxB,CAAE;AAAA,KACJ;AAAA,EACF;AACF;;;;"}
1
+ {"version":3,"file":"MemoryKeyStore.cjs.js","sources":["../../src/identity/MemoryKeyStore.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 { KeyStore, AnyJWK, StoredKey } from './types';\nimport { DateTime } from 'luxon';\n\nexport class MemoryKeyStore implements KeyStore {\n private readonly keys = new Map<string, { createdAt: Date; key: string }>();\n\n async addKey(key: AnyJWK): Promise<void> {\n this.keys.set(key.kid, {\n createdAt: DateTime.utc().toJSDate(),\n key: JSON.stringify(key),\n });\n }\n\n async removeKeys(kids: string[]): Promise<void> {\n for (const kid of kids) {\n this.keys.delete(kid);\n }\n }\n\n async listKeys(): Promise<{ items: StoredKey[] }> {\n return {\n items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({\n createdAt,\n key: JSON.parse(keyStr),\n })),\n };\n }\n}\n"],"names":["DateTime"],"mappings":";;;;AAmBO,MAAM,cAAA,CAAmC;AAAA,EAC7B,IAAA,uBAAW,GAAA,EAA8C;AAAA,EAE1E,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK;AAAA,MACrB,SAAA,EAAWA,cAAA,CAAS,GAAA,EAAI,CAAE,QAAA,EAAS;AAAA,MACnC,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,GAAG;AAAA,KACxB,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,WAAW,IAAA,EAA+B;AAC9C,IAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,MAAA,IAAA,CAAK,IAAA,CAAK,OAAO,GAAG,CAAA;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAM,QAAA,GAA4C;AAChD,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA,CAAE,GAAA,CAAI,CAAC,GAAG,EAAE,SAAA,EAAW,GAAA,EAAK,MAAA,EAAQ,CAAA,MAAO;AAAA,QACpE,SAAA;AAAA,QACA,GAAA,EAAK,IAAA,CAAK,KAAA,CAAM,MAAM;AAAA,OACxB,CAAE;AAAA,KACJ;AAAA,EACF;AACF;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"StaticKeyStore.cjs.js","sources":["../../src/identity/StaticKeyStore.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 */\nimport { AnyJWK, KeyStore, StoredKey } from './types';\nimport { exportJWK, importPKCS8, importSPKI, JWK } from 'jose';\nimport { KeyLike } from 'jose';\nimport { promises as fs } from 'node:fs';\nimport { Config } from '@backstage/config';\n\nexport type KeyPair = {\n publicKey: JWK;\n privateKey: JWK;\n};\n\nexport type StaticKeyConfig = {\n publicKeyFile: string;\n privateKeyFile: string;\n keyId: string;\n algorithm: string;\n};\n\nconst DEFAULT_ALGORITHM = 'ES256';\n\n/**\n * Key store that loads predefined public/private key pairs from disk\n *\n * The private key should be represented using the PKCS#8 format,\n * while the public key should be in the SPKI format.\n *\n * @remarks\n *\n * You can generate a public and private key pair, using\n * openssl:\n *\n * Generate a private key using the ES256 algorithm\n * ```sh\n * openssl ecparam -name prime256v1 -genkey -out private.ec.key\n * ```\n * Convert it to PKCS#8 format\n * ```sh\n * openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private.ec.key -out private.key\n * ```\n * Extract the public key\n * ```sh\n * openssl ec -inform PEM -outform PEM -pubout -in private.key -out public.key\n * ```\n *\n * Provide the paths to private.key and public.key as the respective\n * private and public key paths in the StaticKeyStore.create(...) method.\n */\nexport class StaticKeyStore implements KeyStore {\n private readonly keyPairs: KeyPair[];\n private readonly createdAt: Date;\n\n private constructor(keyPairs: KeyPair[]) {\n if (keyPairs.length === 0) {\n throw new Error('Should provide at least one key pair');\n }\n\n this.keyPairs = keyPairs;\n this.createdAt = new Date();\n }\n\n public static async fromConfig(config: Config): Promise<StaticKeyStore> {\n const keyConfigs = config\n .getConfigArray('auth.keyStore.static.keys')\n .map(c => {\n const staticKeyConfig: StaticKeyConfig = {\n publicKeyFile: c.getString('publicKeyFile'),\n privateKeyFile: c.getString('privateKeyFile'),\n keyId: c.getString('keyId'),\n algorithm: c.getOptionalString('algorithm') ?? DEFAULT_ALGORITHM,\n };\n\n return staticKeyConfig;\n });\n\n const keyPairs = await Promise.all(\n keyConfigs.map(async k => await this.loadKeyPair(k)),\n );\n\n return new StaticKeyStore(keyPairs);\n }\n\n addKey(_key: AnyJWK): Promise<void> {\n throw new Error('Cannot add keys to the static key store');\n }\n\n listKeys(): Promise<{ items: StoredKey[] }> {\n const keys = this.keyPairs.map(k => this.keyPairToStoredKey(k));\n return Promise.resolve({ items: keys });\n }\n\n getPrivateKey(keyId: string): JWK {\n const keyPair = this.keyPairs.find(k => k.publicKey.kid === keyId);\n if (keyPair === undefined) {\n throw new Error(`Could not find key with keyId: ${keyId}`);\n }\n\n return keyPair.privateKey;\n }\n\n removeKeys(_kids: string[]): Promise<void> {\n throw new Error('Cannot remove keys from the static key store');\n }\n\n private keyPairToStoredKey(keyPair: KeyPair): StoredKey {\n const publicKey = {\n ...keyPair.publicKey,\n use: 'sig',\n };\n\n return {\n key: publicKey as AnyJWK,\n createdAt: this.createdAt,\n };\n }\n\n private static async loadKeyPair(options: StaticKeyConfig): Promise<KeyPair> {\n const algorithm = options.algorithm;\n const keyId = options.keyId;\n const publicKey = await this.loadPublicKeyFromFile(\n options.publicKeyFile,\n keyId,\n algorithm,\n );\n const privateKey = await this.loadPrivateKeyFromFile(\n options.privateKeyFile,\n keyId,\n algorithm,\n );\n\n return { publicKey, privateKey };\n }\n\n private static async loadPublicKeyFromFile(\n path: string,\n keyId: string,\n algorithm: string,\n ): Promise<JWK> {\n return this.loadKeyFromFile(path, keyId, algorithm, importSPKI);\n }\n\n private static async loadPrivateKeyFromFile(\n path: string,\n keyId: string,\n algorithm: string,\n ): Promise<JWK> {\n return this.loadKeyFromFile(path, keyId, algorithm, importPKCS8);\n }\n\n private static async loadKeyFromFile(\n path: string,\n keyId: string,\n algorithm: string,\n importer: (content: string, algorithm: string) => Promise<KeyLike>,\n ): Promise<JWK> {\n const content = await fs.readFile(path, { encoding: 'utf8', flag: 'r' });\n const key = await importer(content, algorithm);\n const jwk = await exportJWK(key);\n jwk.kid = keyId;\n jwk.alg = algorithm;\n\n return jwk;\n }\n}\n"],"names":["importSPKI","importPKCS8","fs","exportJWK"],"mappings":"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"}
1
+ {"version":3,"file":"StaticKeyStore.cjs.js","sources":["../../src/identity/StaticKeyStore.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 */\nimport { AnyJWK, KeyStore, StoredKey } from './types';\nimport { exportJWK, importPKCS8, importSPKI, JWK } from 'jose';\nimport { KeyLike } from 'jose';\nimport { promises as fs } from 'node:fs';\nimport { Config } from '@backstage/config';\n\nexport type KeyPair = {\n publicKey: JWK;\n privateKey: JWK;\n};\n\nexport type StaticKeyConfig = {\n publicKeyFile: string;\n privateKeyFile: string;\n keyId: string;\n algorithm: string;\n};\n\nconst DEFAULT_ALGORITHM = 'ES256';\n\n/**\n * Key store that loads predefined public/private key pairs from disk\n *\n * The private key should be represented using the PKCS#8 format,\n * while the public key should be in the SPKI format.\n *\n * @remarks\n *\n * You can generate a public and private key pair, using\n * openssl:\n *\n * Generate a private key using the ES256 algorithm\n * ```sh\n * openssl ecparam -name prime256v1 -genkey -out private.ec.key\n * ```\n * Convert it to PKCS#8 format\n * ```sh\n * openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private.ec.key -out private.key\n * ```\n * Extract the public key\n * ```sh\n * openssl ec -inform PEM -outform PEM -pubout -in private.key -out public.key\n * ```\n *\n * Provide the paths to private.key and public.key as the respective\n * private and public key paths in the StaticKeyStore.create(...) method.\n */\nexport class StaticKeyStore implements KeyStore {\n private readonly keyPairs: KeyPair[];\n private readonly createdAt: Date;\n\n private constructor(keyPairs: KeyPair[]) {\n if (keyPairs.length === 0) {\n throw new Error('Should provide at least one key pair');\n }\n\n this.keyPairs = keyPairs;\n this.createdAt = new Date();\n }\n\n public static async fromConfig(config: Config): Promise<StaticKeyStore> {\n const keyConfigs = config\n .getConfigArray('auth.keyStore.static.keys')\n .map(c => {\n const staticKeyConfig: StaticKeyConfig = {\n publicKeyFile: c.getString('publicKeyFile'),\n privateKeyFile: c.getString('privateKeyFile'),\n keyId: c.getString('keyId'),\n algorithm: c.getOptionalString('algorithm') ?? DEFAULT_ALGORITHM,\n };\n\n return staticKeyConfig;\n });\n\n const keyPairs = await Promise.all(\n keyConfigs.map(async k => await this.loadKeyPair(k)),\n );\n\n return new StaticKeyStore(keyPairs);\n }\n\n addKey(_key: AnyJWK): Promise<void> {\n throw new Error('Cannot add keys to the static key store');\n }\n\n listKeys(): Promise<{ items: StoredKey[] }> {\n const keys = this.keyPairs.map(k => this.keyPairToStoredKey(k));\n return Promise.resolve({ items: keys });\n }\n\n getPrivateKey(keyId: string): JWK {\n const keyPair = this.keyPairs.find(k => k.publicKey.kid === keyId);\n if (keyPair === undefined) {\n throw new Error(`Could not find key with keyId: ${keyId}`);\n }\n\n return keyPair.privateKey;\n }\n\n removeKeys(_kids: string[]): Promise<void> {\n throw new Error('Cannot remove keys from the static key store');\n }\n\n private keyPairToStoredKey(keyPair: KeyPair): StoredKey {\n const publicKey = {\n ...keyPair.publicKey,\n use: 'sig',\n };\n\n return {\n key: publicKey as AnyJWK,\n createdAt: this.createdAt,\n };\n }\n\n private static async loadKeyPair(options: StaticKeyConfig): Promise<KeyPair> {\n const algorithm = options.algorithm;\n const keyId = options.keyId;\n const publicKey = await this.loadPublicKeyFromFile(\n options.publicKeyFile,\n keyId,\n algorithm,\n );\n const privateKey = await this.loadPrivateKeyFromFile(\n options.privateKeyFile,\n keyId,\n algorithm,\n );\n\n return { publicKey, privateKey };\n }\n\n private static async loadPublicKeyFromFile(\n path: string,\n keyId: string,\n algorithm: string,\n ): Promise<JWK> {\n return this.loadKeyFromFile(path, keyId, algorithm, importSPKI);\n }\n\n private static async loadPrivateKeyFromFile(\n path: string,\n keyId: string,\n algorithm: string,\n ): Promise<JWK> {\n return this.loadKeyFromFile(path, keyId, algorithm, importPKCS8);\n }\n\n private static async loadKeyFromFile(\n path: string,\n keyId: string,\n algorithm: string,\n importer: (content: string, algorithm: string) => Promise<KeyLike>,\n ): Promise<JWK> {\n const content = await fs.readFile(path, { encoding: 'utf8', flag: 'r' });\n const key = await importer(content, algorithm);\n const jwk = await exportJWK(key);\n jwk.kid = keyId;\n jwk.alg = algorithm;\n\n return jwk;\n }\n}\n"],"names":["importSPKI","importPKCS8","fs","exportJWK"],"mappings":"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"}
@@ -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 { 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,iBAAA,CAAyC;AAAA,EACnC,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,wBAAA;AAAA,EACA,mBAAA;AAAA,EAEV,WAAA,CAAY,SAAkB,QAAA,EAA0B;AAC7D,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,2BAA2B,OAAA,CAAQ,wBAAA;AACxC,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,sBAAsB,OAAA,CAAQ,mBAAA;AAAA,EACrC;AAAA,EAEA,MAAa,WACX,MAAA,EACgC;AAChC,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,aAAA,EAAc;AAErC,IAAA,OAAOA,6BAAA,CAAe;AAAA,MACpB,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,GAAA;AAAA,MACA,oBAAoB,IAAA,CAAK,wBAAA;AAAA,MACzB,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,qBAAqB,IAAA,CAAK,mBAAA;AAAA,MAC1B;AAAA,KACD,CAAA;AAAA,EACH;AAAA,EAEA,MAAc,aAAA,GAA8B;AAC1C,IAAA,MAAM,EAAE,KAAA,EAAO,IAAA,KAAS,MAAM,IAAA,CAAK,SAAS,QAAA,EAAS;AACrD,IAAA,IAAI,IAAA,CAAK,UAAU,CAAA,EAAG;AACpB,MAAA,OAAO,KAAK,QAAA,CAAS,aAAA,CAAc,KAAK,CAAC,CAAA,CAAE,IAAI,GAAG,CAAA;AAAA,IACpD;AACA,IAAA,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAAA,EACvD;AAAA,EAEA,MAAa,cAAA,GAA8C;AACzD,IAAA,MAAM,EAAE,KAAA,EAAO,IAAA,KAAS,MAAM,IAAA,CAAK,SAAS,QAAA,EAAS;AACrD,IAAA,OAAO,EAAE,MAAM,IAAA,CAAK,GAAA,CAAI,CAAC,EAAE,GAAA,EAAI,KAAM,GAAG,CAAA,EAAE;AAAA,EAC5C;AACF;;;;"}
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,iBAAA,CAAyC;AAAA,EACnC,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,wBAAA;AAAA,EACA,mBAAA;AAAA,EAEV,WAAA,CAAY,SAAkB,QAAA,EAA0B;AAC7D,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,2BAA2B,OAAA,CAAQ,wBAAA;AACxC,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,sBAAsB,OAAA,CAAQ,mBAAA;AAAA,EACrC;AAAA,EAEA,MAAa,WACX,MAAA,EACgC;AAChC,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,aAAA,EAAc;AAErC,IAAA,OAAOA,6BAAA,CAAe;AAAA,MACpB,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,GAAA;AAAA,MACA,oBAAoB,IAAA,CAAK,wBAAA;AAAA,MACzB,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,qBAAqB,IAAA,CAAK,mBAAA;AAAA,MAC1B;AAAA,KACD,CAAA;AAAA,EACH;AAAA,EAEA,MAAc,aAAA,GAA8B;AAC1C,IAAA,MAAM,EAAE,KAAA,EAAO,IAAA,KAAS,MAAM,IAAA,CAAK,SAAS,QAAA,EAAS;AACrD,IAAA,IAAI,IAAA,CAAK,UAAU,CAAA,EAAG;AACpB,MAAA,OAAO,KAAK,QAAA,CAAS,aAAA,CAAc,KAAK,CAAC,CAAA,CAAE,IAAI,GAAG,CAAA;AAAA,IACpD;AACA,IAAA,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAAA,EACvD;AAAA,EAEA,MAAa,cAAA,GAA8C;AACzD,IAAA,MAAM,EAAE,KAAA,EAAO,IAAA,KAAS,MAAM,IAAA,CAAK,SAAS,QAAA,EAAS;AACrD,IAAA,OAAO,EAAE,MAAM,IAAA,CAAK,GAAA,CAAI,CAAC,EAAE,GAAA,EAAI,KAAM,GAAG,CAAA,EAAE;AAAA,EAC5C;AACF;;"}
@@ -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 { randomUUID as uuid } from 'node:crypto';\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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"}
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 { randomUUID as uuid } from 'node:crypto';\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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"}
@@ -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 { 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":"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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":"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"}