From 9539a0b0a95e093107e54f9d50d56b5650a2f682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 14 Jun 2024 12:09:48 +0200 Subject: [PATCH] move the auth services to backend-defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/brave-icons-dream.md | 5 + .changeset/chilly-peaches-dress.md | 5 + .changeset/new-chefs-rescue.md | 5 + .changeset/witty-parents-give.md | 5 + packages/backend-app-api/api-report.md | 6 +- .../auth/authServiceFactory.ts | 75 +---- .../services/implementations/deprecated.ts | 3 + .../httpAuth/httpAuthServiceFactory.ts | 276 +---------------- .../src/services/implementations/index.ts | 3 - .../userInfo/userInfoServiceFactory.ts | 93 +----- .../src/auth/createLegacyAuthAdapters.ts | 2 +- packages/backend-defaults/api-report-auth.md | 13 + .../backend-defaults/api-report-httpAuth.md | 16 + .../backend-defaults/api-report-userInfo.md | 16 + packages/backend-defaults/config.d.ts | 277 +++++++++++++++++ .../auth}/20240327104803_public_keys.js | 0 packages/backend-defaults/package.json | 17 + .../backend-defaults/src/CreateBackend.ts | 6 +- .../entrypoints}/auth/DefaultAuthService.ts | 0 .../src/entrypoints}/auth/JwksClient.ts | 0 .../auth/authServiceFactory.test.ts | 2 +- .../entrypoints/auth/authServiceFactory.ts | 89 ++++++ .../external/ExternalTokenHandler.test.ts | 0 .../auth/external/ExternalTokenHandler.ts | 0 .../auth/external/helpers.test.ts | 0 .../src/entrypoints}/auth/external/helpers.ts | 0 .../entrypoints}/auth/external/jwks.test.ts | 0 .../src/entrypoints}/auth/external/jwks.ts | 0 .../entrypoints}/auth/external/legacy.test.ts | 0 .../src/entrypoints}/auth/external/legacy.ts | 0 .../entrypoints}/auth/external/static.test.ts | 0 .../src/entrypoints}/auth/external/static.ts | 0 .../src/entrypoints}/auth/external/types.ts | 0 .../src/entrypoints}/auth/helpers.ts | 0 .../src/entrypoints/auth/index.ts | 17 + .../auth/plugin/PluginTokenHandler.test.ts | 0 .../auth/plugin/PluginTokenHandler.ts | 0 .../auth/plugin/keys/DatabaseKeyStore.test.ts | 0 .../auth/plugin/keys/DatabaseKeyStore.ts | 4 +- .../keys/DatabasePluginKeySource.test.ts | 0 .../plugin/keys/DatabasePluginKeySource.ts | 0 .../keys/StaticConfigPluginKeySource.test.ts | 0 .../keys/StaticConfigPluginKeySource.ts | 0 .../plugin/keys/createPluginKeySource.test.ts | 0 .../auth/plugin/keys/createPluginKeySource.ts | 0 .../entrypoints}/auth/plugin/keys/types.ts | 0 .../src/entrypoints}/auth/types.ts | 0 .../auth/user/UserTokenHandler.test.ts | 0 .../auth/user/UserTokenHandler.ts | 0 .../httpAuth/httpAuthServiceFactory.ts | 290 ++++++++++++++++++ .../src/entrypoints/httpAuth/index.ts | 17 + .../src/entrypoints/userInfo/index.ts | 17 + .../userInfo/userInfoServiceFactory.ts | 103 +++++++ .../src/services/definitions/coreServices.ts | 114 +++++-- yarn.lock | 5 + 55 files changed, 1026 insertions(+), 455 deletions(-) create mode 100644 .changeset/brave-icons-dream.md create mode 100644 .changeset/chilly-peaches-dress.md create mode 100644 .changeset/new-chefs-rescue.md create mode 100644 .changeset/witty-parents-give.md create mode 100644 packages/backend-defaults/api-report-auth.md create mode 100644 packages/backend-defaults/api-report-httpAuth.md create mode 100644 packages/backend-defaults/api-report-userInfo.md rename packages/{backend-app-api/migrations => backend-defaults/migrations/auth}/20240327104803_public_keys.js (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/DefaultAuthService.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/JwksClient.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/authServiceFactory.test.ts (99%) create mode 100644 packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/ExternalTokenHandler.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/ExternalTokenHandler.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/helpers.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/helpers.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/jwks.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/jwks.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/legacy.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/legacy.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/static.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/static.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/external/types.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/helpers.ts (100%) create mode 100644 packages/backend-defaults/src/entrypoints/auth/index.ts rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/PluginTokenHandler.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/PluginTokenHandler.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/keys/DatabaseKeyStore.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/keys/DatabaseKeyStore.ts (98%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/keys/DatabasePluginKeySource.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/keys/DatabasePluginKeySource.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/keys/StaticConfigPluginKeySource.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/keys/StaticConfigPluginKeySource.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/keys/createPluginKeySource.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/keys/createPluginKeySource.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/plugin/keys/types.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/types.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/user/UserTokenHandler.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/auth/user/UserTokenHandler.ts (100%) create mode 100644 packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts create mode 100644 packages/backend-defaults/src/entrypoints/httpAuth/index.ts create mode 100644 packages/backend-defaults/src/entrypoints/userInfo/index.ts create mode 100644 packages/backend-defaults/src/entrypoints/userInfo/userInfoServiceFactory.ts diff --git a/.changeset/brave-icons-dream.md b/.changeset/brave-icons-dream.md new file mode 100644 index 0000000000..0d73c71f47 --- /dev/null +++ b/.changeset/brave-icons-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Import utility functions from `backend-defaults` instead of `backend-app-api` diff --git a/.changeset/chilly-peaches-dress.md b/.changeset/chilly-peaches-dress.md new file mode 100644 index 0000000000..e97588c5d8 --- /dev/null +++ b/.changeset/chilly-peaches-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Deprecated `authServiceFactory`, `httpAuthServiceFactory`, and `userInfoServiceFactory`. Please import them from `@backstage/backend-defaults/auth`, `@backstage/backend-defaults/httpAuth`, and `@backstage/backend-defaults/userInfo` respectively instead. diff --git a/.changeset/new-chefs-rescue.md b/.changeset/new-chefs-rescue.md new file mode 100644 index 0000000000..7ebb7f4719 --- /dev/null +++ b/.changeset/new-chefs-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Improved `coreServices` doc comments diff --git a/.changeset/witty-parents-give.md b/.changeset/witty-parents-give.md new file mode 100644 index 0000000000..e169d8160a --- /dev/null +++ b/.changeset/witty-parents-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added `@backstage/backend-defaults/auth`, `@backstage/backend-defaults/httpAuth`, and `@backstage/backend-defaults/userInfo` to house their respective backend service factories. You should now import these services from those new locations, instead of `@backstage/backend-app-api`. diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 3d7357cb87..45525efa75 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -45,7 +45,7 @@ import { transport } from 'winston'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UserInfoService } from '@backstage/backend-plugin-api'; -// @public (undocumented) +// @public @deprecated (undocumented) export const authServiceFactory: () => ServiceFactory; // @public (undocumented) @@ -151,7 +151,7 @@ export class HostDiscovery implements DiscoveryService { getExternalBaseUrl(pluginId: string): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export const httpAuthServiceFactory: () => ServiceFactory< HttpAuthService, 'plugin' @@ -344,7 +344,7 @@ export const urlReaderServiceFactory: () => ServiceFactory< 'plugin' >; -// @public (undocumented) +// @public @deprecated (undocumented) export const userInfoServiceFactory: () => ServiceFactory< UserInfoService, 'plugin' diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 2a5a210cb7..988b907b4c 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -14,72 +14,11 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { DefaultAuthService } from './DefaultAuthService'; -import { ExternalTokenHandler } from './external/ExternalTokenHandler'; -import { PluginTokenHandler } from './plugin/PluginTokenHandler'; -import { createPluginKeySource } from './plugin/keys/createPluginKeySource'; -import { UserTokenHandler } from './user/UserTokenHandler'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { authServiceFactory as _authServiceFactory } from '../../../../../backend-defaults/src/entrypoints/auth'; -/** @public */ -export const authServiceFactory = createServiceFactory({ - service: coreServices.auth, - deps: { - config: coreServices.rootConfig, - logger: coreServices.rootLogger, - discovery: coreServices.discovery, - plugin: coreServices.pluginMetadata, - database: coreServices.database, - // Re-using the token manager makes sure that we use the same generated keys for - // development as plugins that have not yet been migrated. It's important that this - // keeps working as long as there are plugins that have not been migrated to the - // new auth services in the new backend system. - tokenManager: coreServices.tokenManager, - }, - async factory({ config, discovery, plugin, tokenManager, logger, database }) { - const disableDefaultAuthPolicy = - config.getOptionalBoolean( - 'backend.auth.dangerouslyDisableDefaultAuthPolicy', - ) ?? false; - - const keyDuration = { hours: 1 }; - - const keySource = await createPluginKeySource({ - config, - database, - logger, - keyDuration, - }); - - const userTokens = UserTokenHandler.create({ - discovery, - }); - - const pluginTokens = PluginTokenHandler.create({ - ownPluginId: plugin.getId(), - logger, - keySource, - keyDuration, - discovery, - }); - - const externalTokens = ExternalTokenHandler.create({ - ownPluginId: plugin.getId(), - config, - logger, - }); - - return new DefaultAuthService( - userTokens, - pluginTokens, - externalTokens, - tokenManager, - plugin.getId(), - disableDefaultAuthPolicy, - keySource, - ); - }, -}); +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/auth` instead. + */ +export const authServiceFactory = _authServiceFactory; diff --git a/packages/backend-app-api/src/services/implementations/deprecated.ts b/packages/backend-app-api/src/services/implementations/deprecated.ts index 6f4f76e679..5bf022bb57 100644 --- a/packages/backend-app-api/src/services/implementations/deprecated.ts +++ b/packages/backend-app-api/src/services/implementations/deprecated.ts @@ -14,4 +14,7 @@ * limitations under the License. */ +export * from './auth'; +export * from './httpAuth'; export * from './scheduler'; +export * from './userInfo'; diff --git a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts index c85ba6e4b3..373dd10ead 100644 --- a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts @@ -14,273 +14,11 @@ * limitations under the License. */ -import { - AuthService, - BackstageCredentials, - BackstagePrincipalTypes, - BackstageUserPrincipal, - DiscoveryService, - HttpAuthService, - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { AuthenticationError, NotAllowedError } from '@backstage/errors'; -import { parse as parseCookie } from 'cookie'; -import { Request, Response } from 'express'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { httpAuthServiceFactory as _httpAuthServiceFactory } from '../../../../../backend-defaults/src/entrypoints/httpAuth'; -const FIVE_MINUTES_MS = 5 * 60 * 1000; - -const BACKSTAGE_AUTH_COOKIE = 'backstage-auth'; - -function getTokenFromRequest(req: Request) { - // TODO: support multiple auth headers (iterate rawHeaders) - const authHeader = req.headers.authorization; - if (typeof authHeader === 'string') { - const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i); - const token = matches?.[1]; - if (token) { - return token; - } - } - - return undefined; -} - -function getCookieFromRequest(req: Request) { - const cookieHeader = req.headers.cookie; - if (cookieHeader) { - const cookies = parseCookie(cookieHeader); - const token = cookies[BACKSTAGE_AUTH_COOKIE]; - if (token) { - return token; - } - } - - return undefined; -} - -function willExpireSoon(expiresAt: Date) { - return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime(); -} - -const credentialsSymbol = Symbol('backstage-credentials'); -const limitedCredentialsSymbol = Symbol('backstage-limited-credentials'); - -type RequestWithCredentials = Request & { - [credentialsSymbol]?: Promise; - [limitedCredentialsSymbol]?: Promise; -}; - -class DefaultHttpAuthService implements HttpAuthService { - readonly #auth: AuthService; - readonly #discovery: DiscoveryService; - readonly #pluginId: string; - - constructor( - auth: AuthService, - discovery: DiscoveryService, - pluginId: string, - ) { - this.#auth = auth; - this.#discovery = discovery; - this.#pluginId = pluginId; - } - - async #extractCredentialsFromRequest(req: Request) { - const token = getTokenFromRequest(req); - if (!token) { - return await this.#auth.getNoneCredentials(); - } - - return await this.#auth.authenticate(token); - } - - async #extractLimitedCredentialsFromRequest(req: Request) { - const token = getTokenFromRequest(req); - if (token) { - return await this.#auth.authenticate(token, { - allowLimitedAccess: true, - }); - } - - const cookie = getCookieFromRequest(req); - if (cookie) { - return await this.#auth.authenticate(cookie, { - allowLimitedAccess: true, - }); - } - - return await this.#auth.getNoneCredentials(); - } - - async #getCredentials(req: RequestWithCredentials) { - return (req[credentialsSymbol] ??= - this.#extractCredentialsFromRequest(req)); - } - - async #getLimitedCredentials(req: RequestWithCredentials) { - return (req[limitedCredentialsSymbol] ??= - this.#extractLimitedCredentialsFromRequest(req)); - } - - async credentials( - req: Request, - options?: { - allow?: Array; - allowLimitedAccess?: boolean; - }, - ): Promise> { - // Limited and full credentials are treated as two separate cases, this lets - // us avoid internal dependencies between the AuthService and - // HttpAuthService implementations - const credentials = options?.allowLimitedAccess - ? await this.#getLimitedCredentials(req) - : await this.#getCredentials(req); - - const allowed = options?.allow; - if (!allowed) { - return credentials as any; - } - - if (this.#auth.isPrincipal(credentials, 'none')) { - if (allowed.includes('none' as TAllowed)) { - return credentials as any; - } - - throw new AuthenticationError('Missing credentials'); - } else if (this.#auth.isPrincipal(credentials, 'user')) { - if (allowed.includes('user' as TAllowed)) { - return credentials as any; - } - - throw new NotAllowedError( - `This endpoint does not allow 'user' credentials`, - ); - } else if (this.#auth.isPrincipal(credentials, 'service')) { - if (allowed.includes('service' as TAllowed)) { - return credentials as any; - } - - throw new NotAllowedError( - `This endpoint does not allow 'service' credentials`, - ); - } - - throw new NotAllowedError( - 'Unknown principal type, this should never happen', - ); - } - - async issueUserCookie( - res: Response, - options?: { credentials?: BackstageCredentials }, - ): Promise<{ expiresAt: Date }> { - if (res.headersSent) { - throw new Error('Failed to issue user cookie, headers were already sent'); - } - - let credentials: BackstageCredentials; - if (options?.credentials) { - if (this.#auth.isPrincipal(options.credentials, 'none')) { - res.clearCookie( - BACKSTAGE_AUTH_COOKIE, - await this.#getCookieOptions(res.req), - ); - return { expiresAt: new Date() }; - } - if (!this.#auth.isPrincipal(options.credentials, 'user')) { - throw new AuthenticationError( - 'Refused to issue cookie for non-user principal', - ); - } - credentials = options.credentials; - } else { - credentials = await this.credentials(res.req, { allow: ['user'] }); - } - - const existingExpiresAt = await this.#existingCookieExpiration(res.req); - if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) { - return { expiresAt: existingExpiresAt }; - } - - const { token, expiresAt } = await this.#auth.getLimitedUserToken( - credentials, - ); - if (!token) { - throw new Error('User credentials is unexpectedly missing token'); - } - - res.cookie(BACKSTAGE_AUTH_COOKIE, token, { - ...(await this.#getCookieOptions(res.req)), - expires: expiresAt, - }); - - return { expiresAt }; - } - - async #getCookieOptions(_req: Request): Promise<{ - domain: string; - httpOnly: true; - secure: boolean; - priority: 'high'; - sameSite: 'none' | 'lax'; - }> { - // TODO: eventually we should read from `${req.protocol}://${req.hostname}` - // once https://github.com/backstage/backstage/issues/24169 has landed - const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl( - this.#pluginId, - ); - const externalBaseUrl = new URL(externalBaseUrlStr); - - const secure = - externalBaseUrl.protocol === 'https:' || - externalBaseUrl.hostname === 'localhost'; - - return { - domain: externalBaseUrl.hostname, - httpOnly: true, - secure, - priority: 'high', - sameSite: secure ? 'none' : 'lax', - }; - } - - async #existingCookieExpiration(req: Request): Promise { - const existingCookie = getCookieFromRequest(req); - if (!existingCookie) { - return undefined; - } - - try { - const existingCredentials = await this.#auth.authenticate( - existingCookie, - { - allowLimitedAccess: true, - }, - ); - if (!this.#auth.isPrincipal(existingCredentials, 'user')) { - return undefined; - } - - return existingCredentials.expiresAt; - } catch (error) { - if (error.name === 'AuthenticationError') { - return undefined; - } - throw error; - } - } -} - -/** @public */ -export const httpAuthServiceFactory = createServiceFactory({ - service: coreServices.httpAuth, - deps: { - auth: coreServices.auth, - discovery: coreServices.discovery, - plugin: coreServices.pluginMetadata, - }, - async factory({ auth, discovery, plugin }) { - return new DefaultHttpAuthService(auth, discovery, plugin.getId()); - }, -}); +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/httpAuth` instead. + */ +export const httpAuthServiceFactory = _httpAuthServiceFactory; diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index e6656801a9..b03031549f 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -14,12 +14,10 @@ * limitations under the License. */ -export * from './auth'; export * from './cache'; export * from './config'; export * from './database'; export * from './discovery'; -export * from './httpAuth'; export * from './httpRouter'; export * from './identity'; export * from './lifecycle'; @@ -30,6 +28,5 @@ export * from './rootLifecycle'; export * from './rootLogger'; export * from './tokenManager'; export * from './urlReader'; -export * from './userInfo'; export * from './deprecated'; diff --git a/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts b/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts index db550bf9e8..746f480c41 100644 --- a/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts @@ -14,90 +14,11 @@ * limitations under the License. */ -import { - UserInfoService, - BackstageUserInfo, - coreServices, - createServiceFactory, - DiscoveryService, - BackstageCredentials, -} from '@backstage/backend-plugin-api'; -import { ResponseError } from '@backstage/errors'; -import { decodeJwt } from 'jose'; -import fetch from 'node-fetch'; -import { toInternalBackstageCredentials } from '../auth/helpers'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { userInfoServiceFactory as _userInfoServiceFactory } from '../../../../../backend-defaults/src/entrypoints/userInfo'; -type Options = { - discovery: DiscoveryService; -}; - -export class DefaultUserInfoService implements UserInfoService { - private readonly discovery: DiscoveryService; - - constructor(options: Options) { - this.discovery = options.discovery; - } - - async getUserInfo( - credentials: BackstageCredentials, - ): Promise { - const internalCredentials = toInternalBackstageCredentials(credentials); - if (internalCredentials.principal.type !== 'user') { - throw new Error('Only user credentials are supported'); - } - if (!internalCredentials.token) { - throw new Error('User credentials is unexpectedly missing token'); - } - const { sub: userEntityRef, ent: tokenEnt } = decodeJwt( - internalCredentials.token, - ); - - if (typeof userEntityRef !== 'string') { - throw new Error('User entity ref must be a string'); - } - - let ownershipEntityRefs = tokenEnt; - - if (!ownershipEntityRefs) { - const userInfoResp = await fetch( - `${await this.discovery.getBaseUrl('auth')}/v1/userinfo`, - { - headers: { - Authorization: `Bearer ${internalCredentials.token}`, - }, - }, - ); - - if (!userInfoResp.ok) { - throw await ResponseError.fromResponse(userInfoResp); - } - - const { - claims: { ent }, - } = await userInfoResp.json(); - ownershipEntityRefs = ent; - } - - if (!ownershipEntityRefs) { - throw new Error('Ownership entity refs can not be determined'); - } else if ( - !Array.isArray(ownershipEntityRefs) || - ownershipEntityRefs.some(ref => typeof ref !== 'string') - ) { - throw new Error('Ownership entity refs must be an array of strings'); - } - - return { userEntityRef, ownershipEntityRefs }; - } -} - -/** @public */ -export const userInfoServiceFactory = createServiceFactory({ - service: coreServices.userInfo, - deps: { - discovery: coreServices.discovery, - }, - async factory({ discovery }) { - return new DefaultUserInfoService({ discovery }); - }, -}); +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/userInfo` instead. + */ +export const userInfoServiceFactory = _userInfoServiceFactory; diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index 94f997640d..5e342783f7 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -35,7 +35,7 @@ import { createCredentialsWithUserPrincipal, createCredentialsWithNonePrincipal, toInternalBackstageCredentials, -} from '../../../backend-app-api/src/services/implementations/auth/helpers'; +} from '../../../backend-defaults/src/entrypoints/auth/helpers'; // TODO is this circular thingy a problem? Test in e2e import { type IdentityApiGetIdentityRequest, diff --git a/packages/backend-defaults/api-report-auth.md b/packages/backend-defaults/api-report-auth.md new file mode 100644 index 0000000000..d64ce5bc2a --- /dev/null +++ b/packages/backend-defaults/api-report-auth.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AuthService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public +export const authServiceFactory: () => ServiceFactory; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-httpAuth.md b/packages/backend-defaults/api-report-httpAuth.md new file mode 100644 index 0000000000..6ffe3d6539 --- /dev/null +++ b/packages/backend-defaults/api-report-httpAuth.md @@ -0,0 +1,16 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { HttpAuthService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public +export const httpAuthServiceFactory: () => ServiceFactory< + HttpAuthService, + 'plugin' +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-userInfo.md b/packages/backend-defaults/api-report-userInfo.md new file mode 100644 index 0000000000..d11fb69718 --- /dev/null +++ b/packages/backend-defaults/api-report-userInfo.md @@ -0,0 +1,16 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { UserInfoService } from '@backstage/backend-plugin-api'; + +// @public +export const userInfoServiceFactory: () => ServiceFactory< + UserInfoService, + 'plugin' +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index 569ab436db..ab1e443fc2 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -15,6 +15,283 @@ */ export interface Config { + backend?: { + /** + * Options used by the default auth, httpAuth and userInfo services. + */ + auth?: { + /** + * This disables the otherwise default auth policy, which requires all + * requests to be authenticated with either user or service credentials. + * + * Disabling this check means that the backend will no longer block + * unauthenticated requests, but instead allow them to pass through to + * plugins. + * + * If permissions are enabled, unauthenticated requests will be treated + * exactly as such, leaving it to the permission policy to determine what + * permissions should be allowed for an unauthenticated identity. Note + * that this will also apply to service-to-service calls between plugins + * unless you configure credentials for service calls. + */ + dangerouslyDisableDefaultAuthPolicy?: boolean; + + /** Controls how to store keys for plugin-to-plugin auth */ + pluginKeyStore?: + | { type: 'database' } + | { + type: 'static'; + static: { + /** + * Must be declared at least once and the first one will be used for signing. + */ + keys: Array<{ + /** + * Path to the public key file in the SPKI format. Should be an absolute path. + */ + publicKeyFile: string; + /** + * Path to the matching private key file in the PKCS#8 format. Should be an absolute path. + * + * The first array entry must specify a private key file, the rest must not. + */ + privateKeyFile?: string; + /** + * ID to uniquely identify this key within the JWK set. + */ + keyId: string; + /** + * JWS "alg" (Algorithm) Header Parameter value. Defaults to ES256. + * Must match the algorithm used to generate the keys in the provided files + */ + algorithm?: string; + }>; + }; + }; + + /** + * Configures methods of external access, ie ways for callers outside of + * the Backstage ecosystem to get authorized for access to APIs that do + * not permit unauthorized access. + */ + externalAccess: Array< + | { + /** + * This is the legacy service-to-service access method, where a set + * of static keys were shared among plugins and used for symmetric + * signing and verification. These correspond to the old + * `backend.auth.keys` set and retain their behavior for backwards + * compatibility. Please migrate to other access methods when + * possible. + * + * Callers generate JWT tokens with the following payload: + * + * ```json + * { + * "sub": "backstage-plugin", + * "exp": + * } + * ``` + * + * And sign them with HS256, using the base64 decoded secret. The + * tokens are then passed along with requests in the Authorization + * header: + * + * ``` + * Authorization: Bearer eyJhbGciOiJIUzI... + * ``` + */ + type: 'legacy'; + options: { + /** + * Any set of base64 encoded random bytes to be used as both the + * signing and verification key. Should be sufficiently long so as + * not to be easy to guess by brute force. + * + * Can be generated eg using + * + * ```sh + * node -p 'require("crypto").randomBytes(24).toString("base64")' + * ``` + * + * @visibility secret + */ + secret: string; + + /** + * Sets the subject of the principal, when matching this token. + * Useful for debugging and tracking purposes. + */ + subject: string; + }; + /** + * Restricts what types of access that are permitted for this access + * method. If no access restrictions are given, it'll have unlimited + * access. This access restriction applies for the framework level; + * individual plugins may have their own access control mechanisms + * on top of this. + */ + accessRestrictions?: Array<{ + /** + * Permit access to make requests to this plugin. + * + * Can be further refined by setting additional fields below. + */ + plugin: string; + /** + * If given, this method is limited to only performing actions + * with these named permissions in this plugin. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permission?: string | Array; + /** + * If given, this method is limited to only performing actions + * whose permissions have these attributes. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permissionAttribute?: { + /** + * One of more of 'create', 'read', 'update', or 'delete'. + */ + action?: string | Array; + }; + }>; + } + | { + /** + * This access method consists of random static tokens that can be + * handed out to callers. + * + * The tokens are then passed along verbatim with requests in the + * Authorization header: + * + * ``` + * Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW + * ``` + */ + type: 'static'; + options: { + /** + * A raw token that can be any string, but for security reasons + * should be sufficiently long so as not to be easy to guess by + * brute force. + * + * Can be generated eg using + * + * ```sh + * node -p 'require("crypto").randomBytes(24).toString("base64")' + * ``` + * + * Since the tokens can be any string, you are free to add + * additional identifying data to them if you like. For example, + * adding a `freben-local-dev-` prefix for debugging purposes to a + * token that you know will be handed out for use as a personal + * access token during development. + * + * @visibility secret + */ + token: string; + + /** + * Sets the subject of the principal, when matching this token. + * Useful for debugging and tracking purposes. + */ + subject: string; + }; + /** + * Restricts what types of access that are permitted for this access + * method. If no access restrictions are given, it'll have unlimited + * access. This access restriction applies for the framework level; + * individual plugins may have their own access control mechanisms + * on top of this. + */ + accessRestrictions?: Array<{ + /** + * Permit access to make requests to this plugin. + * + * Can be further refined by setting additional fields below. + */ + plugin: string; + /** + * If given, this method is limited to only performing actions + * with these named permissions in this plugin. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permission?: string | Array; + /** + * If given, this method is limited to only performing actions + * whose permissions have these attributes. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permissionAttribute?: { + /** + * One of more of 'create', 'read', 'update', or 'delete'. + */ + action?: string | Array; + }; + }>; + } + | { + /** + * This access method consists of a JWKS endpoint that can be used to + * verify JWT tokens. + * + * Callers generate JWT tokens via 3rd party tooling + * and pass them in the Authorization header: + * + * ``` + * Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW + * ``` + */ + type: 'jwks'; + options: { + /** + * The full URL of the JWKS endpoint. + */ + url: string; + /** + * Sets the algorithm(s) that should be used to verify the JWT tokens. + * The passed JWTs must have been signed using one of the listed algorithms. + */ + algorithm?: string | string[]; + /** + * Sets the issuer(s) that should be used to verify the JWT tokens. + * Passed JWTs must have an `iss` claim which matches one of the specified issuers. + */ + issuer?: string | string[]; + /** + * Sets the audience(s) that should be used to verify the JWT tokens. + * The passed JWTs must have an "aud" claim that matches one of the audiences specified, + * or have no audience specified. + */ + audience?: string | string[]; + /** + * Sets an optional subject prefix. Passes the subject to called plugins. + * Useful for debugging and tracking purposes. + */ + subjectPrefix?: string; + }; + } + >; + }; + }; + /** * Options used by the default discovery service. */ diff --git a/packages/backend-app-api/migrations/20240327104803_public_keys.js b/packages/backend-defaults/migrations/auth/20240327104803_public_keys.js similarity index 100% rename from packages/backend-app-api/migrations/20240327104803_public_keys.js rename to packages/backend-defaults/migrations/auth/20240327104803_public_keys.js diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 2cdf30e0d2..06be729126 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -20,21 +20,27 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", + "./auth": "./src/entrypoints/auth/index.ts", "./cache": "./src/entrypoints/cache/index.ts", "./database": "./src/entrypoints/database/index.ts", "./discovery": "./src/entrypoints/discovery/index.ts", + "./httpAuth": "./src/entrypoints/httpAuth/index.ts", "./lifecycle": "./src/entrypoints/lifecycle/index.ts", "./permissions": "./src/entrypoints/permissions/index.ts", "./rootConfig": "./src/entrypoints/rootConfig/index.ts", "./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts", "./scheduler": "./src/entrypoints/scheduler/index.ts", "./urlReader": "./src/entrypoints/urlReader/index.ts", + "./userInfo": "./src/entrypoints/userInfo/index.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { + "auth": [ + "src/entrypoints/auth/index.ts" + ], "cache": [ "src/entrypoints/cache/index.ts" ], @@ -44,6 +50,9 @@ "discovery": [ "src/entrypoints/discovery/index.ts" ], + "httpAuth": [ + "src/entrypoints/httpAuth/index.ts" + ], "lifecycle": [ "src/entrypoints/lifecycle/index.ts" ], @@ -62,6 +71,9 @@ "urlReader": [ "src/entrypoints/urlReader/index.ts" ], + "userInfo": [ + "src/entrypoints/userInfo/index.ts" + ], "package.json": [ "package.json" ] @@ -88,6 +100,7 @@ "@aws-sdk/credential-providers": "^3.350.0", "@aws-sdk/types": "^3.347.0", "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-common": "workspace:^", "@backstage/backend-dev-utils": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", @@ -95,6 +108,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/integration-aws-node": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-events-node": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", "@backstage/types": "workspace:^", @@ -107,10 +121,13 @@ "base64-stream": "^1.0.0", "better-sqlite3": "^9.0.0", "concat-stream": "^2.0.0", + "cookie": "^0.6.0", "cron": "^3.0.0", + "express": "^4.17.1", "fs-extra": "^11.2.0", "git-url-parse": "^14.0.0", "isomorphic-git": "^1.23.0", + "jose": "^5.0.0", "keyv": "^4.5.2", "knex": "^3.0.0", "lodash": "^4.17.21", diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 823b3c2833..834d54e9e5 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -16,26 +16,26 @@ import { Backend, - authServiceFactory, createSpecializedBackend, - httpAuthServiceFactory, httpRouterServiceFactory, identityServiceFactory, loggerServiceFactory, rootHttpRouterServiceFactory, rootLoggerServiceFactory, tokenManagerServiceFactory, - userInfoServiceFactory, } from '@backstage/backend-app-api'; +import { authServiceFactory } from '@backstage/backend-defaults/auth'; import { cacheServiceFactory } from '@backstage/backend-defaults/cache'; import { databaseServiceFactory } from '@backstage/backend-defaults/database'; import { discoveryServiceFactory } from '@backstage/backend-defaults/discovery'; +import { httpAuthServiceFactory } from '@backstage/backend-defaults/httpAuth'; import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle'; import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions'; import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig'; import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle'; import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler'; import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader'; +import { userInfoServiceFactory } from '@backstage/backend-defaults/userInfo'; import { eventsServiceFactory } from '@backstage/plugin-events-node'; export const defaultServiceFactories = [ diff --git a/packages/backend-app-api/src/services/implementations/auth/DefaultAuthService.ts b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/DefaultAuthService.ts rename to packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/JwksClient.ts b/packages/backend-defaults/src/entrypoints/auth/JwksClient.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/JwksClient.ts rename to packages/backend-defaults/src/entrypoints/auth/JwksClient.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts similarity index 99% rename from packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts rename to packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index a717d5a2bb..22357d0f49 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -19,6 +19,7 @@ import { mockServices, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; +import { tokenManagerServiceFactory } from '@backstage/backend-app-api'; import { authServiceFactory } from './authServiceFactory'; import { base64url, decodeJwt } from 'jose'; import { discoveryServiceFactory } from '../discovery'; @@ -26,7 +27,6 @@ import { BackstageServicePrincipal, BackstageUserPrincipal, } from '@backstage/backend-plugin-api'; -import { tokenManagerServiceFactory } from '../tokenManager'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { InternalBackstageCredentials } from './types'; diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts new file mode 100644 index 0000000000..6a1aa51604 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -0,0 +1,89 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { DefaultAuthService } from './DefaultAuthService'; +import { ExternalTokenHandler } from './external/ExternalTokenHandler'; +import { PluginTokenHandler } from './plugin/PluginTokenHandler'; +import { createPluginKeySource } from './plugin/keys/createPluginKeySource'; +import { UserTokenHandler } from './user/UserTokenHandler'; + +/** + * Token authentication and credentials management. + * + * @public + */ +export const authServiceFactory = createServiceFactory({ + service: coreServices.auth, + deps: { + config: coreServices.rootConfig, + logger: coreServices.rootLogger, + discovery: coreServices.discovery, + plugin: coreServices.pluginMetadata, + database: coreServices.database, + // Re-using the token manager makes sure that we use the same generated keys for + // development as plugins that have not yet been migrated. It's important that this + // keeps working as long as there are plugins that have not been migrated to the + // new auth services in the new backend system. + tokenManager: coreServices.tokenManager, + }, + async factory({ config, discovery, plugin, tokenManager, logger, database }) { + const disableDefaultAuthPolicy = + config.getOptionalBoolean( + 'backend.auth.dangerouslyDisableDefaultAuthPolicy', + ) ?? false; + + const keyDuration = { hours: 1 }; + + const keySource = await createPluginKeySource({ + config, + database, + logger, + keyDuration, + }); + + const userTokens = UserTokenHandler.create({ + discovery, + }); + + const pluginTokens = PluginTokenHandler.create({ + ownPluginId: plugin.getId(), + logger, + keySource, + keyDuration, + discovery, + }); + + const externalTokens = ExternalTokenHandler.create({ + ownPluginId: plugin.getId(), + config, + logger, + }); + + return new DefaultAuthService( + userTokens, + pluginTokens, + externalTokens, + tokenManager, + plugin.getId(), + disableDefaultAuthPolicy, + keySource, + ); + }, +}); diff --git a/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.test.ts rename to packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts rename to packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/helpers.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/helpers.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/helpers.test.ts rename to packages/backend-defaults/src/entrypoints/auth/external/helpers.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/helpers.ts rename to packages/backend-defaults/src/entrypoints/auth/external/helpers.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/jwks.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/jwks.test.ts rename to packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/jwks.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/jwks.ts rename to packages/backend-defaults/src/entrypoints/auth/external/jwks.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/legacy.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/legacy.test.ts rename to packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/legacy.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/legacy.ts rename to packages/backend-defaults/src/entrypoints/auth/external/legacy.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/static.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/static.test.ts rename to packages/backend-defaults/src/entrypoints/auth/external/static.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/static.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/static.ts rename to packages/backend-defaults/src/entrypoints/auth/external/static.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/external/types.ts b/packages/backend-defaults/src/entrypoints/auth/external/types.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/external/types.ts rename to packages/backend-defaults/src/entrypoints/auth/external/types.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/helpers.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/helpers.ts rename to packages/backend-defaults/src/entrypoints/auth/helpers.ts diff --git a/packages/backend-defaults/src/entrypoints/auth/index.ts b/packages/backend-defaults/src/entrypoints/auth/index.ts new file mode 100644 index 0000000000..1b55d46a83 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/auth/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { authServiceFactory } from './authServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/PluginTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/PluginTokenHandler.test.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/PluginTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/PluginTokenHandler.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/DatabaseKeyStore.test.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabaseKeyStore.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/keys/DatabaseKeyStore.test.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabaseKeyStore.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/DatabaseKeyStore.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabaseKeyStore.ts similarity index 98% rename from packages/backend-app-api/src/services/implementations/auth/plugin/keys/DatabaseKeyStore.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabaseKeyStore.ts index aea126d00a..9f5c278a9c 100644 --- a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/DatabaseKeyStore.ts +++ b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabaseKeyStore.ts @@ -37,8 +37,8 @@ type Row = { export function applyDatabaseMigrations(knex: Knex): Promise { const migrationsDir = resolvePackagePath( - '@backstage/backend-app-api', - 'migrations', + '@backstage/backend-defaults', + 'migrations/auth', ); return knex.migrate.latest({ diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/DatabasePluginKeySource.test.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/keys/DatabasePluginKeySource.test.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/DatabasePluginKeySource.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/keys/DatabasePluginKeySource.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/StaticConfigPluginKeySource.test.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/StaticConfigPluginKeySource.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/keys/StaticConfigPluginKeySource.test.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/keys/StaticConfigPluginKeySource.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/StaticConfigPluginKeySource.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/StaticConfigPluginKeySource.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/keys/StaticConfigPluginKeySource.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/keys/StaticConfigPluginKeySource.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/createPluginKeySource.test.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/createPluginKeySource.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/keys/createPluginKeySource.test.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/keys/createPluginKeySource.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/createPluginKeySource.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/createPluginKeySource.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/keys/createPluginKeySource.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/keys/createPluginKeySource.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/plugin/keys/types.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/types.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/plugin/keys/types.ts rename to packages/backend-defaults/src/entrypoints/auth/plugin/keys/types.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/types.ts b/packages/backend-defaults/src/entrypoints/auth/types.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/types.ts rename to packages/backend-defaults/src/entrypoints/auth/types.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/user/UserTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/user/UserTokenHandler.test.ts rename to packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/user/UserTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/auth/user/UserTokenHandler.ts rename to packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.ts diff --git a/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts b/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts new file mode 100644 index 0000000000..e24c80aa6a --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/httpAuth/httpAuthServiceFactory.ts @@ -0,0 +1,290 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AuthService, + BackstageCredentials, + BackstagePrincipalTypes, + BackstageUserPrincipal, + DiscoveryService, + HttpAuthService, + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { AuthenticationError, NotAllowedError } from '@backstage/errors'; +import { parse as parseCookie } from 'cookie'; +import { Request, Response } from 'express'; + +const FIVE_MINUTES_MS = 5 * 60 * 1000; + +const BACKSTAGE_AUTH_COOKIE = 'backstage-auth'; + +function getTokenFromRequest(req: Request) { + // TODO: support multiple auth headers (iterate rawHeaders) + const authHeader = req.headers.authorization; + if (typeof authHeader === 'string') { + const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i); + const token = matches?.[1]; + if (token) { + return token; + } + } + + return undefined; +} + +function getCookieFromRequest(req: Request) { + const cookieHeader = req.headers.cookie; + if (cookieHeader) { + const cookies = parseCookie(cookieHeader); + const token = cookies[BACKSTAGE_AUTH_COOKIE]; + if (token) { + return token; + } + } + + return undefined; +} + +function willExpireSoon(expiresAt: Date) { + return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime(); +} + +const credentialsSymbol = Symbol('backstage-credentials'); +const limitedCredentialsSymbol = Symbol('backstage-limited-credentials'); + +type RequestWithCredentials = Request & { + [credentialsSymbol]?: Promise; + [limitedCredentialsSymbol]?: Promise; +}; + +class DefaultHttpAuthService implements HttpAuthService { + readonly #auth: AuthService; + readonly #discovery: DiscoveryService; + readonly #pluginId: string; + + constructor( + auth: AuthService, + discovery: DiscoveryService, + pluginId: string, + ) { + this.#auth = auth; + this.#discovery = discovery; + this.#pluginId = pluginId; + } + + async #extractCredentialsFromRequest(req: Request) { + const token = getTokenFromRequest(req); + if (!token) { + return await this.#auth.getNoneCredentials(); + } + + return await this.#auth.authenticate(token); + } + + async #extractLimitedCredentialsFromRequest(req: Request) { + const token = getTokenFromRequest(req); + if (token) { + return await this.#auth.authenticate(token, { + allowLimitedAccess: true, + }); + } + + const cookie = getCookieFromRequest(req); + if (cookie) { + return await this.#auth.authenticate(cookie, { + allowLimitedAccess: true, + }); + } + + return await this.#auth.getNoneCredentials(); + } + + async #getCredentials(req: RequestWithCredentials) { + return (req[credentialsSymbol] ??= + this.#extractCredentialsFromRequest(req)); + } + + async #getLimitedCredentials(req: RequestWithCredentials) { + return (req[limitedCredentialsSymbol] ??= + this.#extractLimitedCredentialsFromRequest(req)); + } + + async credentials( + req: Request, + options?: { + allow?: Array; + allowLimitedAccess?: boolean; + }, + ): Promise> { + // Limited and full credentials are treated as two separate cases, this lets + // us avoid internal dependencies between the AuthService and + // HttpAuthService implementations + const credentials = options?.allowLimitedAccess + ? await this.#getLimitedCredentials(req) + : await this.#getCredentials(req); + + const allowed = options?.allow; + if (!allowed) { + return credentials as any; + } + + if (this.#auth.isPrincipal(credentials, 'none')) { + if (allowed.includes('none' as TAllowed)) { + return credentials as any; + } + + throw new AuthenticationError('Missing credentials'); + } else if (this.#auth.isPrincipal(credentials, 'user')) { + if (allowed.includes('user' as TAllowed)) { + return credentials as any; + } + + throw new NotAllowedError( + `This endpoint does not allow 'user' credentials`, + ); + } else if (this.#auth.isPrincipal(credentials, 'service')) { + if (allowed.includes('service' as TAllowed)) { + return credentials as any; + } + + throw new NotAllowedError( + `This endpoint does not allow 'service' credentials`, + ); + } + + throw new NotAllowedError( + 'Unknown principal type, this should never happen', + ); + } + + async issueUserCookie( + res: Response, + options?: { credentials?: BackstageCredentials }, + ): Promise<{ expiresAt: Date }> { + if (res.headersSent) { + throw new Error('Failed to issue user cookie, headers were already sent'); + } + + let credentials: BackstageCredentials; + if (options?.credentials) { + if (this.#auth.isPrincipal(options.credentials, 'none')) { + res.clearCookie( + BACKSTAGE_AUTH_COOKIE, + await this.#getCookieOptions(res.req), + ); + return { expiresAt: new Date() }; + } + if (!this.#auth.isPrincipal(options.credentials, 'user')) { + throw new AuthenticationError( + 'Refused to issue cookie for non-user principal', + ); + } + credentials = options.credentials; + } else { + credentials = await this.credentials(res.req, { allow: ['user'] }); + } + + const existingExpiresAt = await this.#existingCookieExpiration(res.req); + if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) { + return { expiresAt: existingExpiresAt }; + } + + const { token, expiresAt } = await this.#auth.getLimitedUserToken( + credentials, + ); + if (!token) { + throw new Error('User credentials is unexpectedly missing token'); + } + + res.cookie(BACKSTAGE_AUTH_COOKIE, token, { + ...(await this.#getCookieOptions(res.req)), + expires: expiresAt, + }); + + return { expiresAt }; + } + + async #getCookieOptions(_req: Request): Promise<{ + domain: string; + httpOnly: true; + secure: boolean; + priority: 'high'; + sameSite: 'none' | 'lax'; + }> { + // TODO: eventually we should read from `${req.protocol}://${req.hostname}` + // once https://github.com/backstage/backstage/issues/24169 has landed + const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl( + this.#pluginId, + ); + const externalBaseUrl = new URL(externalBaseUrlStr); + + const secure = + externalBaseUrl.protocol === 'https:' || + externalBaseUrl.hostname === 'localhost'; + + return { + domain: externalBaseUrl.hostname, + httpOnly: true, + secure, + priority: 'high', + sameSite: secure ? 'none' : 'lax', + }; + } + + async #existingCookieExpiration(req: Request): Promise { + const existingCookie = getCookieFromRequest(req); + if (!existingCookie) { + return undefined; + } + + try { + const existingCredentials = await this.#auth.authenticate( + existingCookie, + { + allowLimitedAccess: true, + }, + ); + if (!this.#auth.isPrincipal(existingCredentials, 'user')) { + return undefined; + } + + return existingCredentials.expiresAt; + } catch (error) { + if (error.name === 'AuthenticationError') { + return undefined; + } + throw error; + } + } +} + +/** + * Authentication of HTTP requests. + * + * @public + */ +export const httpAuthServiceFactory = createServiceFactory({ + service: coreServices.httpAuth, + deps: { + auth: coreServices.auth, + discovery: coreServices.discovery, + plugin: coreServices.pluginMetadata, + }, + async factory({ auth, discovery, plugin }) { + return new DefaultHttpAuthService(auth, discovery, plugin.getId()); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/httpAuth/index.ts b/packages/backend-defaults/src/entrypoints/httpAuth/index.ts new file mode 100644 index 0000000000..edd7e53026 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/httpAuth/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { httpAuthServiceFactory } from './httpAuthServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/userInfo/index.ts b/packages/backend-defaults/src/entrypoints/userInfo/index.ts new file mode 100644 index 0000000000..13b42f053c --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/userInfo/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { userInfoServiceFactory } from './userInfoServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/userInfo/userInfoServiceFactory.ts b/packages/backend-defaults/src/entrypoints/userInfo/userInfoServiceFactory.ts new file mode 100644 index 0000000000..db550bf9e8 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/userInfo/userInfoServiceFactory.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + UserInfoService, + BackstageUserInfo, + coreServices, + createServiceFactory, + DiscoveryService, + BackstageCredentials, +} from '@backstage/backend-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { decodeJwt } from 'jose'; +import fetch from 'node-fetch'; +import { toInternalBackstageCredentials } from '../auth/helpers'; + +type Options = { + discovery: DiscoveryService; +}; + +export class DefaultUserInfoService implements UserInfoService { + private readonly discovery: DiscoveryService; + + constructor(options: Options) { + this.discovery = options.discovery; + } + + async getUserInfo( + credentials: BackstageCredentials, + ): Promise { + const internalCredentials = toInternalBackstageCredentials(credentials); + if (internalCredentials.principal.type !== 'user') { + throw new Error('Only user credentials are supported'); + } + if (!internalCredentials.token) { + throw new Error('User credentials is unexpectedly missing token'); + } + const { sub: userEntityRef, ent: tokenEnt } = decodeJwt( + internalCredentials.token, + ); + + if (typeof userEntityRef !== 'string') { + throw new Error('User entity ref must be a string'); + } + + let ownershipEntityRefs = tokenEnt; + + if (!ownershipEntityRefs) { + const userInfoResp = await fetch( + `${await this.discovery.getBaseUrl('auth')}/v1/userinfo`, + { + headers: { + Authorization: `Bearer ${internalCredentials.token}`, + }, + }, + ); + + if (!userInfoResp.ok) { + throw await ResponseError.fromResponse(userInfoResp); + } + + const { + claims: { ent }, + } = await userInfoResp.json(); + ownershipEntityRefs = ent; + } + + if (!ownershipEntityRefs) { + throw new Error('Ownership entity refs can not be determined'); + } else if ( + !Array.isArray(ownershipEntityRefs) || + ownershipEntityRefs.some(ref => typeof ref !== 'string') + ) { + throw new Error('Ownership entity refs must be an array of strings'); + } + + return { userEntityRef, ownershipEntityRefs }; + } +} + +/** @public */ +export const userInfoServiceFactory = createServiceFactory({ + service: coreServices.userInfo, + deps: { + discovery: coreServices.discovery, + }, + async factory({ discovery }) { + return new DefaultUserInfoService({ discovery }); + }, +}); diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index 4e02611f80..b0518fb9d4 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -23,7 +23,11 @@ import { createServiceRef } from '../system'; */ export namespace coreServices { /** - * The service reference for the plugin scoped {@link AuthService}. + * Handles token authentication and credentials management. + * + * See {@link AuthService} + * and {@link https://backstage.io/docs/backend-system/core-services/auth | the service docs} + * for more information. * * @public */ @@ -32,7 +36,11 @@ export namespace coreServices { }); /** - * The service reference for the plugin scoped {@link UserInfoService}. + * Authenticated user information retrieval. + * + * See {@link UserInfoService} + * and {@link https://backstage.io/docs/backend-system/core-services/user-info | the service docs} + * for more information. * * @public */ @@ -43,7 +51,11 @@ export namespace coreServices { }); /** - * The service reference for the plugin scoped {@link CacheService}. + * Key-value store for caching data. + * + * See {@link CacheService} + * and {@link https://backstage.io/docs/backend-system/core-services/cache | the service docs} + * for more information. * * @public */ @@ -52,7 +64,11 @@ export namespace coreServices { }); /** - * The service reference for the root scoped {@link RootConfigService}. + * Access to static configuration. + * + * See {@link RootConfigService} + * and {@link https://backstage.io/docs/backend-system/core-services/root-config | the service docs} + * for more information. * * @public */ @@ -61,7 +77,11 @@ export namespace coreServices { >({ id: 'core.rootConfig', scope: 'root' }); /** - * The service reference for the plugin scoped {@link DatabaseService}. + * Database access and management via `knex`. + * + * See {@link DatabaseService} + * and {@link https://backstage.io/docs/backend-system/core-services/database | the service docs} + * for more information. * * @public */ @@ -70,7 +90,11 @@ export namespace coreServices { >({ id: 'core.database' }); /** - * The service reference for the plugin scoped {@link DiscoveryService}. + * Service discovery for inter-plugin communication. + * + * See {@link DiscoveryService} + * and {@link https://backstage.io/docs/backend-system/core-services/discovery | the service docs} + * for more information. * * @public */ @@ -79,7 +103,11 @@ export namespace coreServices { >({ id: 'core.discovery' }); /** - * The service reference for the plugin scoped {@link HttpAuthService}. + * Authentication of HTTP requests. + * + * See {@link HttpAuthService} + * and {@link https://backstage.io/docs/backend-system/core-services/http-auth | the service docs} + * for more information. * * @public */ @@ -88,7 +116,11 @@ export namespace coreServices { >({ id: 'core.httpAuth' }); /** - * The service reference for the plugin scoped {@link HttpRouterService}. + * HTTP route registration for plugins. + * + * See {@link HttpRouterService} + * and {@link https://backstage.io/docs/backend-system/core-services/http-router | the service docs} + * for more information. * * @public */ @@ -97,7 +129,11 @@ export namespace coreServices { >({ id: 'core.httpRouter' }); /** - * The service reference for the plugin scoped {@link LifecycleService}. + * Registration of plugin startup and shutdown lifecycle hooks. + * + * See {@link LifecycleService} + * and {@link https://backstage.io/docs/backend-system/core-services/lifecycle | the service docs} + * for more information. * * @public */ @@ -106,7 +142,11 @@ export namespace coreServices { >({ id: 'core.lifecycle' }); /** - * The service reference for the plugin scoped {@link LoggerService}. + * Plugin-level logging. + * + * See {@link LoggerService} + * and {@link https://backstage.io/docs/backend-system/core-services/logger | the service docs} + * for more information. * * @public */ @@ -115,7 +155,11 @@ export namespace coreServices { >({ id: 'core.logger' }); /** - * The service reference for the plugin scoped {@link PermissionsService}. + * Permission system integration for authorization of user actions. + * + * See {@link PermissionsService} + * and {@link https://backstage.io/docs/backend-system/core-services/permissions | the service docs} + * for more information. * * @public */ @@ -124,7 +168,11 @@ export namespace coreServices { >({ id: 'core.permissions' }); /** - * The service reference for the plugin scoped {@link PluginMetadataService}. + * Built-in service for accessing metadata about the current plugin. + * + * See {@link PluginMetadataService} + * and {@link https://backstage.io/docs/backend-system/core-services/plugin-metadata | the service docs} + * for more information. * * @public */ @@ -133,7 +181,11 @@ export namespace coreServices { >({ id: 'core.pluginMetadata' }); /** - * The service reference for the root scoped {@link RootHttpRouterService}. + * HTTP route registration for root services. + * + * See {@link RootHttpRouterService} + * and {@link https://backstage.io/docs/backend-system/core-services/root-http-router | the service docs} + * for more information. * * @public */ @@ -142,7 +194,11 @@ export namespace coreServices { >({ id: 'core.rootHttpRouter', scope: 'root' }); /** - * The service reference for the root scoped {@link RootLifecycleService}. + * Registration of backend startup and shutdown lifecycle hooks. + * + * See {@link RootLifecycleService} + * and {@link https://backstage.io/docs/backend-system/core-services/root-lifecycle | the service docs} + * for more information. * * @public */ @@ -151,7 +207,11 @@ export namespace coreServices { >({ id: 'core.rootLifecycle', scope: 'root' }); /** - * The service reference for the root scoped {@link RootLoggerService}. + * Root-level logging. + * + * See {@link RootLoggerService} + * and {@link https://backstage.io/docs/backend-system/core-services/root-logger | the service docs} + * for more information. * * @public */ @@ -160,7 +220,11 @@ export namespace coreServices { >({ id: 'core.rootLogger', scope: 'root' }); /** - * The service reference for the plugin scoped {@link SchedulerService}. + * Scheduling of distributed background tasks. + * + * See {@link SchedulerService} + * and {@link https://backstage.io/docs/backend-system/core-services/scheduler | the service docs} + * for more information. * * @public */ @@ -169,7 +233,11 @@ export namespace coreServices { >({ id: 'core.scheduler' }); /** - * The service reference for the plugin scoped {@link TokenManagerService}. + * Deprecated service authentication service, use the `auth` service instead. + * + * See {@link TokenManagerService} + * and {@link https://backstage.io/docs/backend-system/core-services/token-manager | the service docs} + * for more information. * * @public * @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead @@ -179,7 +247,11 @@ export namespace coreServices { >({ id: 'core.tokenManager' }); /** - * The service reference for the plugin scoped {@link UrlReaderService}. + * Reading content from external systems. + * + * See {@link UrlReaderService} + * and {@link https://backstage.io/docs/backend-system/core-services/url-reader | the service docs} + * for more information. * * @public */ @@ -188,7 +260,11 @@ export namespace coreServices { >({ id: 'core.urlReader' }); /** - * The service reference for the plugin scoped {@link IdentityService}. + * Deprecated user authentication service, use the `auth` service instead. + * + * See {@link IdentityService} + * and {@link https://backstage.io/docs/backend-system/core-services/identity | the service docs} + * for more information. * * @public * @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead diff --git a/yarn.lock b/yarn.lock index 984ba1b998..8e40f43582 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3606,6 +3606,7 @@ __metadata: "@aws-sdk/types": ^3.347.0 "@aws-sdk/util-stream-node": ^3.350.0 "@backstage/backend-app-api": "workspace:^" + "@backstage/backend-common": "workspace:^" "@backstage/backend-dev-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -3615,6 +3616,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/integration-aws-node": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-events-node": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@backstage/types": "workspace:^" @@ -3628,10 +3630,13 @@ __metadata: base64-stream: ^1.0.0 better-sqlite3: ^9.0.0 concat-stream: ^2.0.0 + cookie: ^0.6.0 cron: ^3.0.0 + express: ^4.17.1 fs-extra: ^11.2.0 git-url-parse: ^14.0.0 isomorphic-git: ^1.23.0 + jose: ^5.0.0 keyv: ^4.5.2 knex: ^3.0.0 lodash: ^4.17.21