@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.
- package/CHANGELOG.md +25 -0
- package/dist/actions/createWhoAmIAction.cjs.js.map +1 -1
- package/dist/actions/index.cjs.js.map +1 -1
- package/dist/authPlugin.cjs.js.map +1 -1
- package/dist/database/AuthDatabase.cjs.js.map +1 -1
- package/dist/database/OfflineSessionDatabase.cjs.js.map +1 -1
- package/dist/database/OidcDatabase.cjs.js +1 -0
- package/dist/database/OidcDatabase.cjs.js.map +1 -1
- package/dist/database/UserInfoDatabase.cjs.js +1 -0
- package/dist/database/UserInfoDatabase.cjs.js.map +1 -1
- package/dist/identity/DatabaseKeyStore.cjs.js +1 -0
- package/dist/identity/DatabaseKeyStore.cjs.js.map +1 -1
- package/dist/identity/FirestoreKeyStore.cjs.js.map +1 -1
- package/dist/identity/KeyStores.cjs.js.map +1 -1
- package/dist/identity/MemoryKeyStore.cjs.js.map +1 -1
- package/dist/identity/StaticKeyStore.cjs.js.map +1 -1
- package/dist/identity/StaticTokenIssuer.cjs.js.map +1 -1
- package/dist/identity/TokenFactory.cjs.js.map +1 -1
- package/dist/identity/issueUserToken.cjs.js.map +1 -1
- package/dist/lib/catalog/CatalogIdentityClient.cjs.js +1 -1
- package/dist/lib/catalog/CatalogIdentityClient.cjs.js.map +1 -1
- package/dist/lib/refreshToken.cjs.js.map +1 -1
- package/dist/lib/resolvers/CatalogAuthResolverContext.cjs.js +1 -3
- package/dist/lib/resolvers/CatalogAuthResolverContext.cjs.js.map +1 -1
- package/dist/providers/router.cjs.js.map +1 -1
- package/dist/service/CimdClient.cjs.js.map +1 -1
- package/dist/service/OfflineAccessService.cjs.js.map +1 -1
- package/dist/service/OidcError.cjs.js.map +1 -1
- package/dist/service/OidcRouter.cjs.js.map +1 -1
- package/dist/service/OidcService.cjs.js.map +1 -1
- package/dist/service/readTokenExpiration.cjs.js.map +1 -1
- package/dist/service/router.cjs.js.map +1 -1
- package/package.json +12 -12
|
@@ -46,7 +46,7 @@ class CatalogIdentityClient {
|
|
|
46
46
|
const { entityRefs, logger } = query;
|
|
47
47
|
const resolvedEntityRefs = entityRefs.map((ref) => {
|
|
48
48
|
try {
|
|
49
|
-
const parsedRef = catalogModel.parseEntityRef(ref.
|
|
49
|
+
const parsedRef = catalogModel.parseEntityRef(ref.toLowerCase(), {
|
|
50
50
|
defaultKind: "user",
|
|
51
51
|
defaultNamespace: "default"
|
|
52
52
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CatalogIdentityClient.cjs.js","sources":["../../../src/lib/catalog/CatalogIdentityClient.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 { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n CompoundEntityRef,\n parseEntityRef,\n RELATION_MEMBER_OF,\n stringifyEntityRef,\n UserEntity,\n} from '@backstage/catalog-model';\n\n/**\n * A catalog client tailored for reading out identity data from the catalog.\n */\nexport class CatalogIdentityClient {\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n\n constructor(options: { catalog: CatalogService; auth: AuthService }) {\n this.catalog = options.catalog;\n this.auth = options.auth;\n }\n\n /**\n * Looks up a single user using a query.\n *\n * Throws a NotFoundError or ConflictError if 0 or multiple users are found.\n */\n async findUser(query: {\n annotations: Record<string, string>;\n }): Promise<UserEntity> {\n const filter: Record<string, string> = {\n kind: 'user',\n };\n for (const [key, value] of Object.entries(query.annotations)) {\n filter[`metadata.annotations.${key}`] = value;\n }\n\n const { items } = await this.catalog.getEntities(\n { filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n\n if (items.length !== 1) {\n if (items.length > 1) {\n throw new ConflictError('User lookup resulted in multiple matches');\n } else {\n throw new NotFoundError('User not found');\n }\n }\n\n return items[0] as UserEntity;\n }\n\n /**\n * Resolve additional entity claims from the catalog, using the passed-in entity names. Designed\n * to be used within a `signInResolver` where additional entity claims might be provided, but\n * group membership and transient group membership lean on imported catalog relations.\n *\n * Returns a superset of the entity names that can be passed directly to `issueToken` as `ent`.\n */\n async resolveCatalogMembership(query: {\n entityRefs: string[];\n logger?: LoggerService;\n }): Promise<string[]> {\n const { entityRefs, logger } = query;\n const resolvedEntityRefs = entityRefs\n .map((ref: string) => {\n try {\n const parsedRef = parseEntityRef(ref.
|
|
1
|
+
{"version":3,"file":"CatalogIdentityClient.cjs.js","sources":["../../../src/lib/catalog/CatalogIdentityClient.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 { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n CompoundEntityRef,\n parseEntityRef,\n RELATION_MEMBER_OF,\n stringifyEntityRef,\n UserEntity,\n} from '@backstage/catalog-model';\n\n/**\n * A catalog client tailored for reading out identity data from the catalog.\n */\nexport class CatalogIdentityClient {\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n\n constructor(options: { catalog: CatalogService; auth: AuthService }) {\n this.catalog = options.catalog;\n this.auth = options.auth;\n }\n\n /**\n * Looks up a single user using a query.\n *\n * Throws a NotFoundError or ConflictError if 0 or multiple users are found.\n */\n async findUser(query: {\n annotations: Record<string, string>;\n }): Promise<UserEntity> {\n const filter: Record<string, string> = {\n kind: 'user',\n };\n for (const [key, value] of Object.entries(query.annotations)) {\n filter[`metadata.annotations.${key}`] = value;\n }\n\n const { items } = await this.catalog.getEntities(\n { filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n\n if (items.length !== 1) {\n if (items.length > 1) {\n throw new ConflictError('User lookup resulted in multiple matches');\n } else {\n throw new NotFoundError('User not found');\n }\n }\n\n return items[0] as UserEntity;\n }\n\n /**\n * Resolve additional entity claims from the catalog, using the passed-in entity names. Designed\n * to be used within a `signInResolver` where additional entity claims might be provided, but\n * group membership and transient group membership lean on imported catalog relations.\n *\n * Returns a superset of the entity names that can be passed directly to `issueToken` as `ent`.\n */\n async resolveCatalogMembership(query: {\n entityRefs: string[];\n logger?: LoggerService;\n }): Promise<string[]> {\n const { entityRefs, logger } = query;\n const resolvedEntityRefs = entityRefs\n .map((ref: string) => {\n try {\n const parsedRef = parseEntityRef(ref.toLowerCase(), {\n defaultKind: 'user',\n defaultNamespace: 'default',\n });\n return parsedRef;\n } catch {\n logger?.warn(`Failed to parse entityRef from ${ref}, ignoring`);\n return null;\n }\n })\n .filter((ref): ref is CompoundEntityRef => ref !== null);\n\n const filter = resolvedEntityRefs.map(ref => ({\n kind: ref.kind,\n 'metadata.namespace': ref.namespace,\n 'metadata.name': ref.name,\n }));\n\n const entities = await this.catalog\n .getEntities(\n { filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n )\n .then(r => r.items);\n\n if (entityRefs.length !== entities.length) {\n const foundEntityNames = entities.map(stringifyEntityRef);\n const missingEntityNames = resolvedEntityRefs\n .map(stringifyEntityRef)\n .filter(s => !foundEntityNames.includes(s));\n logger?.debug(`Entities not found for refs ${missingEntityNames.join()}`);\n }\n\n const memberOf = entities.flatMap(\n e =>\n e!.relations\n ?.filter(r => r.type === RELATION_MEMBER_OF)\n .map(r => r.targetRef) ?? [],\n );\n\n const newEntityRefs = [\n ...new Set(resolvedEntityRefs.map(stringifyEntityRef).concat(memberOf)),\n ];\n\n logger?.debug(`Found catalog membership: ${newEntityRefs.join()}`);\n return newEntityRefs;\n }\n}\n"],"names":["ConflictError","NotFoundError","parseEntityRef","stringifyEntityRef","RELATION_MEMBER_OF"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refreshToken.cjs.js","sources":["../../src/lib/refreshToken.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 {\n randomBytes,\n scrypt,\n timingSafeEqual,\n ScryptOptions,\n} from 'node:crypto';\n\nconst SALT_LENGTH = 16;\nconst KEY_LENGTH = 64;\nconst SCRYPT_OPTIONS: ScryptOptions = { N: 16384, r: 8, p: 1 };\n\nfunction scryptAsync(\n password: string,\n salt: Buffer,\n keylen: number,\n options: ScryptOptions,\n): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n scrypt(password, salt, keylen, options, (err, derivedKey) => {\n if (err) reject(err);\n else resolve(derivedKey);\n });\n });\n}\n\n/**\n * Hash a token using scrypt\n * @internal\n */\nasync function hashToken(token: string): Promise<string> {\n const salt = randomBytes(SALT_LENGTH);\n const hash = await scryptAsync(token, salt, KEY_LENGTH, SCRYPT_OPTIONS);\n\n // Store salt + hash together\n return `${salt.toString('base64')}.${hash.toString('base64')}`;\n}\n\n/**\n * Generate a cryptographically secure refresh token with embedded session ID\n *\n * @param id - The session ID to embed in the token\n * @returns Object containing the token and its hash\n * @internal\n */\nexport async function generateRefreshToken(id: string): Promise<{\n token: string;\n hash: string;\n}> {\n // Generate 32 bytes of random data\n const randomPart = randomBytes(32).toString('base64url');\n\n // Format: <id>.<random_bytes>\n const token = `${id}.${randomPart}`;\n const hash = await hashToken(token);\n\n return { token, hash };\n}\n\n/**\n * Extract the session ID from a refresh token\n *\n * @param token - The refresh token\n * @returns The session ID\n * @throws Error if token format is invalid\n * @internal\n */\nexport function getRefreshTokenId(token: string): string {\n if (!token || typeof token !== 'string') {\n throw new Error('Invalid refresh token format');\n }\n\n const parts = token.split('.');\n if (parts.length !== 2 || !parts[0] || !parts[1]) {\n throw new Error('Invalid refresh token format');\n }\n\n return parts[0];\n}\n\n/**\n * Verify a refresh token against a stored hash\n *\n * @param token - The refresh token to verify\n * @param storedHash - The stored hash (salt.hash format)\n * @returns true if token is valid, false otherwise\n * @internal\n */\nexport async function verifyRefreshToken(\n token: string,\n storedHash: string,\n): Promise<boolean> {\n try {\n const [saltBase64, hashBase64] = storedHash.split('.');\n if (!saltBase64 || !hashBase64) {\n return false;\n }\n\n const salt = Buffer.from(saltBase64, 'base64');\n const storedHashBuffer = Buffer.from(hashBase64, 'base64');\n\n const computedHash = await scryptAsync(\n token,\n salt,\n KEY_LENGTH,\n SCRYPT_OPTIONS,\n );\n\n // Use timing-safe comparison to prevent timing attacks\n return timingSafeEqual(storedHashBuffer, computedHash);\n } catch {\n return false;\n }\n}\n"],"names":["scrypt","randomBytes","timingSafeEqual"],"mappings":";;;;AAuBA,MAAM,WAAA,GAAc,EAAA;AACpB,MAAM,UAAA,GAAa,EAAA;AACnB,MAAM,iBAAgC,EAAE,CAAA,EAAG,OAAO,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AAE7D,SAAS,WAAA,CACP,QAAA,EACA,IAAA,EACA,MAAA,EACA,OAAA,EACiB;AACjB,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAAA,aAAA,CAAO,UAAU,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,CAAC,KAAK,UAAA,KAAe;AAC3D,MAAA,IAAI,GAAA,SAAY,GAAG,CAAA;AAAA,mBACN,UAAU,CAAA;AAAA,IACzB,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;AAMA,eAAe,UAAU,KAAA,EAAgC;AACvD,EAAA,MAAM,IAAA,GAAOC,mBAAY,WAAW,CAAA;AACpC,EAAA,MAAM,OAAO,MAAM,WAAA,CAAY,KAAA,EAAO,IAAA,EAAM,YAAY,cAAc,CAAA;AAGtE,EAAA,OAAO,CAAA,EAAG,KAAK,QAAA,CAAS,QAAQ,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAC,CAAA,CAAA;AAC9D;AASA,eAAsB,qBAAqB,EAAA,EAGxC;AAED,EAAA,MAAM,UAAA,GAAaA,kBAAA,CAAY,EAAE,CAAA,CAAE,SAAS,WAAW,CAAA;AAGvD,EAAA,MAAM,KAAA,GAAQ,CAAA,EAAG,EAAE,CAAA,CAAA,EAAI,UAAU,CAAA,CAAA;AACjC,EAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAU,KAAK,CAAA;AAElC,EAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AACvB;AAUO,SAAS,kBAAkB,KAAA,EAAuB;AACvD,EAAA,IAAI,CAAC,KAAA,IAAS,OAAO,KAAA,KAAU,QAAA,EAAU;AACvC,IAAA,MAAM,IAAI,MAAM,8BAA8B,CAAA;AAAA,EAChD;AAEA,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA;AAC7B,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,IAAK,CAAC,KAAA,CAAM,CAAC,CAAA,IAAK,CAAC,KAAA,CAAM,CAAC,CAAA,EAAG;AAChD,IAAA,MAAM,IAAI,MAAM,8BAA8B,CAAA;AAAA,EAChD;AAEA,EAAA,OAAO,MAAM,CAAC,CAAA;AAChB;AAUA,eAAsB,kBAAA,CACpB,OACA,UAAA,EACkB;AAClB,EAAA,IAAI;AACF,IAAA,MAAM,CAAC,UAAA,EAAY,UAAU,CAAA,GAAI,UAAA,CAAW,MAAM,GAAG,CAAA;AACrD,IAAA,IAAI,CAAC,UAAA,IAAc,CAAC,UAAA,EAAY;AAC9B,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,UAAA,EAAY,QAAQ,CAAA;AAC7C,IAAA,MAAM,gBAAA,GAAmB,MAAA,CAAO,IAAA,CAAK,UAAA,EAAY,QAAQ,CAAA;AAEzD,IAAA,MAAM,eAAe,MAAM,WAAA;AAAA,MACzB,KAAA;AAAA,MACA,IAAA;AAAA,MACA,UAAA;AAAA,MACA;AAAA,KACF;AAGA,IAAA,OAAOC,sBAAA,CAAgB,kBAAkB,YAAY,CAAA;AAAA,EACvD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF
|
|
1
|
+
{"version":3,"file":"refreshToken.cjs.js","sources":["../../src/lib/refreshToken.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 {\n randomBytes,\n scrypt,\n timingSafeEqual,\n ScryptOptions,\n} from 'node:crypto';\n\nconst SALT_LENGTH = 16;\nconst KEY_LENGTH = 64;\nconst SCRYPT_OPTIONS: ScryptOptions = { N: 16384, r: 8, p: 1 };\n\nfunction scryptAsync(\n password: string,\n salt: Buffer,\n keylen: number,\n options: ScryptOptions,\n): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n scrypt(password, salt, keylen, options, (err, derivedKey) => {\n if (err) reject(err);\n else resolve(derivedKey);\n });\n });\n}\n\n/**\n * Hash a token using scrypt\n * @internal\n */\nasync function hashToken(token: string): Promise<string> {\n const salt = randomBytes(SALT_LENGTH);\n const hash = await scryptAsync(token, salt, KEY_LENGTH, SCRYPT_OPTIONS);\n\n // Store salt + hash together\n return `${salt.toString('base64')}.${hash.toString('base64')}`;\n}\n\n/**\n * Generate a cryptographically secure refresh token with embedded session ID\n *\n * @param id - The session ID to embed in the token\n * @returns Object containing the token and its hash\n * @internal\n */\nexport async function generateRefreshToken(id: string): Promise<{\n token: string;\n hash: string;\n}> {\n // Generate 32 bytes of random data\n const randomPart = randomBytes(32).toString('base64url');\n\n // Format: <id>.<random_bytes>\n const token = `${id}.${randomPart}`;\n const hash = await hashToken(token);\n\n return { token, hash };\n}\n\n/**\n * Extract the session ID from a refresh token\n *\n * @param token - The refresh token\n * @returns The session ID\n * @throws Error if token format is invalid\n * @internal\n */\nexport function getRefreshTokenId(token: string): string {\n if (!token || typeof token !== 'string') {\n throw new Error('Invalid refresh token format');\n }\n\n const parts = token.split('.');\n if (parts.length !== 2 || !parts[0] || !parts[1]) {\n throw new Error('Invalid refresh token format');\n }\n\n return parts[0];\n}\n\n/**\n * Verify a refresh token against a stored hash\n *\n * @param token - The refresh token to verify\n * @param storedHash - The stored hash (salt.hash format)\n * @returns true if token is valid, false otherwise\n * @internal\n */\nexport async function verifyRefreshToken(\n token: string,\n storedHash: string,\n): Promise<boolean> {\n try {\n const [saltBase64, hashBase64] = storedHash.split('.');\n if (!saltBase64 || !hashBase64) {\n return false;\n }\n\n const salt = Buffer.from(saltBase64, 'base64');\n const storedHashBuffer = Buffer.from(hashBase64, 'base64');\n\n const computedHash = await scryptAsync(\n token,\n salt,\n KEY_LENGTH,\n SCRYPT_OPTIONS,\n );\n\n // Use timing-safe comparison to prevent timing attacks\n return timingSafeEqual(storedHashBuffer, computedHash);\n } catch {\n return false;\n }\n}\n"],"names":["scrypt","randomBytes","timingSafeEqual"],"mappings":";;;;AAuBA,MAAM,WAAA,GAAc,EAAA;AACpB,MAAM,UAAA,GAAa,EAAA;AACnB,MAAM,iBAAgC,EAAE,CAAA,EAAG,OAAO,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AAE7D,SAAS,WAAA,CACP,QAAA,EACA,IAAA,EACA,MAAA,EACA,OAAA,EACiB;AACjB,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAAA,aAAA,CAAO,UAAU,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,CAAC,KAAK,UAAA,KAAe;AAC3D,MAAA,IAAI,GAAA,SAAY,GAAG,CAAA;AAAA,mBACN,UAAU,CAAA;AAAA,IACzB,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;AAMA,eAAe,UAAU,KAAA,EAAgC;AACvD,EAAA,MAAM,IAAA,GAAOC,mBAAY,WAAW,CAAA;AACpC,EAAA,MAAM,OAAO,MAAM,WAAA,CAAY,KAAA,EAAO,IAAA,EAAM,YAAY,cAAc,CAAA;AAGtE,EAAA,OAAO,CAAA,EAAG,KAAK,QAAA,CAAS,QAAQ,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAC,CAAA,CAAA;AAC9D;AASA,eAAsB,qBAAqB,EAAA,EAGxC;AAED,EAAA,MAAM,UAAA,GAAaA,kBAAA,CAAY,EAAE,CAAA,CAAE,SAAS,WAAW,CAAA;AAGvD,EAAA,MAAM,KAAA,GAAQ,CAAA,EAAG,EAAE,CAAA,CAAA,EAAI,UAAU,CAAA,CAAA;AACjC,EAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAU,KAAK,CAAA;AAElC,EAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AACvB;AAUO,SAAS,kBAAkB,KAAA,EAAuB;AACvD,EAAA,IAAI,CAAC,KAAA,IAAS,OAAO,KAAA,KAAU,QAAA,EAAU;AACvC,IAAA,MAAM,IAAI,MAAM,8BAA8B,CAAA;AAAA,EAChD;AAEA,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA;AAC7B,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,IAAK,CAAC,KAAA,CAAM,CAAC,CAAA,IAAK,CAAC,KAAA,CAAM,CAAC,CAAA,EAAG;AAChD,IAAA,MAAM,IAAI,MAAM,8BAA8B,CAAA;AAAA,EAChD;AAEA,EAAA,OAAO,MAAM,CAAC,CAAA;AAChB;AAUA,eAAsB,kBAAA,CACpB,OACA,UAAA,EACkB;AAClB,EAAA,IAAI;AACF,IAAA,MAAM,CAAC,UAAA,EAAY,UAAU,CAAA,GAAI,UAAA,CAAW,MAAM,GAAG,CAAA;AACrD,IAAA,IAAI,CAAC,UAAA,IAAc,CAAC,UAAA,EAAY;AAC9B,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,UAAA,EAAY,QAAQ,CAAA;AAC7C,IAAA,MAAM,gBAAA,GAAmB,MAAA,CAAO,IAAA,CAAK,UAAA,EAAY,QAAQ,CAAA;AAEzD,IAAA,MAAM,eAAe,MAAM,WAAA;AAAA,MACzB,KAAA;AAAA,MACA,IAAA;AAAA,MACA,UAAA;AAAA,MACA;AAAA,KACF;AAGA,IAAA,OAAOC,sBAAA,CAAgB,kBAAkB,YAAY,CAAA;AAAA,EACvD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;;;;"}
|
|
@@ -81,9 +81,7 @@ class CatalogAuthResolverContext {
|
|
|
81
81
|
result = res.items;
|
|
82
82
|
} else if ("filter" in query) {
|
|
83
83
|
const filter = [query.filter].flat().map((value) => {
|
|
84
|
-
if (!Object.keys(value).some(
|
|
85
|
-
(key) => key.toLocaleLowerCase("en-US") === "kind"
|
|
86
|
-
)) {
|
|
84
|
+
if (!Object.keys(value).some((key) => key.toLowerCase() === "kind")) {
|
|
87
85
|
return {
|
|
88
86
|
...value,
|
|
89
87
|
kind: "user"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CatalogAuthResolverContext.cjs.js","sources":["../../../src/lib/resolvers/CatalogAuthResolverContext.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DEFAULT_NAMESPACE,\n Entity,\n parseEntityRef,\n RELATION_MEMBER_OF,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { ConflictError, InputError, NotFoundError } from '@backstage/errors';\nimport { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { TokenIssuer } from '../../identity/types';\nimport {\n AuthOwnershipResolver,\n AuthResolverCatalogUserQuery,\n AuthResolverContext,\n TokenParams,\n} from '@backstage/plugin-auth-node';\nimport { CatalogIdentityClient } from '../catalog/CatalogIdentityClient';\nimport { UserInfoDatabase } from '../../database/UserInfoDatabase';\n\nfunction getDefaultOwnershipEntityRefs(entity: Entity) {\n const membershipRefs =\n entity.relations\n ?.filter(\n r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'),\n )\n .map(r => r.targetRef) ?? [];\n\n return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs]));\n}\n\nexport class CatalogAuthResolverContext implements AuthResolverContext {\n static create(options: {\n logger: LoggerService;\n catalog: CatalogService;\n tokenIssuer: TokenIssuer;\n auth: AuthService;\n ownershipResolver?: AuthOwnershipResolver;\n userInfo: UserInfoDatabase;\n }): CatalogAuthResolverContext {\n const catalogIdentityClient = new CatalogIdentityClient({\n catalog: options.catalog,\n auth: options.auth,\n });\n\n return new CatalogAuthResolverContext(\n options.logger,\n options.tokenIssuer,\n catalogIdentityClient,\n options.catalog,\n options.auth,\n options.userInfo,\n options.ownershipResolver,\n );\n }\n\n public readonly logger: LoggerService;\n public readonly tokenIssuer: TokenIssuer;\n public readonly catalogIdentityClient: CatalogIdentityClient;\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly userInfo: UserInfoDatabase;\n private readonly ownershipResolver?: AuthOwnershipResolver;\n\n private constructor(\n logger: LoggerService,\n tokenIssuer: TokenIssuer,\n catalogIdentityClient: CatalogIdentityClient,\n catalog: CatalogService,\n auth: AuthService,\n userInfo: UserInfoDatabase,\n ownershipResolver?: AuthOwnershipResolver,\n ) {\n this.logger = logger;\n this.tokenIssuer = tokenIssuer;\n this.catalogIdentityClient = catalogIdentityClient;\n this.catalog = catalog;\n this.auth = auth;\n this.userInfo = userInfo;\n this.ownershipResolver = ownershipResolver;\n }\n\n async issueToken(params: TokenParams) {\n const { sub, ent = [sub], ...additionalClaims } = params.claims;\n const claims = {\n sub,\n ent,\n ...additionalClaims,\n };\n\n const issuedToken = await this.tokenIssuer.issueToken({\n claims,\n });\n\n // Store the user info in the database upon successful token\n // issuance so that it can be retrieved later by limited user tokens\n await this.userInfo.addUserInfo({\n claims,\n });\n\n return issuedToken;\n }\n\n async findCatalogUser(query: AuthResolverCatalogUserQuery) {\n let result: Entity[] | Entity | undefined = undefined;\n\n if ('entityRef' in query) {\n const entityRef = parseEntityRef(query.entityRef, {\n defaultKind: 'User',\n defaultNamespace: DEFAULT_NAMESPACE,\n });\n result = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n } else if ('annotations' in query) {\n const filter: Record<string, string> = {\n kind: 'user',\n };\n for (const [key, value] of Object.entries(query.annotations)) {\n filter[`metadata.annotations.${key}`] = value;\n }\n const res = await this.catalog.getEntities(\n { filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n result = res.items;\n } else if ('filter' in query) {\n const filter = [query.filter].flat().map(value => {\n if (\n !Object.keys(value).some(\n key => key.toLocaleLowerCase('en-US') === 'kind',\n )\n ) {\n return {\n ...value,\n kind: 'user',\n };\n }\n return value;\n });\n const res = await this.catalog.getEntities(\n { filter: filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n result = res.items;\n } else {\n throw new InputError('Invalid user lookup query');\n }\n\n if (Array.isArray(result)) {\n if (result.length > 1) {\n throw new ConflictError('User lookup resulted in multiple matches');\n }\n result = result[0];\n }\n if (!result) {\n throw new NotFoundError('User not found');\n }\n\n return { entity: result };\n }\n\n async signInWithCatalogUser(\n query: AuthResolverCatalogUserQuery,\n options?: {\n dangerousEntityRefFallback?: {\n entityRef:\n | string\n | {\n kind?: string;\n namespace?: string;\n name: string;\n };\n };\n },\n ) {\n try {\n const { entity } = await this.findCatalogUser(query);\n\n const { ownershipEntityRefs } = await this.resolveOwnershipEntityRefs(\n entity,\n );\n\n return await this.issueToken({\n claims: {\n sub: stringifyEntityRef(entity),\n ent: ownershipEntityRefs,\n },\n });\n } catch (error) {\n if (\n error?.name !== 'NotFoundError' ||\n !options?.dangerousEntityRefFallback\n ) {\n throw error;\n }\n const userEntityRef = stringifyEntityRef(\n parseEntityRef(options.dangerousEntityRefFallback.entityRef, {\n defaultKind: 'User',\n defaultNamespace: DEFAULT_NAMESPACE,\n }),\n );\n\n return await this.issueToken({\n claims: {\n sub: userEntityRef,\n ent: [userEntityRef],\n },\n });\n }\n }\n\n async resolveOwnershipEntityRefs(\n entity: Entity,\n ): Promise<{ ownershipEntityRefs: string[] }> {\n if (this.ownershipResolver) {\n return this.ownershipResolver.resolveOwnershipEntityRefs(entity);\n }\n return { ownershipEntityRefs: getDefaultOwnershipEntityRefs(entity) };\n }\n}\n"],"names":["RELATION_MEMBER_OF","stringifyEntityRef","CatalogIdentityClient","parseEntityRef","DEFAULT_NAMESPACE","InputError","ConflictError","NotFoundError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CatalogAuthResolverContext.cjs.js","sources":["../../../src/lib/resolvers/CatalogAuthResolverContext.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DEFAULT_NAMESPACE,\n Entity,\n parseEntityRef,\n RELATION_MEMBER_OF,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { ConflictError, InputError, NotFoundError } from '@backstage/errors';\nimport { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { TokenIssuer } from '../../identity/types';\nimport {\n AuthOwnershipResolver,\n AuthResolverCatalogUserQuery,\n AuthResolverContext,\n TokenParams,\n} from '@backstage/plugin-auth-node';\nimport { CatalogIdentityClient } from '../catalog/CatalogIdentityClient';\nimport { UserInfoDatabase } from '../../database/UserInfoDatabase';\n\nfunction getDefaultOwnershipEntityRefs(entity: Entity) {\n const membershipRefs =\n entity.relations\n ?.filter(\n r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'),\n )\n .map(r => r.targetRef) ?? [];\n\n return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs]));\n}\n\nexport class CatalogAuthResolverContext implements AuthResolverContext {\n static create(options: {\n logger: LoggerService;\n catalog: CatalogService;\n tokenIssuer: TokenIssuer;\n auth: AuthService;\n ownershipResolver?: AuthOwnershipResolver;\n userInfo: UserInfoDatabase;\n }): CatalogAuthResolverContext {\n const catalogIdentityClient = new CatalogIdentityClient({\n catalog: options.catalog,\n auth: options.auth,\n });\n\n return new CatalogAuthResolverContext(\n options.logger,\n options.tokenIssuer,\n catalogIdentityClient,\n options.catalog,\n options.auth,\n options.userInfo,\n options.ownershipResolver,\n );\n }\n\n public readonly logger: LoggerService;\n public readonly tokenIssuer: TokenIssuer;\n public readonly catalogIdentityClient: CatalogIdentityClient;\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly userInfo: UserInfoDatabase;\n private readonly ownershipResolver?: AuthOwnershipResolver;\n\n private constructor(\n logger: LoggerService,\n tokenIssuer: TokenIssuer,\n catalogIdentityClient: CatalogIdentityClient,\n catalog: CatalogService,\n auth: AuthService,\n userInfo: UserInfoDatabase,\n ownershipResolver?: AuthOwnershipResolver,\n ) {\n this.logger = logger;\n this.tokenIssuer = tokenIssuer;\n this.catalogIdentityClient = catalogIdentityClient;\n this.catalog = catalog;\n this.auth = auth;\n this.userInfo = userInfo;\n this.ownershipResolver = ownershipResolver;\n }\n\n async issueToken(params: TokenParams) {\n const { sub, ent = [sub], ...additionalClaims } = params.claims;\n const claims = {\n sub,\n ent,\n ...additionalClaims,\n };\n\n const issuedToken = await this.tokenIssuer.issueToken({\n claims,\n });\n\n // Store the user info in the database upon successful token\n // issuance so that it can be retrieved later by limited user tokens\n await this.userInfo.addUserInfo({\n claims,\n });\n\n return issuedToken;\n }\n\n async findCatalogUser(query: AuthResolverCatalogUserQuery) {\n let result: Entity[] | Entity | undefined = undefined;\n\n if ('entityRef' in query) {\n const entityRef = parseEntityRef(query.entityRef, {\n defaultKind: 'User',\n defaultNamespace: DEFAULT_NAMESPACE,\n });\n result = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n } else if ('annotations' in query) {\n const filter: Record<string, string> = {\n kind: 'user',\n };\n for (const [key, value] of Object.entries(query.annotations)) {\n filter[`metadata.annotations.${key}`] = value;\n }\n const res = await this.catalog.getEntities(\n { filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n result = res.items;\n } else if ('filter' in query) {\n const filter = [query.filter].flat().map(value => {\n if (!Object.keys(value).some(key => key.toLowerCase() === 'kind')) {\n return {\n ...value,\n kind: 'user',\n };\n }\n return value;\n });\n const res = await this.catalog.getEntities(\n { filter: filter },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n result = res.items;\n } else {\n throw new InputError('Invalid user lookup query');\n }\n\n if (Array.isArray(result)) {\n if (result.length > 1) {\n throw new ConflictError('User lookup resulted in multiple matches');\n }\n result = result[0];\n }\n if (!result) {\n throw new NotFoundError('User not found');\n }\n\n return { entity: result };\n }\n\n async signInWithCatalogUser(\n query: AuthResolverCatalogUserQuery,\n options?: {\n dangerousEntityRefFallback?: {\n entityRef:\n | string\n | {\n kind?: string;\n namespace?: string;\n name: string;\n };\n };\n },\n ) {\n try {\n const { entity } = await this.findCatalogUser(query);\n\n const { ownershipEntityRefs } = await this.resolveOwnershipEntityRefs(\n entity,\n );\n\n return await this.issueToken({\n claims: {\n sub: stringifyEntityRef(entity),\n ent: ownershipEntityRefs,\n },\n });\n } catch (error) {\n if (\n error?.name !== 'NotFoundError' ||\n !options?.dangerousEntityRefFallback\n ) {\n throw error;\n }\n const userEntityRef = stringifyEntityRef(\n parseEntityRef(options.dangerousEntityRefFallback.entityRef, {\n defaultKind: 'User',\n defaultNamespace: DEFAULT_NAMESPACE,\n }),\n );\n\n return await this.issueToken({\n claims: {\n sub: userEntityRef,\n ent: [userEntityRef],\n },\n });\n }\n }\n\n async resolveOwnershipEntityRefs(\n entity: Entity,\n ): Promise<{ ownershipEntityRefs: string[] }> {\n if (this.ownershipResolver) {\n return this.ownershipResolver.resolveOwnershipEntityRefs(entity);\n }\n return { ownershipEntityRefs: getDefaultOwnershipEntityRefs(entity) };\n }\n}\n"],"names":["RELATION_MEMBER_OF","stringifyEntityRef","CatalogIdentityClient","parseEntityRef","DEFAULT_NAMESPACE","InputError","ConflictError","NotFoundError"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.cjs.js","sources":["../../src/providers/router.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { NotFoundError, toError } from '@backstage/errors';\nimport {\n AuthOwnershipResolver,\n AuthProviderFactory,\n} from '@backstage/plugin-auth-node';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { Minimatch } from 'minimatch';\nimport { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext';\nimport { TokenIssuer } from '../identity/types';\nimport { UserInfoDatabase } from '../database/UserInfoDatabase';\n\nexport type ProviderFactories = { [s: string]: AuthProviderFactory };\n\nexport function bindProviderRouters(\n targetRouter: express.Router,\n options: {\n providers: ProviderFactories;\n appUrl: string;\n baseUrl: string;\n config: Config;\n logger: LoggerService;\n auth: AuthService;\n tokenIssuer: TokenIssuer;\n userInfo: UserInfoDatabase;\n ownershipResolver?: AuthOwnershipResolver;\n catalog: CatalogService;\n },\n) {\n const {\n providers,\n appUrl,\n baseUrl,\n config,\n logger,\n auth,\n tokenIssuer,\n catalog,\n ownershipResolver,\n userInfo,\n } = options;\n\n const providersConfig = config.getOptionalConfig('auth.providers');\n\n const isOriginAllowed = createOriginFilter(config);\n\n for (const [providerId, providerFactory] of Object.entries(providers)) {\n if (providersConfig?.has(providerId)) {\n logger.info(`Configuring auth provider: ${providerId}`);\n try {\n const provider = providerFactory({\n providerId,\n appUrl,\n baseUrl,\n isOriginAllowed,\n globalConfig: {\n baseUrl,\n appUrl,\n isOriginAllowed,\n },\n config: providersConfig.getConfig(providerId),\n logger,\n resolverContext: CatalogAuthResolverContext.create({\n logger,\n catalog,\n tokenIssuer,\n auth,\n ownershipResolver,\n userInfo,\n }),\n });\n\n const r = Router();\n\n r.get('/start', provider.start.bind(provider));\n r.get('/handler/frame', provider.frameHandler.bind(provider));\n r.post('/handler/frame', provider.frameHandler.bind(provider));\n if (provider.logout) {\n r.post('/logout', provider.logout.bind(provider));\n }\n if (provider.refresh) {\n r.get('/refresh', provider.refresh.bind(provider));\n r.post('/refresh', provider.refresh.bind(provider));\n }\n\n targetRouter.use(`/${providerId}`, r);\n } catch (e) {\n if (process.env.NODE_ENV !== 'development') {\n throw new Error(\n `Failed to initialize ${providerId} auth provider, ${\n toError(e).message\n }`,\n );\n }\n\n logger.warn(\n `Skipping ${providerId} auth provider, ${toError(e).message}`,\n );\n\n targetRouter.use(`/${providerId}`, () => {\n // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found.\n throw new NotFoundError(\n `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` +\n `auth.providers.${providerId} are missing or the environment variables used are not defined. ` +\n `Check the auth backend plugin logs when the backend starts to see more details.`,\n );\n });\n }\n } else {\n targetRouter.use(`/${providerId}`, () => {\n throw new NotFoundError(\n `No auth provider registered for '${providerId}'`,\n );\n });\n }\n }\n}\n\nexport function createOriginFilter(\n config: Config,\n): (origin: string) => boolean {\n const appUrl = config.getString('app.baseUrl');\n const { origin: appOrigin } = new URL(appUrl);\n\n const allowedOrigins = config.getOptionalStringArray(\n 'auth.experimentalExtraAllowedOrigins',\n );\n\n const allowedOriginPatterns =\n allowedOrigins?.map(\n pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }),\n ) ?? [];\n\n return origin => {\n if (origin === appOrigin) {\n return true;\n }\n return allowedOriginPatterns.some(pattern => pattern.match(origin));\n };\n}\n"],"names":["CatalogAuthResolverContext","Router","toError","NotFoundError","Minimatch"],"mappings":"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
|
|
1
|
+
{"version":3,"file":"router.cjs.js","sources":["../../src/providers/router.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { NotFoundError, toError } from '@backstage/errors';\nimport {\n AuthOwnershipResolver,\n AuthProviderFactory,\n} from '@backstage/plugin-auth-node';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { Minimatch } from 'minimatch';\nimport { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext';\nimport { TokenIssuer } from '../identity/types';\nimport { UserInfoDatabase } from '../database/UserInfoDatabase';\n\nexport type ProviderFactories = { [s: string]: AuthProviderFactory };\n\nexport function bindProviderRouters(\n targetRouter: express.Router,\n options: {\n providers: ProviderFactories;\n appUrl: string;\n baseUrl: string;\n config: Config;\n logger: LoggerService;\n auth: AuthService;\n tokenIssuer: TokenIssuer;\n userInfo: UserInfoDatabase;\n ownershipResolver?: AuthOwnershipResolver;\n catalog: CatalogService;\n },\n) {\n const {\n providers,\n appUrl,\n baseUrl,\n config,\n logger,\n auth,\n tokenIssuer,\n catalog,\n ownershipResolver,\n userInfo,\n } = options;\n\n const providersConfig = config.getOptionalConfig('auth.providers');\n\n const isOriginAllowed = createOriginFilter(config);\n\n for (const [providerId, providerFactory] of Object.entries(providers)) {\n if (providersConfig?.has(providerId)) {\n logger.info(`Configuring auth provider: ${providerId}`);\n try {\n const provider = providerFactory({\n providerId,\n appUrl,\n baseUrl,\n isOriginAllowed,\n globalConfig: {\n baseUrl,\n appUrl,\n isOriginAllowed,\n },\n config: providersConfig.getConfig(providerId),\n logger,\n resolverContext: CatalogAuthResolverContext.create({\n logger,\n catalog,\n tokenIssuer,\n auth,\n ownershipResolver,\n userInfo,\n }),\n });\n\n const r = Router();\n\n r.get('/start', provider.start.bind(provider));\n r.get('/handler/frame', provider.frameHandler.bind(provider));\n r.post('/handler/frame', provider.frameHandler.bind(provider));\n if (provider.logout) {\n r.post('/logout', provider.logout.bind(provider));\n }\n if (provider.refresh) {\n r.get('/refresh', provider.refresh.bind(provider));\n r.post('/refresh', provider.refresh.bind(provider));\n }\n\n targetRouter.use(`/${providerId}`, r);\n } catch (e) {\n if (process.env.NODE_ENV !== 'development') {\n throw new Error(\n `Failed to initialize ${providerId} auth provider, ${\n toError(e).message\n }`,\n );\n }\n\n logger.warn(\n `Skipping ${providerId} auth provider, ${toError(e).message}`,\n );\n\n targetRouter.use(`/${providerId}`, () => {\n // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found.\n throw new NotFoundError(\n `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` +\n `auth.providers.${providerId} are missing or the environment variables used are not defined. ` +\n `Check the auth backend plugin logs when the backend starts to see more details.`,\n );\n });\n }\n } else {\n targetRouter.use(`/${providerId}`, () => {\n throw new NotFoundError(\n `No auth provider registered for '${providerId}'`,\n );\n });\n }\n }\n}\n\nexport function createOriginFilter(\n config: Config,\n): (origin: string) => boolean {\n const appUrl = config.getString('app.baseUrl');\n const { origin: appOrigin } = new URL(appUrl);\n\n const allowedOrigins = config.getOptionalStringArray(\n 'auth.experimentalExtraAllowedOrigins',\n );\n\n const allowedOriginPatterns =\n allowedOrigins?.map(\n pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }),\n ) ?? [];\n\n return origin => {\n if (origin === appOrigin) {\n return true;\n }\n return allowedOriginPatterns.some(pattern => pattern.match(origin));\n };\n}\n"],"names":["CatalogAuthResolverContext","Router","toError","NotFoundError","Minimatch"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CimdClient.cjs.js","sources":["../../src/service/CimdClient.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 { InputError, isError } from '@backstage/errors';\nimport { lookup } from 'node:dns/promises';\nimport ipaddr from 'ipaddr.js';\n\nconst FETCH_TIMEOUT_MS = 10000;\nconst MAX_RESPONSE_BYTES = 64 * 1024;\n\n/** Auth methods that require a client secret - forbidden for CIMD clients */\nconst FORBIDDEN_AUTH_METHODS = [\n 'client_secret_basic',\n 'client_secret_post',\n 'client_secret_jwt',\n];\n\n/**\n * Raw metadata document from a CIMD URL.\n * Note: client_secret fields are included for validation (must NOT be present).\n */\ninterface CimdMetadata {\n client_id: string;\n client_name?: string;\n redirect_uris: string[];\n response_types?: string[];\n grant_types?: string[];\n scope?: string;\n token_endpoint_auth_method?: string;\n client_secret?: string;\n client_secret_expires_at?: number;\n}\n\n/** Validated CIMD client info */\nexport interface CimdClientInfo {\n clientId: string;\n clientName: string;\n redirectUris: string[];\n responseTypes: string[];\n grantTypes: string[];\n scope?: string;\n}\n\n/**\n * Validates and parses a CIMD URL per the IETF draft specification.\n * Requires HTTPS for production, but allows HTTP for localhost (development).\n *\n * @see https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/\n * @throws InputError if the URL is invalid per the CIMD spec\n */\nexport function validateCimdUrl(clientId: string): URL {\n // Per IETF draft: MUST NOT contain single-dot or double-dot path segments\n // Check before URL parsing since the URL constructor normalizes these away\n if (/\\/\\.\\.?(\\/|$)/.test(clientId)) {\n throw new InputError(\n 'Invalid client_id: path must not contain dot segments',\n );\n }\n\n let url: URL;\n try {\n url = new URL(clientId);\n } catch {\n throw new InputError('Invalid client_id: not a valid URL');\n }\n\n const isHttps = url.protocol === 'https:';\n const isLocalHttp =\n url.protocol === 'http:' &&\n (url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&\n process.env.NODE_ENV === 'development';\n\n if (!isHttps && !isLocalHttp) {\n throw new InputError(\n 'Invalid client_id: must use HTTPS (or HTTP for localhost in development)',\n );\n }\n\n if (url.pathname === '' || url.pathname === '/') {\n throw new InputError('Invalid client_id: must have a path component');\n }\n\n if (url.hash) {\n throw new InputError('Invalid client_id: must not contain a fragment');\n }\n\n if (url.username || url.password) {\n throw new InputError('Invalid client_id: must not contain credentials');\n }\n\n // Per IETF draft: SHOULD NOT include a query string\n // We reject this for stricter compliance and security\n if (url.search) {\n throw new InputError('Invalid client_id: must not contain a query string');\n }\n\n return url;\n}\n\n/**\n * SSRF (Server-Side Request Forgery) Protection\n *\n * When fetching CIMD metadata from client-provided URLs, we must prevent\n * attackers from tricking Backstage into accessing internal resources.\n * For example, an attacker could provide a URL that resolves to:\n * - 127.0.0.1 (localhost services)\n * - 10.x.x.x, 172.16-31.x.x, 192.168.x.x (internal network)\n * - Cloud metadata endpoints (169.254.169.254)\n *\n * We use ipaddr.js to check if resolved IPs are in non-public ranges.\n * Only 'unicast' (public internet) addresses are allowed.\n *\n * @see https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/\n * Section 5.1 - Security Considerations\n */\nfunction isNonPublicIp(ip: string): boolean {\n try {\n const addr = ipaddr.parse(ip);\n const range = addr.range();\n // Only allow public unicast addresses\n return range !== 'unicast';\n } catch {\n // If we can't parse the IP, treat it as non-public and block it\n return true;\n }\n}\n\nasync function validateHostNotPrivate(hostname: string): Promise<void> {\n try {\n const addresses = await lookup(hostname, { all: true });\n const nonPublicAddr = addresses.find(addr => isNonPublicIp(addr.address));\n if (nonPublicAddr) {\n throw new InputError('Invalid client_id URL');\n }\n } catch (error) {\n if (isError(error) && error.name === 'InputError') throw error;\n throw new InputError('Failed to fetch client metadata');\n }\n}\n\nfunction validateMetadata(\n metadata: CimdMetadata,\n expectedClientId: string,\n): void {\n if (metadata.client_id !== expectedClientId) {\n throw new InputError('Client ID mismatch in metadata document');\n }\n\n if (\n !Array.isArray(metadata.redirect_uris) ||\n metadata.redirect_uris.length === 0\n ) {\n throw new InputError('Metadata must include at least one redirect_uri');\n }\n\n for (const uri of metadata.redirect_uris) {\n if (!URL.canParse(uri)) {\n throw new InputError(`Invalid redirect_uri in metadata: ${uri}`);\n }\n }\n\n if (\n metadata.client_secret !== undefined ||\n metadata.client_secret_expires_at !== undefined\n ) {\n throw new InputError('Client metadata must not contain client_secret');\n }\n\n if (\n metadata.token_endpoint_auth_method &&\n FORBIDDEN_AUTH_METHODS.includes(metadata.token_endpoint_auth_method)\n ) {\n throw new InputError('Client metadata uses forbidden auth method');\n }\n}\n\nasync function readCappedResponseBody(response: Response): Promise<string> {\n if (!response.body) {\n return '';\n }\n\n const chunks: Buffer[] = [];\n let received = 0;\n for await (const chunk of response.body) {\n received += chunk.byteLength;\n if (received > MAX_RESPONSE_BYTES) {\n throw new InputError('Client metadata document too large');\n }\n chunks.push(Buffer.from(chunk));\n }\n\n return Buffer.concat(chunks).toString('utf8');\n}\n\n/**\n * Fetches and validates a CIMD metadata document.\n * @throws InputError if fetching or validation fails\n */\nexport async function fetchCimdMetadata(opts: {\n clientId: string;\n validatedUrl?: URL;\n skipSsrfCheck?: boolean;\n}): Promise<CimdClientInfo> {\n const url = opts.validatedUrl ?? validateCimdUrl(opts.clientId);\n\n // Skip SSRF validation for localhost in development, or when the caller\n // has determined the client_id matched an exact (non-wildcard) pattern.\n const isLocalhostDev =\n (url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&\n process.env.NODE_ENV === 'development';\n if (!isLocalhostDev && !opts.skipSsrfCheck) {\n await validateHostNotPrivate(url.hostname);\n }\n\n let response: Response;\n try {\n response = await fetch(url.toString(), {\n method: 'GET',\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n redirect: 'error',\n });\n } catch {\n throw new InputError('Failed to fetch client metadata');\n }\n\n if (!response.ok) {\n throw new InputError('Failed to fetch client metadata');\n }\n\n const contentLength = Number(response.headers.get('content-length'));\n if (contentLength > MAX_RESPONSE_BYTES) {\n throw new InputError('Client metadata document too large');\n }\n\n let metadata: CimdMetadata;\n try {\n const responseBody = await readCappedResponseBody(response);\n metadata = JSON.parse(responseBody) as CimdMetadata;\n } catch (error) {\n if (isError(error) && error.name === 'InputError') {\n throw error;\n }\n throw new InputError('Invalid client metadata document');\n }\n\n validateMetadata(metadata, opts.clientId);\n\n return {\n clientId: metadata.client_id,\n clientName: metadata.client_name || metadata.client_id,\n redirectUris: metadata.redirect_uris,\n responseTypes: metadata.response_types || ['code'],\n grantTypes: metadata.grant_types || ['authorization_code'],\n scope: metadata.scope,\n };\n}\n"],"names":["InputError","ipaddr","lookup","isError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CimdClient.cjs.js","sources":["../../src/service/CimdClient.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 { InputError, isError } from '@backstage/errors';\nimport { lookup } from 'node:dns/promises';\nimport ipaddr from 'ipaddr.js';\n\nconst FETCH_TIMEOUT_MS = 10000;\nconst MAX_RESPONSE_BYTES = 64 * 1024;\n\n/** Auth methods that require a client secret - forbidden for CIMD clients */\nconst FORBIDDEN_AUTH_METHODS = [\n 'client_secret_basic',\n 'client_secret_post',\n 'client_secret_jwt',\n];\n\n/**\n * Raw metadata document from a CIMD URL.\n * Note: client_secret fields are included for validation (must NOT be present).\n */\ninterface CimdMetadata {\n client_id: string;\n client_name?: string;\n redirect_uris: string[];\n response_types?: string[];\n grant_types?: string[];\n scope?: string;\n token_endpoint_auth_method?: string;\n client_secret?: string;\n client_secret_expires_at?: number;\n}\n\n/** Validated CIMD client info */\nexport interface CimdClientInfo {\n clientId: string;\n clientName: string;\n redirectUris: string[];\n responseTypes: string[];\n grantTypes: string[];\n scope?: string;\n}\n\n/**\n * Validates and parses a CIMD URL per the IETF draft specification.\n * Requires HTTPS for production, but allows HTTP for localhost (development).\n *\n * @see https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/\n * @throws InputError if the URL is invalid per the CIMD spec\n */\nexport function validateCimdUrl(clientId: string): URL {\n // Per IETF draft: MUST NOT contain single-dot or double-dot path segments\n // Check before URL parsing since the URL constructor normalizes these away\n if (/\\/\\.\\.?(\\/|$)/.test(clientId)) {\n throw new InputError(\n 'Invalid client_id: path must not contain dot segments',\n );\n }\n\n let url: URL;\n try {\n url = new URL(clientId);\n } catch {\n throw new InputError('Invalid client_id: not a valid URL');\n }\n\n const isHttps = url.protocol === 'https:';\n const isLocalHttp =\n url.protocol === 'http:' &&\n (url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&\n process.env.NODE_ENV === 'development';\n\n if (!isHttps && !isLocalHttp) {\n throw new InputError(\n 'Invalid client_id: must use HTTPS (or HTTP for localhost in development)',\n );\n }\n\n if (url.pathname === '' || url.pathname === '/') {\n throw new InputError('Invalid client_id: must have a path component');\n }\n\n if (url.hash) {\n throw new InputError('Invalid client_id: must not contain a fragment');\n }\n\n if (url.username || url.password) {\n throw new InputError('Invalid client_id: must not contain credentials');\n }\n\n // Per IETF draft: SHOULD NOT include a query string\n // We reject this for stricter compliance and security\n if (url.search) {\n throw new InputError('Invalid client_id: must not contain a query string');\n }\n\n return url;\n}\n\n/**\n * SSRF (Server-Side Request Forgery) Protection\n *\n * When fetching CIMD metadata from client-provided URLs, we must prevent\n * attackers from tricking Backstage into accessing internal resources.\n * For example, an attacker could provide a URL that resolves to:\n * - 127.0.0.1 (localhost services)\n * - 10.x.x.x, 172.16-31.x.x, 192.168.x.x (internal network)\n * - Cloud metadata endpoints (169.254.169.254)\n *\n * We use ipaddr.js to check if resolved IPs are in non-public ranges.\n * Only 'unicast' (public internet) addresses are allowed.\n *\n * @see https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/\n * Section 5.1 - Security Considerations\n */\nfunction isNonPublicIp(ip: string): boolean {\n try {\n const addr = ipaddr.parse(ip);\n const range = addr.range();\n // Only allow public unicast addresses\n return range !== 'unicast';\n } catch {\n // If we can't parse the IP, treat it as non-public and block it\n return true;\n }\n}\n\nasync function validateHostNotPrivate(hostname: string): Promise<void> {\n try {\n const addresses = await lookup(hostname, { all: true });\n const nonPublicAddr = addresses.find(addr => isNonPublicIp(addr.address));\n if (nonPublicAddr) {\n throw new InputError('Invalid client_id URL');\n }\n } catch (error) {\n if (isError(error) && error.name === 'InputError') throw error;\n throw new InputError('Failed to fetch client metadata');\n }\n}\n\nfunction validateMetadata(\n metadata: CimdMetadata,\n expectedClientId: string,\n): void {\n if (metadata.client_id !== expectedClientId) {\n throw new InputError('Client ID mismatch in metadata document');\n }\n\n if (\n !Array.isArray(metadata.redirect_uris) ||\n metadata.redirect_uris.length === 0\n ) {\n throw new InputError('Metadata must include at least one redirect_uri');\n }\n\n for (const uri of metadata.redirect_uris) {\n if (!URL.canParse(uri)) {\n throw new InputError(`Invalid redirect_uri in metadata: ${uri}`);\n }\n }\n\n if (\n metadata.client_secret !== undefined ||\n metadata.client_secret_expires_at !== undefined\n ) {\n throw new InputError('Client metadata must not contain client_secret');\n }\n\n if (\n metadata.token_endpoint_auth_method &&\n FORBIDDEN_AUTH_METHODS.includes(metadata.token_endpoint_auth_method)\n ) {\n throw new InputError('Client metadata uses forbidden auth method');\n }\n}\n\nasync function readCappedResponseBody(response: Response): Promise<string> {\n if (!response.body) {\n return '';\n }\n\n const chunks: Buffer[] = [];\n let received = 0;\n for await (const chunk of response.body) {\n received += chunk.byteLength;\n if (received > MAX_RESPONSE_BYTES) {\n throw new InputError('Client metadata document too large');\n }\n chunks.push(Buffer.from(chunk));\n }\n\n return Buffer.concat(chunks).toString('utf8');\n}\n\n/**\n * Fetches and validates a CIMD metadata document.\n * @throws InputError if fetching or validation fails\n */\nexport async function fetchCimdMetadata(opts: {\n clientId: string;\n validatedUrl?: URL;\n skipSsrfCheck?: boolean;\n}): Promise<CimdClientInfo> {\n const url = opts.validatedUrl ?? validateCimdUrl(opts.clientId);\n\n // Skip SSRF validation for localhost in development, or when the caller\n // has determined the client_id matched an exact (non-wildcard) pattern.\n const isLocalhostDev =\n (url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&\n process.env.NODE_ENV === 'development';\n if (!isLocalhostDev && !opts.skipSsrfCheck) {\n await validateHostNotPrivate(url.hostname);\n }\n\n let response: Response;\n try {\n response = await fetch(url.toString(), {\n method: 'GET',\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n redirect: 'error',\n });\n } catch {\n throw new InputError('Failed to fetch client metadata');\n }\n\n if (!response.ok) {\n throw new InputError('Failed to fetch client metadata');\n }\n\n const contentLength = Number(response.headers.get('content-length'));\n if (contentLength > MAX_RESPONSE_BYTES) {\n throw new InputError('Client metadata document too large');\n }\n\n let metadata: CimdMetadata;\n try {\n const responseBody = await readCappedResponseBody(response);\n metadata = JSON.parse(responseBody) as CimdMetadata;\n } catch (error) {\n if (isError(error) && error.name === 'InputError') {\n throw error;\n }\n throw new InputError('Invalid client metadata document');\n }\n\n validateMetadata(metadata, opts.clientId);\n\n return {\n clientId: metadata.client_id,\n clientName: metadata.client_name || metadata.client_id,\n redirectUris: metadata.redirect_uris,\n responseTypes: metadata.response_types || ['code'],\n grantTypes: metadata.grant_types || ['authorization_code'],\n scope: metadata.scope,\n };\n}\n"],"names":["InputError","ipaddr","lookup","isError"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OfflineAccessService.cjs.js","sources":["../../src/service/OfflineAccessService.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 {\n AuthService,\n DatabaseService,\n LifecycleService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError } from '@backstage/errors';\nimport { readDurationFromConfig } from '@backstage/config';\nimport { durationToMilliseconds } from '@backstage/types';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { randomUUID as uuid } from 'node:crypto';\nimport { OfflineSessionDatabase } from '../database/OfflineSessionDatabase';\nimport {\n generateRefreshToken,\n getRefreshTokenId,\n verifyRefreshToken,\n} from '../lib/refreshToken';\nimport { TokenIssuer } from '../identity/types';\n\n/**\n * Service for managing offline access (refresh tokens)\n * @internal\n */\nexport class OfflineAccessService {\n readonly #offlineSessionDb: OfflineSessionDatabase;\n readonly #logger: LoggerService;\n readonly #dangerouslyDisableCatalogPresenceCheck: boolean;\n readonly #catalog: CatalogService;\n readonly #auth: AuthService;\n\n static async create(options: {\n config: RootConfigService;\n database: DatabaseService;\n logger: LoggerService;\n lifecycle: LifecycleService;\n catalog: CatalogService;\n auth: AuthService;\n }): Promise<OfflineAccessService> {\n const { config, database, logger, lifecycle } = options;\n\n const tokenLifetime = config.has(\n 'auth.experimentalRefreshToken.tokenLifetime',\n )\n ? readDurationFromConfig(config, {\n key: 'auth.experimentalRefreshToken.tokenLifetime',\n })\n : { days: 30 };\n\n const maxRotationLifetime = config.has(\n 'auth.experimentalRefreshToken.maxRotationLifetime',\n )\n ? readDurationFromConfig(config, {\n key: 'auth.experimentalRefreshToken.maxRotationLifetime',\n })\n : { years: 1 };\n\n const tokenLifetimeSeconds = Math.floor(\n durationToMilliseconds(tokenLifetime) / 1000,\n );\n const maxRotationLifetimeSeconds = Math.floor(\n durationToMilliseconds(maxRotationLifetime) / 1000,\n );\n\n if (tokenLifetimeSeconds <= 0) {\n throw new Error(\n 'auth.experimentalRefreshToken.tokenLifetime must be a positive duration',\n );\n }\n if (maxRotationLifetimeSeconds <= 0) {\n throw new Error(\n 'auth.experimentalRefreshToken.maxRotationLifetime must be a positive duration',\n );\n }\n if (maxRotationLifetimeSeconds <= tokenLifetimeSeconds) {\n throw new Error(\n 'auth.experimentalRefreshToken.maxRotationLifetime must be greater than tokenLifetime',\n );\n }\n\n const maxTokensPerUser =\n config.getOptionalNumber(\n 'auth.experimentalRefreshToken.maxTokensPerUser',\n ) ?? 20;\n\n if (maxTokensPerUser <= 0) {\n throw new Error(\n 'auth.experimentalRefreshToken.maxTokensPerUser must be a positive number',\n );\n }\n\n const dangerouslyDisableCatalogPresenceCheck =\n config.getOptionalBoolean(\n 'auth.experimentalRefreshToken.dangerouslyDisableCatalogPresenceCheck',\n ) ?? false;\n\n const knex = await database.getClient();\n\n if (\n knex.client.config.client.includes('sqlite') ||\n knex.client.config.client.includes('better-sqlite')\n ) {\n logger.warn(\n 'Refresh tokens are enabled with SQLite, which does not support row-level locking. ' +\n 'Concurrent token rotation may not be fully protected against race conditions. ' +\n 'Use PostgreSQL for production deployments.',\n );\n }\n\n const offlineSessionDb = OfflineSessionDatabase.create({\n knex,\n tokenLifetimeSeconds,\n maxRotationLifetimeSeconds,\n maxTokensPerUser,\n });\n\n const cleanupIntervalMs = 60 * 60 * 1000;\n const cleanupInterval = setInterval(async () => {\n try {\n const deleted = await offlineSessionDb.cleanupExpiredSessions();\n if (deleted > 0) {\n logger.info(`Cleaned up ${deleted} expired offline sessions`);\n }\n } catch (error) {\n logger.error('Failed to cleanup expired offline sessions', error);\n }\n }, cleanupIntervalMs);\n cleanupInterval.unref();\n\n lifecycle.addShutdownHook(() => {\n clearInterval(cleanupInterval);\n });\n\n return new OfflineAccessService(\n offlineSessionDb,\n logger,\n dangerouslyDisableCatalogPresenceCheck,\n options.catalog,\n options.auth,\n );\n }\n\n private constructor(\n offlineSessionDb: OfflineSessionDatabase,\n logger: LoggerService,\n dangerouslyDisableCatalogPresenceCheck: boolean,\n catalog: CatalogService,\n auth: AuthService,\n ) {\n this.#offlineSessionDb = offlineSessionDb;\n this.#logger = logger;\n this.#dangerouslyDisableCatalogPresenceCheck =\n dangerouslyDisableCatalogPresenceCheck;\n this.#catalog = catalog;\n this.#auth = auth;\n }\n\n /**\n * Issue a new refresh token for a user\n */\n async issueRefreshToken(options: {\n userEntityRef: string;\n oidcClientId?: string;\n }): Promise<string> {\n const { userEntityRef, oidcClientId } = options;\n\n const sessionId = uuid();\n const { token, hash } = await generateRefreshToken(sessionId);\n\n await this.#offlineSessionDb.createSession({\n id: sessionId,\n userEntityRef,\n oidcClientId,\n tokenHash: hash,\n });\n\n this.#logger.debug(\n `Issued refresh token for user ${userEntityRef} with session ${sessionId}`,\n );\n\n return token;\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshAccessToken(options: {\n refreshToken: string;\n tokenIssuer: TokenIssuer;\n clientId?: string;\n }): Promise<{ accessToken: string; refreshToken: string }> {\n const { refreshToken, tokenIssuer, clientId } = options;\n\n let sessionId: string;\n try {\n sessionId = getRefreshTokenId(refreshToken);\n } catch (error) {\n this.#logger.debug('Failed to extract refresh token ID', error);\n throw new AuthenticationError('Invalid refresh token format');\n }\n\n const session = await this.#offlineSessionDb.getSessionById(sessionId);\n if (!session) {\n throw new AuthenticationError('Invalid refresh token');\n }\n\n if (this.#offlineSessionDb.isSessionExpired(session)) {\n await this.#offlineSessionDb.deleteSession(sessionId);\n throw new AuthenticationError('Invalid refresh token');\n }\n\n if (clientId && session.oidcClientId && clientId !== session.oidcClientId) {\n throw new AuthenticationError(\n 'Refresh token was not issued to this client',\n );\n }\n\n // Verify the caller actually holds a valid token, not just the session ID\n const isValid = await verifyRefreshToken(refreshToken, session.tokenHash);\n if (!isValid) {\n throw new AuthenticationError('Invalid refresh token');\n }\n\n if (!this.#dangerouslyDisableCatalogPresenceCheck) {\n try {\n const entity = await this.#catalog.getEntityByRef(\n session.userEntityRef,\n { credentials: await this.#auth.getOwnServiceCredentials() },\n );\n if (!entity) {\n this.#logger.info(\n `Rejecting refresh for user ${session.userEntityRef} - catalog entity not found, revoking session ${sessionId}`,\n );\n await this.#offlineSessionDb.deleteSession(sessionId);\n throw new AuthenticationError(\n 'User entity no longer exists in the catalog',\n );\n }\n } catch (error) {\n if (error.name === 'AuthenticationError') {\n throw error;\n }\n this.#logger.warn(\n `Failed to validate catalog user existence for ${session.userEntityRef}, rejecting refresh`,\n error,\n );\n throw new AuthenticationError('Unable to validate user existence');\n }\n }\n\n const { token: newRefreshToken, hash: newHash } =\n await generateRefreshToken(sessionId);\n\n // Atomically swap the hash so a concurrent request with the same token fails\n const rotatedSession = await this.#offlineSessionDb.getAndRotateToken(\n sessionId,\n session.tokenHash,\n newHash,\n );\n\n if (!rotatedSession) {\n throw new AuthenticationError('Invalid refresh token');\n }\n\n const { token: accessToken } = await tokenIssuer.issueToken({\n claims: {\n sub: rotatedSession.userEntityRef,\n },\n });\n\n this.#logger.debug(\n `Refreshed access token for user ${session.userEntityRef} with session ${sessionId}`,\n );\n\n return { accessToken, refreshToken: newRefreshToken };\n }\n\n /**\n * Revoke a refresh token\n */\n async revokeRefreshToken(refreshToken: string): Promise<void> {\n try {\n const sessionId = getRefreshTokenId(refreshToken);\n await this.#offlineSessionDb.deleteSession(sessionId);\n this.#logger.debug(`Revoked refresh token with session ${sessionId}`);\n } catch (error) {\n // Ignore errors when revoking - token may already be invalid\n this.#logger.debug('Failed to revoke refresh token', error);\n }\n }\n\n /**\n * Revoke all refresh tokens for a user\n */\n async revokeRefreshTokensByUserEntityRef(\n userEntityRef: string,\n ): Promise<void> {\n const deletedCount =\n await this.#offlineSessionDb.deleteSessionsByUserEntityRef(userEntityRef);\n this.#logger.debug(\n `Revoked ${deletedCount} refresh tokens for user ${userEntityRef}`,\n );\n }\n}\n"],"names":["config","readDurationFromConfig","durationToMilliseconds","OfflineSessionDatabase","uuid","generateRefreshToken","refreshToken","getRefreshTokenId","AuthenticationError","verifyRefreshToken"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OfflineAccessService.cjs.js","sources":["../../src/service/OfflineAccessService.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 {\n AuthService,\n DatabaseService,\n LifecycleService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError } from '@backstage/errors';\nimport { readDurationFromConfig } from '@backstage/config';\nimport { durationToMilliseconds } from '@backstage/types';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { randomUUID as uuid } from 'node:crypto';\nimport { OfflineSessionDatabase } from '../database/OfflineSessionDatabase';\nimport {\n generateRefreshToken,\n getRefreshTokenId,\n verifyRefreshToken,\n} from '../lib/refreshToken';\nimport { TokenIssuer } from '../identity/types';\n\n/**\n * Service for managing offline access (refresh tokens)\n * @internal\n */\nexport class OfflineAccessService {\n readonly #offlineSessionDb: OfflineSessionDatabase;\n readonly #logger: LoggerService;\n readonly #dangerouslyDisableCatalogPresenceCheck: boolean;\n readonly #catalog: CatalogService;\n readonly #auth: AuthService;\n\n static async create(options: {\n config: RootConfigService;\n database: DatabaseService;\n logger: LoggerService;\n lifecycle: LifecycleService;\n catalog: CatalogService;\n auth: AuthService;\n }): Promise<OfflineAccessService> {\n const { config, database, logger, lifecycle } = options;\n\n const tokenLifetime = config.has(\n 'auth.experimentalRefreshToken.tokenLifetime',\n )\n ? readDurationFromConfig(config, {\n key: 'auth.experimentalRefreshToken.tokenLifetime',\n })\n : { days: 30 };\n\n const maxRotationLifetime = config.has(\n 'auth.experimentalRefreshToken.maxRotationLifetime',\n )\n ? readDurationFromConfig(config, {\n key: 'auth.experimentalRefreshToken.maxRotationLifetime',\n })\n : { years: 1 };\n\n const tokenLifetimeSeconds = Math.floor(\n durationToMilliseconds(tokenLifetime) / 1000,\n );\n const maxRotationLifetimeSeconds = Math.floor(\n durationToMilliseconds(maxRotationLifetime) / 1000,\n );\n\n if (tokenLifetimeSeconds <= 0) {\n throw new Error(\n 'auth.experimentalRefreshToken.tokenLifetime must be a positive duration',\n );\n }\n if (maxRotationLifetimeSeconds <= 0) {\n throw new Error(\n 'auth.experimentalRefreshToken.maxRotationLifetime must be a positive duration',\n );\n }\n if (maxRotationLifetimeSeconds <= tokenLifetimeSeconds) {\n throw new Error(\n 'auth.experimentalRefreshToken.maxRotationLifetime must be greater than tokenLifetime',\n );\n }\n\n const maxTokensPerUser =\n config.getOptionalNumber(\n 'auth.experimentalRefreshToken.maxTokensPerUser',\n ) ?? 20;\n\n if (maxTokensPerUser <= 0) {\n throw new Error(\n 'auth.experimentalRefreshToken.maxTokensPerUser must be a positive number',\n );\n }\n\n const dangerouslyDisableCatalogPresenceCheck =\n config.getOptionalBoolean(\n 'auth.experimentalRefreshToken.dangerouslyDisableCatalogPresenceCheck',\n ) ?? false;\n\n const knex = await database.getClient();\n\n if (\n knex.client.config.client.includes('sqlite') ||\n knex.client.config.client.includes('better-sqlite')\n ) {\n logger.warn(\n 'Refresh tokens are enabled with SQLite, which does not support row-level locking. ' +\n 'Concurrent token rotation may not be fully protected against race conditions. ' +\n 'Use PostgreSQL for production deployments.',\n );\n }\n\n const offlineSessionDb = OfflineSessionDatabase.create({\n knex,\n tokenLifetimeSeconds,\n maxRotationLifetimeSeconds,\n maxTokensPerUser,\n });\n\n const cleanupIntervalMs = 60 * 60 * 1000;\n const cleanupInterval = setInterval(async () => {\n try {\n const deleted = await offlineSessionDb.cleanupExpiredSessions();\n if (deleted > 0) {\n logger.info(`Cleaned up ${deleted} expired offline sessions`);\n }\n } catch (error) {\n logger.error('Failed to cleanup expired offline sessions', error);\n }\n }, cleanupIntervalMs);\n cleanupInterval.unref();\n\n lifecycle.addShutdownHook(() => {\n clearInterval(cleanupInterval);\n });\n\n return new OfflineAccessService(\n offlineSessionDb,\n logger,\n dangerouslyDisableCatalogPresenceCheck,\n options.catalog,\n options.auth,\n );\n }\n\n private constructor(\n offlineSessionDb: OfflineSessionDatabase,\n logger: LoggerService,\n dangerouslyDisableCatalogPresenceCheck: boolean,\n catalog: CatalogService,\n auth: AuthService,\n ) {\n this.#offlineSessionDb = offlineSessionDb;\n this.#logger = logger;\n this.#dangerouslyDisableCatalogPresenceCheck =\n dangerouslyDisableCatalogPresenceCheck;\n this.#catalog = catalog;\n this.#auth = auth;\n }\n\n /**\n * Issue a new refresh token for a user\n */\n async issueRefreshToken(options: {\n userEntityRef: string;\n oidcClientId?: string;\n }): Promise<string> {\n const { userEntityRef, oidcClientId } = options;\n\n const sessionId = uuid();\n const { token, hash } = await generateRefreshToken(sessionId);\n\n await this.#offlineSessionDb.createSession({\n id: sessionId,\n userEntityRef,\n oidcClientId,\n tokenHash: hash,\n });\n\n this.#logger.debug(\n `Issued refresh token for user ${userEntityRef} with session ${sessionId}`,\n );\n\n return token;\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshAccessToken(options: {\n refreshToken: string;\n tokenIssuer: TokenIssuer;\n clientId?: string;\n }): Promise<{ accessToken: string; refreshToken: string }> {\n const { refreshToken, tokenIssuer, clientId } = options;\n\n let sessionId: string;\n try {\n sessionId = getRefreshTokenId(refreshToken);\n } catch (error) {\n this.#logger.debug('Failed to extract refresh token ID', error);\n throw new AuthenticationError('Invalid refresh token format');\n }\n\n const session = await this.#offlineSessionDb.getSessionById(sessionId);\n if (!session) {\n throw new AuthenticationError('Invalid refresh token');\n }\n\n if (this.#offlineSessionDb.isSessionExpired(session)) {\n await this.#offlineSessionDb.deleteSession(sessionId);\n throw new AuthenticationError('Invalid refresh token');\n }\n\n if (clientId && session.oidcClientId && clientId !== session.oidcClientId) {\n throw new AuthenticationError(\n 'Refresh token was not issued to this client',\n );\n }\n\n // Verify the caller actually holds a valid token, not just the session ID\n const isValid = await verifyRefreshToken(refreshToken, session.tokenHash);\n if (!isValid) {\n throw new AuthenticationError('Invalid refresh token');\n }\n\n if (!this.#dangerouslyDisableCatalogPresenceCheck) {\n try {\n const entity = await this.#catalog.getEntityByRef(\n session.userEntityRef,\n { credentials: await this.#auth.getOwnServiceCredentials() },\n );\n if (!entity) {\n this.#logger.info(\n `Rejecting refresh for user ${session.userEntityRef} - catalog entity not found, revoking session ${sessionId}`,\n );\n await this.#offlineSessionDb.deleteSession(sessionId);\n throw new AuthenticationError(\n 'User entity no longer exists in the catalog',\n );\n }\n } catch (error) {\n if (error.name === 'AuthenticationError') {\n throw error;\n }\n this.#logger.warn(\n `Failed to validate catalog user existence for ${session.userEntityRef}, rejecting refresh`,\n error,\n );\n throw new AuthenticationError('Unable to validate user existence');\n }\n }\n\n const { token: newRefreshToken, hash: newHash } =\n await generateRefreshToken(sessionId);\n\n // Atomically swap the hash so a concurrent request with the same token fails\n const rotatedSession = await this.#offlineSessionDb.getAndRotateToken(\n sessionId,\n session.tokenHash,\n newHash,\n );\n\n if (!rotatedSession) {\n throw new AuthenticationError('Invalid refresh token');\n }\n\n const { token: accessToken } = await tokenIssuer.issueToken({\n claims: {\n sub: rotatedSession.userEntityRef,\n },\n });\n\n this.#logger.debug(\n `Refreshed access token for user ${session.userEntityRef} with session ${sessionId}`,\n );\n\n return { accessToken, refreshToken: newRefreshToken };\n }\n\n /**\n * Revoke a refresh token\n */\n async revokeRefreshToken(refreshToken: string): Promise<void> {\n try {\n const sessionId = getRefreshTokenId(refreshToken);\n await this.#offlineSessionDb.deleteSession(sessionId);\n this.#logger.debug(`Revoked refresh token with session ${sessionId}`);\n } catch (error) {\n // Ignore errors when revoking - token may already be invalid\n this.#logger.debug('Failed to revoke refresh token', error);\n }\n }\n\n /**\n * Revoke all refresh tokens for a user\n */\n async revokeRefreshTokensByUserEntityRef(\n userEntityRef: string,\n ): Promise<void> {\n const deletedCount =\n await this.#offlineSessionDb.deleteSessionsByUserEntityRef(userEntityRef);\n this.#logger.debug(\n `Revoked ${deletedCount} refresh tokens for user ${userEntityRef}`,\n );\n }\n}\n"],"names":["config","readDurationFromConfig","durationToMilliseconds","OfflineSessionDatabase","uuid","generateRefreshToken","refreshToken","getRefreshTokenId","AuthenticationError","verifyRefreshToken"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OidcError.cjs.js","sources":["../../src/service/OidcError.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 { CustomErrorBase, isError } from '@backstage/errors';\nimport { Request, Response, NextFunction } from 'express';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport class OidcError extends CustomErrorBase {\n name = 'OidcError';\n\n readonly body: { error: string; error_description: string };\n readonly statusCode: number;\n\n constructor(\n errorCode: string,\n errorDescription: string,\n statusCode: number,\n cause?: Error | unknown,\n ) {\n super(`${errorCode}, ${errorDescription}`, cause);\n this.statusCode = statusCode;\n this.body = {\n error: errorCode,\n error_description: errorDescription,\n };\n }\n\n static isOidcError(error: unknown): error is OidcError {\n return isError(error) && error.name === 'OidcError';\n }\n\n static fromError(error: unknown): OidcError {\n if (OidcError.isOidcError(error)) {\n return error;\n }\n\n if (!isError(error)) {\n return new OidcError('server_error', 'Unknown error', 500, error);\n }\n\n const errorMessage = error.message || 'Unknown error';\n\n switch (error.name) {\n case 'InputError':\n return new OidcError('invalid_request', errorMessage, 400, error);\n case 'AuthenticationError':\n return new OidcError('invalid_client', errorMessage, 401, error);\n case 'NotAllowedError':\n return new OidcError('access_denied', errorMessage, 403, error);\n case 'NotFoundError':\n return new OidcError('invalid_request', errorMessage, 400, error);\n default:\n return new OidcError('server_error', errorMessage, 500, error);\n }\n }\n\n static middleware(\n logger: LoggerService,\n ): (err: unknown, _req: Request, res: Response, next: NextFunction) => void {\n return (\n err: unknown,\n _req: Request,\n res: Response,\n next: NextFunction,\n ): void => {\n if (OidcError.isOidcError(err)) {\n logger[err.statusCode >= 500 ? 'error' : 'info'](\n `OIDC Request failed with status ${err.statusCode}: ${err.body.error} - ${err.body.error_description}`,\n err.cause,\n );\n res.status(err.statusCode).json(err.body);\n return;\n }\n next(err);\n };\n }\n}\n"],"names":["CustomErrorBase","isError"],"mappings":";;;;AAmBO,MAAM,kBAAkBA,sBAAA,CAAgB;AAAA,EAC7C,IAAA,GAAO,WAAA;AAAA,EAEE,IAAA;AAAA,EACA,UAAA;AAAA,EAET,WAAA,CACE,SAAA,EACA,gBAAA,EACA,UAAA,EACA,KAAA,EACA;AACA,IAAA,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,EAAA,EAAK,gBAAgB,IAAI,KAAK,CAAA;AAChD,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,IAAA,GAAO;AAAA,MACV,KAAA,EAAO,SAAA;AAAA,MACP,iBAAA,EAAmB;AAAA,KACrB;AAAA,EACF;AAAA,EAEA,OAAO,YAAY,KAAA,EAAoC;AACrD,IAAA,OAAOC,cAAA,CAAQ,KAAK,CAAA,IAAK,KAAA,CAAM,IAAA,KAAS,WAAA;AAAA,EAC1C;AAAA,EAEA,OAAO,UAAU,KAAA,EAA2B;AAC1C,IAAA,IAAI,SAAA,CAAU,WAAA,CAAY,KAAK,CAAA,EAAG;AAChC,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IAAI,CAACA,cAAA,CAAQ,KAAK,CAAA,EAAG;AACnB,MAAA,OAAO,IAAI,SAAA,CAAU,cAAA,EAAgB,eAAA,EAAiB,KAAK,KAAK,CAAA;AAAA,IAClE;AAEA,IAAA,MAAM,YAAA,GAAe,MAAM,OAAA,IAAW,eAAA;AAEtC,IAAA,QAAQ,MAAM,IAAA;AAAM,MAClB,KAAK,YAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,iBAAA,EAAmB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MAClE,KAAK,qBAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,gBAAA,EAAkB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MACjE,KAAK,iBAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,eAAA,EAAiB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MAChE,KAAK,eAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,iBAAA,EAAmB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MAClE;AACE,QAAA,OAAO,IAAI,SAAA,CAAU,cAAA,EAAgB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA;AACjE,EACF;AAAA,EAEA,OAAO,WACL,MAAA,EAC0E;AAC1E,IAAA,OAAO,CACL,GAAA,EACA,IAAA,EACA,GAAA,EACA,IAAA,KACS;AACT,MAAA,IAAI,SAAA,CAAU,WAAA,CAAY,GAAG,CAAA,EAAG;AAC9B,QAAA,MAAA,CAAO,GAAA,CAAI,UAAA,IAAc,GAAA,GAAM,OAAA,GAAU,MAAM,CAAA;AAAA,UAC7C,CAAA,gCAAA,EAAmC,GAAA,CAAI,UAAU,CAAA,EAAA,EAAK,GAAA,CAAI,KAAK,KAAK,CAAA,GAAA,EAAM,GAAA,CAAI,IAAA,CAAK,iBAAiB,CAAA,CAAA;AAAA,UACpG,GAAA,CAAI;AAAA,SACN;AACA,QAAA,GAAA,CAAI,OAAO,GAAA,CAAI,UAAU,CAAA,CAAE,IAAA,CAAK,IAAI,IAAI,CAAA;AACxC,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,GAAG,CAAA;AAAA,IACV,CAAA;AAAA,EACF;AACF
|
|
1
|
+
{"version":3,"file":"OidcError.cjs.js","sources":["../../src/service/OidcError.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 { CustomErrorBase, isError } from '@backstage/errors';\nimport { Request, Response, NextFunction } from 'express';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport class OidcError extends CustomErrorBase {\n name = 'OidcError';\n\n readonly body: { error: string; error_description: string };\n readonly statusCode: number;\n\n constructor(\n errorCode: string,\n errorDescription: string,\n statusCode: number,\n cause?: Error | unknown,\n ) {\n super(`${errorCode}, ${errorDescription}`, cause);\n this.statusCode = statusCode;\n this.body = {\n error: errorCode,\n error_description: errorDescription,\n };\n }\n\n static isOidcError(error: unknown): error is OidcError {\n return isError(error) && error.name === 'OidcError';\n }\n\n static fromError(error: unknown): OidcError {\n if (OidcError.isOidcError(error)) {\n return error;\n }\n\n if (!isError(error)) {\n return new OidcError('server_error', 'Unknown error', 500, error);\n }\n\n const errorMessage = error.message || 'Unknown error';\n\n switch (error.name) {\n case 'InputError':\n return new OidcError('invalid_request', errorMessage, 400, error);\n case 'AuthenticationError':\n return new OidcError('invalid_client', errorMessage, 401, error);\n case 'NotAllowedError':\n return new OidcError('access_denied', errorMessage, 403, error);\n case 'NotFoundError':\n return new OidcError('invalid_request', errorMessage, 400, error);\n default:\n return new OidcError('server_error', errorMessage, 500, error);\n }\n }\n\n static middleware(\n logger: LoggerService,\n ): (err: unknown, _req: Request, res: Response, next: NextFunction) => void {\n return (\n err: unknown,\n _req: Request,\n res: Response,\n next: NextFunction,\n ): void => {\n if (OidcError.isOidcError(err)) {\n logger[err.statusCode >= 500 ? 'error' : 'info'](\n `OIDC Request failed with status ${err.statusCode}: ${err.body.error} - ${err.body.error_description}`,\n err.cause,\n );\n res.status(err.statusCode).json(err.body);\n return;\n }\n next(err);\n };\n }\n}\n"],"names":["CustomErrorBase","isError"],"mappings":";;;;AAmBO,MAAM,kBAAkBA,sBAAA,CAAgB;AAAA,EAC7C,IAAA,GAAO,WAAA;AAAA,EAEE,IAAA;AAAA,EACA,UAAA;AAAA,EAET,WAAA,CACE,SAAA,EACA,gBAAA,EACA,UAAA,EACA,KAAA,EACA;AACA,IAAA,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,EAAA,EAAK,gBAAgB,IAAI,KAAK,CAAA;AAChD,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,IAAA,GAAO;AAAA,MACV,KAAA,EAAO,SAAA;AAAA,MACP,iBAAA,EAAmB;AAAA,KACrB;AAAA,EACF;AAAA,EAEA,OAAO,YAAY,KAAA,EAAoC;AACrD,IAAA,OAAOC,cAAA,CAAQ,KAAK,CAAA,IAAK,KAAA,CAAM,IAAA,KAAS,WAAA;AAAA,EAC1C;AAAA,EAEA,OAAO,UAAU,KAAA,EAA2B;AAC1C,IAAA,IAAI,SAAA,CAAU,WAAA,CAAY,KAAK,CAAA,EAAG;AAChC,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IAAI,CAACA,cAAA,CAAQ,KAAK,CAAA,EAAG;AACnB,MAAA,OAAO,IAAI,SAAA,CAAU,cAAA,EAAgB,eAAA,EAAiB,KAAK,KAAK,CAAA;AAAA,IAClE;AAEA,IAAA,MAAM,YAAA,GAAe,MAAM,OAAA,IAAW,eAAA;AAEtC,IAAA,QAAQ,MAAM,IAAA;AAAM,MAClB,KAAK,YAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,iBAAA,EAAmB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MAClE,KAAK,qBAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,gBAAA,EAAkB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MACjE,KAAK,iBAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,eAAA,EAAiB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MAChE,KAAK,eAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,iBAAA,EAAmB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MAClE;AACE,QAAA,OAAO,IAAI,SAAA,CAAU,cAAA,EAAgB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA;AACjE,EACF;AAAA,EAEA,OAAO,WACL,MAAA,EAC0E;AAC1E,IAAA,OAAO,CACL,GAAA,EACA,IAAA,EACA,GAAA,EACA,IAAA,KACS;AACT,MAAA,IAAI,SAAA,CAAU,WAAA,CAAY,GAAG,CAAA,EAAG;AAC9B,QAAA,MAAA,CAAO,GAAA,CAAI,UAAA,IAAc,GAAA,GAAM,OAAA,GAAU,MAAM,CAAA;AAAA,UAC7C,CAAA,gCAAA,EAAmC,GAAA,CAAI,UAAU,CAAA,EAAA,EAAK,GAAA,CAAI,KAAK,KAAK,CAAA,GAAA,EAAM,GAAA,CAAI,IAAA,CAAK,iBAAiB,CAAA,CAAA;AAAA,UACpG,GAAA,CAAI;AAAA,SACN;AACA,QAAA,GAAA,CAAI,OAAO,GAAA,CAAI,UAAU,CAAA,CAAE,IAAA,CAAK,IAAI,IAAI,CAAA;AACxC,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,GAAG,CAAA;AAAA,IACV,CAAA;AAAA,EACF;AACF;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OidcRouter.cjs.js","sources":["../../src/service/OidcRouter.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport Router from 'express-promise-router';\nimport { OidcClientCredentials, OidcService } from './OidcService';\nimport { AuthenticationError, isError } from '@backstage/errors';\nimport {\n AuthService,\n HttpAuthService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport { TokenIssuer } from '../identity/types';\nimport { UserInfoDatabase } from '../database/UserInfoDatabase';\nimport { OidcDatabase } from '../database/OidcDatabase';\nimport { OfflineAccessService } from './OfflineAccessService';\nimport { json } from 'express';\nimport { z } from 'zod/v4';\nimport { fromZodError } from 'zod-validation-error/v4';\nimport { OidcError } from './OidcError';\n\nfunction ensureTrailingSlash(url: string): string {\n return url.endsWith('/') ? url : `${url}/`;\n}\n\nconst authorizeQuerySchema = z.object({\n client_id: z.string().min(1),\n redirect_uri: z.string().url(),\n response_type: z.string().min(1),\n scope: z.string().optional(),\n state: z.string().optional(),\n nonce: z.string().optional(),\n code_challenge: z.string().optional(),\n code_challenge_method: z.string().optional(),\n});\n\nconst sessionIdParamSchema = z.object({\n sessionId: z.string().min(1),\n});\n\nconst tokenRequestBodySchema = z.object({\n grant_type: z.string().min(1),\n code: z.string().optional(),\n redirect_uri: z.string().url().optional(),\n code_verifier: z.string().optional(),\n refresh_token: z.string().optional(),\n client_id: z.string().optional(),\n client_secret: z.string().optional(),\n});\n\nconst registerRequestBodySchema = z.object({\n client_name: z.string().optional(),\n redirect_uris: z.array(z.string().url()).min(1),\n response_types: z.array(z.string()).optional(),\n grant_types: z.array(z.string()).optional(),\n scope: z.string().optional(),\n});\n\nconst revokeRequestBodySchema = z.object({\n token: z.string().min(1),\n token_type_hint: z.string().optional(),\n client_id: z.string().optional(),\n client_secret: z.string().optional(),\n});\n\nfunction validateRequest<T>(schema: z.ZodSchema<T>, data: unknown): T {\n const parseResult = schema.safeParse(data);\n if (!parseResult.success) {\n const errorMessage = fromZodError(parseResult.error).message;\n throw new OidcError('invalid_request', errorMessage, 400);\n }\n return parseResult.data;\n}\n\nfunction parseClientCredentials(opts: {\n req: { headers: { authorization?: string } };\n bodyClientId?: string;\n bodyClientSecret?: string;\n}): Partial<OidcClientCredentials> {\n const { req, bodyClientId, bodyClientSecret } = opts;\n\n const basicAuth = req.headers.authorization?.match(/^Basic[ ]+([^\\s]+)$/i);\n if (basicAuth) {\n try {\n const decoded = Buffer.from(basicAuth[1], 'base64').toString('utf8');\n const idx = decoded.indexOf(':');\n if (idx >= 0) {\n const clientId = decoded.slice(0, idx);\n const clientSecret = decoded.slice(idx + 1);\n if (clientId && clientSecret) {\n return { clientId, clientSecret };\n }\n }\n } catch {\n /* ignore */\n }\n }\n\n return { clientId: bodyClientId, clientSecret: bodyClientSecret };\n}\n\nasync function authenticateClient(opts: {\n req: { headers: { authorization?: string } };\n oidc: OidcService;\n bodyClientId?: string;\n bodyClientSecret?: string;\n}): Promise<Required<OidcClientCredentials>> {\n const { req, oidc, bodyClientId, bodyClientSecret } = opts;\n const { clientId, clientSecret } = parseClientCredentials({\n req,\n bodyClientId,\n bodyClientSecret,\n });\n\n if (!clientId || !clientSecret) {\n throw new OidcError(\n 'invalid_client',\n 'Client authentication required',\n 401,\n );\n }\n\n try {\n const isValidClient = await oidc.verifyClientCredentials({\n clientId,\n clientSecret,\n });\n if (!isValidClient) {\n throw new OidcError('invalid_client', 'Invalid client credentials', 401);\n }\n } catch (e) {\n throw OidcError.fromError(e);\n }\n\n return { clientId, clientSecret };\n}\n\nexport class OidcRouter {\n private readonly oidc: OidcService;\n private readonly logger: LoggerService;\n private readonly auth: AuthService;\n private readonly appUrl: string;\n private readonly httpAuth: HttpAuthService;\n private readonly config: RootConfigService;\n private readonly baseUrl: string;\n\n private constructor(\n oidc: OidcService,\n logger: LoggerService,\n auth: AuthService,\n appUrl: string,\n httpAuth: HttpAuthService,\n config: RootConfigService,\n baseUrl: string,\n ) {\n this.oidc = oidc;\n this.logger = logger;\n this.auth = auth;\n this.appUrl = appUrl;\n this.httpAuth = httpAuth;\n this.config = config;\n this.baseUrl = baseUrl;\n }\n\n static create(options: {\n auth: AuthService;\n tokenIssuer: TokenIssuer;\n baseUrl: string;\n appUrl: string;\n logger: LoggerService;\n userInfo: UserInfoDatabase;\n oidc: OidcDatabase;\n httpAuth: HttpAuthService;\n config: RootConfigService;\n offlineAccess?: OfflineAccessService;\n }) {\n return new OidcRouter(\n OidcService.create(options),\n options.logger,\n options.auth,\n options.appUrl,\n options.httpAuth,\n options.config,\n options.baseUrl,\n );\n }\n\n public getRouter() {\n const router = Router();\n\n router.use(json());\n\n const cimdEnabled = this.oidc.isCimdEnabled();\n\n // OpenID Provider Configuration endpoint\n // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig\n // Returns the OpenID Provider Configuration document containing metadata about the provider\n router.get('/.well-known/openid-configuration', (_req, res) => {\n res.json(this.oidc.getConfiguration());\n });\n\n // JSON Web Key Set endpoint\n // https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.10.1.1\n // Returns the public keys used to verify JWTs issued by this provider\n router.get('/.well-known/jwks.json', async (_req, res) => {\n const { keys } = await this.oidc.listPublicKeys();\n res.json({ keys });\n });\n\n // CIMD metadata endpoint for the Backstage CLI\n // Automatically available when CIMD is enabled\n router.get('/.well-known/oauth-client/cli.json', (_req, res) => {\n if (!cimdEnabled) {\n res.status(404).json({\n error: 'not_found',\n error_description: 'Client ID metadata documents not enabled',\n });\n return;\n }\n\n res.json({\n client_id: `${this.baseUrl}/.well-known/oauth-client/cli.json`,\n client_name: 'Backstage CLI',\n redirect_uris: ['http://127.0.0.1:8055/callback'],\n response_types: ['code'],\n grant_types: ['authorization_code'],\n token_endpoint_auth_method: 'none',\n scope: 'openid offline_access',\n });\n });\n\n // UserInfo endpoint\n // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo\n // Returns claims about the authenticated user using an access token\n router.get('/v1/userinfo', async (req, res) => {\n const matches = req.headers.authorization?.match(/^Bearer[ ]+(\\S+)$/i);\n const token = matches?.[1];\n if (!token) {\n throw new AuthenticationError('No token provided');\n }\n\n const userInfo = await this.oidc.getUserInfo({ token });\n\n if (!userInfo) {\n res.status(404).send('User info not found');\n return;\n }\n\n res.json(userInfo);\n });\n\n const dcrEnabled =\n this.config.getOptionalBoolean(\n 'auth.experimentalDynamicClientRegistration.enabled',\n ) ?? false;\n\n if (dcrEnabled) {\n this.logger.warn(\n \"DEPRECATION WARNING: The 'auth.experimentalDynamicClientRegistration' configuration is deprecated. Migrate to Client ID Metadata Documents (CIMD) using 'auth.clientIdMetadataDocuments'.\",\n );\n }\n\n if (dcrEnabled || cimdEnabled) {\n // Authorization endpoint\n // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest\n // Handles the initial authorization request from the client, validates parameters,\n // and redirects to the Authorization Session page for user approval\n router.get('/v1/authorize', async (req, res) => {\n const {\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: responseType,\n scope,\n state,\n nonce,\n code_challenge: codeChallenge,\n code_challenge_method: codeChallengeMethod,\n } = validateRequest(authorizeQuerySchema, req.query);\n\n try {\n const result = await this.oidc.createAuthorizationSession({\n clientId,\n redirectUri,\n responseType,\n scope,\n state,\n nonce,\n codeChallenge,\n codeChallengeMethod,\n });\n\n // todo(blam): maybe this URL could be overridable by config if\n // the plugin is mounted somewhere else?\n // support slashes in baseUrl?\n const authSessionRedirectUrl = new URL(\n `./oauth2/authorize/${result.id}`,\n ensureTrailingSlash(this.appUrl),\n );\n\n return res.redirect(authSessionRedirectUrl.toString());\n } catch (error) {\n if (OidcError.isOidcError(error)) {\n const errorParams = new URLSearchParams();\n errorParams.append('error', error.body.error);\n errorParams.append(\n 'error_description',\n error.body.error_description,\n );\n if (state) {\n errorParams.append('state', state);\n }\n\n const redirectUrl = new URL(redirectUri);\n redirectUrl.search = errorParams.toString();\n return res.redirect(redirectUrl.toString());\n }\n throw error;\n }\n });\n\n // Authorization Session request details endpoint\n // Returns Authorization Session request details for the frontend\n router.get('/v1/sessions/:sessionId', async (req, res) => {\n const { sessionId } = validateRequest(sessionIdParamSchema, req.params);\n\n try {\n const session = await this.oidc.getAuthorizationSession({\n sessionId,\n });\n\n return res.json({\n id: session.id,\n clientId: session.clientId,\n clientName: session.clientName,\n scope: session.scope,\n redirectUri: session.redirectUri,\n });\n } catch (error) {\n throw OidcError.fromError(error);\n }\n });\n\n // Authorization Session approval endpoint\n // Handles user approval of Authorization Session requests and generates authorization codes\n router.post('/v1/sessions/:sessionId/approve', async (req, res) => {\n const { sessionId } = validateRequest(sessionIdParamSchema, req.params);\n\n try {\n const httpCredentials = await this.httpAuth.credentials(req);\n\n if (!this.auth.isPrincipal(httpCredentials, 'user')) {\n throw new OidcError(\n 'access_denied',\n 'Authentication required',\n 403,\n );\n }\n\n const { userEntityRef } = httpCredentials.principal;\n\n const result = await this.oidc.approveAuthorizationSession({\n sessionId,\n userEntityRef,\n });\n\n return res.json({\n redirectUrl: result.redirectUrl,\n });\n } catch (error) {\n throw OidcError.fromError(error);\n }\n });\n\n // Authorization Session rejection endpoint\n // Handles user rejection of Authorization Session requests and redirects with error\n router.post('/v1/sessions/:sessionId/reject', async (req, res) => {\n const { sessionId } = validateRequest(sessionIdParamSchema, req.params);\n\n const httpCredentials = await this.httpAuth.credentials(req);\n\n if (!this.auth.isPrincipal(httpCredentials, 'user')) {\n throw new OidcError('access_denied', 'Authentication required', 403);\n }\n\n const { userEntityRef } = httpCredentials.principal;\n try {\n const session = await this.oidc.getAuthorizationSession({\n sessionId,\n });\n\n await this.oidc.rejectAuthorizationSession({\n sessionId,\n userEntityRef,\n });\n\n const errorParams = new URLSearchParams();\n errorParams.append('error', 'access_denied');\n errorParams.append('error_description', 'User denied the request');\n if (session.state) {\n errorParams.append('state', session.state);\n }\n\n const redirectUrl = new URL(session.redirectUri);\n redirectUrl.search = errorParams.toString();\n\n return res.json({\n redirectUrl: redirectUrl.toString(),\n });\n } catch (error) {\n throw OidcError.fromError(error);\n }\n });\n\n // Token endpoint\n // https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest\n // Exchanges authorization codes for access tokens and ID tokens\n // Also handles refresh token grant type\n router.post('/v1/token', async (req, res) => {\n const {\n grant_type: grantType,\n code,\n redirect_uri: redirectUri,\n code_verifier: codeVerifier,\n refresh_token: refreshToken,\n client_id: bodyClientId,\n client_secret: bodyClientSecret,\n } = validateRequest(tokenRequestBodySchema, req.body);\n\n try {\n // Handle authorization_code grant type\n if (grantType === 'authorization_code') {\n if (!code || !redirectUri) {\n throw new OidcError(\n 'invalid_request',\n 'Missing code or redirect_uri parameters for authorization_code grant',\n 400,\n );\n }\n\n const result = await this.oidc.exchangeCodeForToken({\n code,\n redirectUri,\n codeVerifier,\n grantType,\n });\n\n return res.json({\n access_token: result.accessToken,\n token_type: result.tokenType,\n expires_in: result.expiresIn,\n id_token: result.idToken,\n scope: result.scope,\n ...(result.refreshToken && {\n refresh_token: result.refreshToken,\n }),\n });\n }\n\n // Handle refresh_token grant type\n if (grantType === 'refresh_token') {\n if (!refreshToken) {\n throw new OidcError(\n 'invalid_request',\n 'Missing refresh_token parameter for refresh_token grant',\n 400,\n );\n }\n\n // Authenticate if credentials are provided via Basic auth or body\n const hasCredentials =\n req.headers.authorization?.match(/^Basic[ ]+([^\\s]+)$/i) ||\n (bodyClientId && bodyClientSecret);\n\n let authenticatedClientId: string | undefined;\n if (hasCredentials) {\n const { clientId: authedId } = await authenticateClient({\n req,\n oidc: this.oidc,\n bodyClientId,\n bodyClientSecret,\n });\n authenticatedClientId = authedId;\n }\n\n const result = await this.oidc.refreshAccessToken({\n refreshToken,\n clientId: authenticatedClientId,\n });\n\n return res.json({\n access_token: result.accessToken,\n token_type: result.tokenType,\n expires_in: result.expiresIn,\n refresh_token: result.refreshToken,\n });\n }\n\n // Unsupported grant type\n throw new OidcError(\n 'unsupported_grant_type',\n `Grant type ${grantType} is not supported`,\n 400,\n );\n } catch (error) {\n // Invalid auth codes and refresh tokens should be invalid_grant, not invalid_client.\n // Client auth failures are already thrown as OidcError by authenticateClient.\n if (isError(error) && error.name === 'AuthenticationError') {\n throw new OidcError('invalid_grant', error.message, 400, error);\n }\n throw OidcError.fromError(error);\n }\n });\n\n // Token Revocation endpoint (RFC 7009-like)\n // Allows clients to revoke refresh tokens. DCR clients are confidential\n // and must authenticate with their client secret, while CIMD clients\n // are public clients that only identify themselves with their client ID\n router.post('/v1/revoke', async (req, res) => {\n try {\n const {\n token,\n client_id: bodyClientId,\n client_secret: bodyClientSecret,\n } = validateRequest(revokeRequestBodySchema, req.body ?? {});\n\n const { clientId, clientSecret } = parseClientCredentials({\n req,\n bodyClientId,\n bodyClientSecret,\n });\n\n if (!clientId) {\n throw new OidcError(\n 'invalid_client',\n 'Client authentication required',\n 401,\n );\n }\n\n const isValidClient = await this.oidc.verifyRevocationClient({\n clientId,\n clientSecret,\n });\n if (!isValidClient) {\n throw new OidcError(\n 'invalid_client',\n 'Invalid client credentials',\n 401,\n );\n }\n\n try {\n await this.oidc.revokeRefreshToken(token);\n } catch (e) {\n this.logger.error('Failed to revoke token', e);\n }\n\n return res.status(200).send('');\n } catch (e) {\n throw OidcError.fromError(e);\n }\n });\n }\n\n // Dynamic Client Registration endpoint - only available when DCR is enabled\n if (dcrEnabled) {\n // https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration\n // Allows clients to register themselves dynamically with the provider\n router.post('/v1/register', async (req, res) => {\n const {\n client_name: clientName,\n redirect_uris: redirectUris,\n response_types: responseTypes,\n grant_types: grantTypes,\n scope,\n } = validateRequest(registerRequestBodySchema, req.body);\n\n try {\n const client = await this.oidc.registerClient({\n clientName: clientName ?? 'Backstage CLI',\n redirectUris,\n responseTypes,\n grantTypes,\n scope,\n });\n\n return res.status(201).json({\n client_id: client.clientId,\n redirect_uris: client.redirectUris,\n client_secret: client.clientSecret,\n });\n } catch (e) {\n throw OidcError.fromError(e);\n }\n });\n }\n\n router.use(OidcError.middleware(this.logger));\n\n return router;\n }\n}\n"],"names":["z","fromZodError","OidcError","OidcService","Router","json","AuthenticationError","isError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OidcRouter.cjs.js","sources":["../../src/service/OidcRouter.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport Router from 'express-promise-router';\nimport { OidcClientCredentials, OidcService } from './OidcService';\nimport { AuthenticationError, isError } from '@backstage/errors';\nimport {\n AuthService,\n HttpAuthService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport { TokenIssuer } from '../identity/types';\nimport { UserInfoDatabase } from '../database/UserInfoDatabase';\nimport { OidcDatabase } from '../database/OidcDatabase';\nimport { OfflineAccessService } from './OfflineAccessService';\nimport { json } from 'express';\nimport { z } from 'zod/v4';\nimport { fromZodError } from 'zod-validation-error/v4';\nimport { OidcError } from './OidcError';\n\nfunction ensureTrailingSlash(url: string): string {\n return url.endsWith('/') ? url : `${url}/`;\n}\n\nconst authorizeQuerySchema = z.object({\n client_id: z.string().min(1),\n redirect_uri: z.string().url(),\n response_type: z.string().min(1),\n scope: z.string().optional(),\n state: z.string().optional(),\n nonce: z.string().optional(),\n code_challenge: z.string().optional(),\n code_challenge_method: z.string().optional(),\n});\n\nconst sessionIdParamSchema = z.object({\n sessionId: z.string().min(1),\n});\n\nconst tokenRequestBodySchema = z.object({\n grant_type: z.string().min(1),\n code: z.string().optional(),\n redirect_uri: z.string().url().optional(),\n code_verifier: z.string().optional(),\n refresh_token: z.string().optional(),\n client_id: z.string().optional(),\n client_secret: z.string().optional(),\n});\n\nconst registerRequestBodySchema = z.object({\n client_name: z.string().optional(),\n redirect_uris: z.array(z.string().url()).min(1),\n response_types: z.array(z.string()).optional(),\n grant_types: z.array(z.string()).optional(),\n scope: z.string().optional(),\n});\n\nconst revokeRequestBodySchema = z.object({\n token: z.string().min(1),\n token_type_hint: z.string().optional(),\n client_id: z.string().optional(),\n client_secret: z.string().optional(),\n});\n\nfunction validateRequest<T>(schema: z.ZodSchema<T>, data: unknown): T {\n const parseResult = schema.safeParse(data);\n if (!parseResult.success) {\n const errorMessage = fromZodError(parseResult.error).message;\n throw new OidcError('invalid_request', errorMessage, 400);\n }\n return parseResult.data;\n}\n\nfunction parseClientCredentials(opts: {\n req: { headers: { authorization?: string } };\n bodyClientId?: string;\n bodyClientSecret?: string;\n}): Partial<OidcClientCredentials> {\n const { req, bodyClientId, bodyClientSecret } = opts;\n\n const basicAuth = req.headers.authorization?.match(/^Basic[ ]+([^\\s]+)$/i);\n if (basicAuth) {\n try {\n const decoded = Buffer.from(basicAuth[1], 'base64').toString('utf8');\n const idx = decoded.indexOf(':');\n if (idx >= 0) {\n const clientId = decoded.slice(0, idx);\n const clientSecret = decoded.slice(idx + 1);\n if (clientId && clientSecret) {\n return { clientId, clientSecret };\n }\n }\n } catch {\n /* ignore */\n }\n }\n\n return { clientId: bodyClientId, clientSecret: bodyClientSecret };\n}\n\nasync function authenticateClient(opts: {\n req: { headers: { authorization?: string } };\n oidc: OidcService;\n bodyClientId?: string;\n bodyClientSecret?: string;\n}): Promise<Required<OidcClientCredentials>> {\n const { req, oidc, bodyClientId, bodyClientSecret } = opts;\n const { clientId, clientSecret } = parseClientCredentials({\n req,\n bodyClientId,\n bodyClientSecret,\n });\n\n if (!clientId || !clientSecret) {\n throw new OidcError(\n 'invalid_client',\n 'Client authentication required',\n 401,\n );\n }\n\n try {\n const isValidClient = await oidc.verifyClientCredentials({\n clientId,\n clientSecret,\n });\n if (!isValidClient) {\n throw new OidcError('invalid_client', 'Invalid client credentials', 401);\n }\n } catch (e) {\n throw OidcError.fromError(e);\n }\n\n return { clientId, clientSecret };\n}\n\nexport class OidcRouter {\n private readonly oidc: OidcService;\n private readonly logger: LoggerService;\n private readonly auth: AuthService;\n private readonly appUrl: string;\n private readonly httpAuth: HttpAuthService;\n private readonly config: RootConfigService;\n private readonly baseUrl: string;\n\n private constructor(\n oidc: OidcService,\n logger: LoggerService,\n auth: AuthService,\n appUrl: string,\n httpAuth: HttpAuthService,\n config: RootConfigService,\n baseUrl: string,\n ) {\n this.oidc = oidc;\n this.logger = logger;\n this.auth = auth;\n this.appUrl = appUrl;\n this.httpAuth = httpAuth;\n this.config = config;\n this.baseUrl = baseUrl;\n }\n\n static create(options: {\n auth: AuthService;\n tokenIssuer: TokenIssuer;\n baseUrl: string;\n appUrl: string;\n logger: LoggerService;\n userInfo: UserInfoDatabase;\n oidc: OidcDatabase;\n httpAuth: HttpAuthService;\n config: RootConfigService;\n offlineAccess?: OfflineAccessService;\n }) {\n return new OidcRouter(\n OidcService.create(options),\n options.logger,\n options.auth,\n options.appUrl,\n options.httpAuth,\n options.config,\n options.baseUrl,\n );\n }\n\n public getRouter() {\n const router = Router();\n\n router.use(json());\n\n const cimdEnabled = this.oidc.isCimdEnabled();\n\n // OpenID Provider Configuration endpoint\n // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig\n // Returns the OpenID Provider Configuration document containing metadata about the provider\n router.get('/.well-known/openid-configuration', (_req, res) => {\n res.json(this.oidc.getConfiguration());\n });\n\n // JSON Web Key Set endpoint\n // https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.10.1.1\n // Returns the public keys used to verify JWTs issued by this provider\n router.get('/.well-known/jwks.json', async (_req, res) => {\n const { keys } = await this.oidc.listPublicKeys();\n res.json({ keys });\n });\n\n // CIMD metadata endpoint for the Backstage CLI\n // Automatically available when CIMD is enabled\n router.get('/.well-known/oauth-client/cli.json', (_req, res) => {\n if (!cimdEnabled) {\n res.status(404).json({\n error: 'not_found',\n error_description: 'Client ID metadata documents not enabled',\n });\n return;\n }\n\n res.json({\n client_id: `${this.baseUrl}/.well-known/oauth-client/cli.json`,\n client_name: 'Backstage CLI',\n redirect_uris: ['http://127.0.0.1:8055/callback'],\n response_types: ['code'],\n grant_types: ['authorization_code'],\n token_endpoint_auth_method: 'none',\n scope: 'openid offline_access',\n });\n });\n\n // UserInfo endpoint\n // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo\n // Returns claims about the authenticated user using an access token\n router.get('/v1/userinfo', async (req, res) => {\n const matches = req.headers.authorization?.match(/^Bearer[ ]+(\\S+)$/i);\n const token = matches?.[1];\n if (!token) {\n throw new AuthenticationError('No token provided');\n }\n\n const userInfo = await this.oidc.getUserInfo({ token });\n\n if (!userInfo) {\n res.status(404).send('User info not found');\n return;\n }\n\n res.json(userInfo);\n });\n\n const dcrEnabled =\n this.config.getOptionalBoolean(\n 'auth.experimentalDynamicClientRegistration.enabled',\n ) ?? false;\n\n if (dcrEnabled) {\n this.logger.warn(\n \"DEPRECATION WARNING: The 'auth.experimentalDynamicClientRegistration' configuration is deprecated. Migrate to Client ID Metadata Documents (CIMD) using 'auth.clientIdMetadataDocuments'.\",\n );\n }\n\n if (dcrEnabled || cimdEnabled) {\n // Authorization endpoint\n // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest\n // Handles the initial authorization request from the client, validates parameters,\n // and redirects to the Authorization Session page for user approval\n router.get('/v1/authorize', async (req, res) => {\n const {\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: responseType,\n scope,\n state,\n nonce,\n code_challenge: codeChallenge,\n code_challenge_method: codeChallengeMethod,\n } = validateRequest(authorizeQuerySchema, req.query);\n\n try {\n const result = await this.oidc.createAuthorizationSession({\n clientId,\n redirectUri,\n responseType,\n scope,\n state,\n nonce,\n codeChallenge,\n codeChallengeMethod,\n });\n\n // todo(blam): maybe this URL could be overridable by config if\n // the plugin is mounted somewhere else?\n // support slashes in baseUrl?\n const authSessionRedirectUrl = new URL(\n `./oauth2/authorize/${result.id}`,\n ensureTrailingSlash(this.appUrl),\n );\n\n return res.redirect(authSessionRedirectUrl.toString());\n } catch (error) {\n if (OidcError.isOidcError(error)) {\n const errorParams = new URLSearchParams();\n errorParams.append('error', error.body.error);\n errorParams.append(\n 'error_description',\n error.body.error_description,\n );\n if (state) {\n errorParams.append('state', state);\n }\n\n const redirectUrl = new URL(redirectUri);\n redirectUrl.search = errorParams.toString();\n return res.redirect(redirectUrl.toString());\n }\n throw error;\n }\n });\n\n // Authorization Session request details endpoint\n // Returns Authorization Session request details for the frontend\n router.get('/v1/sessions/:sessionId', async (req, res) => {\n const { sessionId } = validateRequest(sessionIdParamSchema, req.params);\n\n try {\n const session = await this.oidc.getAuthorizationSession({\n sessionId,\n });\n\n return res.json({\n id: session.id,\n clientId: session.clientId,\n clientName: session.clientName,\n scope: session.scope,\n redirectUri: session.redirectUri,\n });\n } catch (error) {\n throw OidcError.fromError(error);\n }\n });\n\n // Authorization Session approval endpoint\n // Handles user approval of Authorization Session requests and generates authorization codes\n router.post('/v1/sessions/:sessionId/approve', async (req, res) => {\n const { sessionId } = validateRequest(sessionIdParamSchema, req.params);\n\n try {\n const httpCredentials = await this.httpAuth.credentials(req);\n\n if (!this.auth.isPrincipal(httpCredentials, 'user')) {\n throw new OidcError(\n 'access_denied',\n 'Authentication required',\n 403,\n );\n }\n\n const { userEntityRef } = httpCredentials.principal;\n\n const result = await this.oidc.approveAuthorizationSession({\n sessionId,\n userEntityRef,\n });\n\n return res.json({\n redirectUrl: result.redirectUrl,\n });\n } catch (error) {\n throw OidcError.fromError(error);\n }\n });\n\n // Authorization Session rejection endpoint\n // Handles user rejection of Authorization Session requests and redirects with error\n router.post('/v1/sessions/:sessionId/reject', async (req, res) => {\n const { sessionId } = validateRequest(sessionIdParamSchema, req.params);\n\n const httpCredentials = await this.httpAuth.credentials(req);\n\n if (!this.auth.isPrincipal(httpCredentials, 'user')) {\n throw new OidcError('access_denied', 'Authentication required', 403);\n }\n\n const { userEntityRef } = httpCredentials.principal;\n try {\n const session = await this.oidc.getAuthorizationSession({\n sessionId,\n });\n\n await this.oidc.rejectAuthorizationSession({\n sessionId,\n userEntityRef,\n });\n\n const errorParams = new URLSearchParams();\n errorParams.append('error', 'access_denied');\n errorParams.append('error_description', 'User denied the request');\n if (session.state) {\n errorParams.append('state', session.state);\n }\n\n const redirectUrl = new URL(session.redirectUri);\n redirectUrl.search = errorParams.toString();\n\n return res.json({\n redirectUrl: redirectUrl.toString(),\n });\n } catch (error) {\n throw OidcError.fromError(error);\n }\n });\n\n // Token endpoint\n // https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest\n // Exchanges authorization codes for access tokens and ID tokens\n // Also handles refresh token grant type\n router.post('/v1/token', async (req, res) => {\n const {\n grant_type: grantType,\n code,\n redirect_uri: redirectUri,\n code_verifier: codeVerifier,\n refresh_token: refreshToken,\n client_id: bodyClientId,\n client_secret: bodyClientSecret,\n } = validateRequest(tokenRequestBodySchema, req.body);\n\n try {\n // Handle authorization_code grant type\n if (grantType === 'authorization_code') {\n if (!code || !redirectUri) {\n throw new OidcError(\n 'invalid_request',\n 'Missing code or redirect_uri parameters for authorization_code grant',\n 400,\n );\n }\n\n const result = await this.oidc.exchangeCodeForToken({\n code,\n redirectUri,\n codeVerifier,\n grantType,\n });\n\n return res.json({\n access_token: result.accessToken,\n token_type: result.tokenType,\n expires_in: result.expiresIn,\n id_token: result.idToken,\n scope: result.scope,\n ...(result.refreshToken && {\n refresh_token: result.refreshToken,\n }),\n });\n }\n\n // Handle refresh_token grant type\n if (grantType === 'refresh_token') {\n if (!refreshToken) {\n throw new OidcError(\n 'invalid_request',\n 'Missing refresh_token parameter for refresh_token grant',\n 400,\n );\n }\n\n // Authenticate if credentials are provided via Basic auth or body\n const hasCredentials =\n req.headers.authorization?.match(/^Basic[ ]+([^\\s]+)$/i) ||\n (bodyClientId && bodyClientSecret);\n\n let authenticatedClientId: string | undefined;\n if (hasCredentials) {\n const { clientId: authedId } = await authenticateClient({\n req,\n oidc: this.oidc,\n bodyClientId,\n bodyClientSecret,\n });\n authenticatedClientId = authedId;\n }\n\n const result = await this.oidc.refreshAccessToken({\n refreshToken,\n clientId: authenticatedClientId,\n });\n\n return res.json({\n access_token: result.accessToken,\n token_type: result.tokenType,\n expires_in: result.expiresIn,\n refresh_token: result.refreshToken,\n });\n }\n\n // Unsupported grant type\n throw new OidcError(\n 'unsupported_grant_type',\n `Grant type ${grantType} is not supported`,\n 400,\n );\n } catch (error) {\n // Invalid auth codes and refresh tokens should be invalid_grant, not invalid_client.\n // Client auth failures are already thrown as OidcError by authenticateClient.\n if (isError(error) && error.name === 'AuthenticationError') {\n throw new OidcError('invalid_grant', error.message, 400, error);\n }\n throw OidcError.fromError(error);\n }\n });\n\n // Token Revocation endpoint (RFC 7009-like)\n // Allows clients to revoke refresh tokens. DCR clients are confidential\n // and must authenticate with their client secret, while CIMD clients\n // are public clients that only identify themselves with their client ID\n router.post('/v1/revoke', async (req, res) => {\n try {\n const {\n token,\n client_id: bodyClientId,\n client_secret: bodyClientSecret,\n } = validateRequest(revokeRequestBodySchema, req.body ?? {});\n\n const { clientId, clientSecret } = parseClientCredentials({\n req,\n bodyClientId,\n bodyClientSecret,\n });\n\n if (!clientId) {\n throw new OidcError(\n 'invalid_client',\n 'Client authentication required',\n 401,\n );\n }\n\n const isValidClient = await this.oidc.verifyRevocationClient({\n clientId,\n clientSecret,\n });\n if (!isValidClient) {\n throw new OidcError(\n 'invalid_client',\n 'Invalid client credentials',\n 401,\n );\n }\n\n try {\n await this.oidc.revokeRefreshToken(token);\n } catch (e) {\n this.logger.error('Failed to revoke token', e);\n }\n\n return res.status(200).send('');\n } catch (e) {\n throw OidcError.fromError(e);\n }\n });\n }\n\n // Dynamic Client Registration endpoint - only available when DCR is enabled\n if (dcrEnabled) {\n // https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration\n // Allows clients to register themselves dynamically with the provider\n router.post('/v1/register', async (req, res) => {\n const {\n client_name: clientName,\n redirect_uris: redirectUris,\n response_types: responseTypes,\n grant_types: grantTypes,\n scope,\n } = validateRequest(registerRequestBodySchema, req.body);\n\n try {\n const client = await this.oidc.registerClient({\n clientName: clientName ?? 'Backstage CLI',\n redirectUris,\n responseTypes,\n grantTypes,\n scope,\n });\n\n return res.status(201).json({\n client_id: client.clientId,\n redirect_uris: client.redirectUris,\n client_secret: client.clientSecret,\n });\n } catch (e) {\n throw OidcError.fromError(e);\n }\n });\n }\n\n router.use(OidcError.middleware(this.logger));\n\n return router;\n }\n}\n"],"names":["z","fromZodError","OidcError","OidcService","Router","json","AuthenticationError","isError"],"mappings":"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"}
|