UNPKG

@backstage/backend-defaults

Version:

Backend defaults used by Backstage backend apps

206 lines (202 loc) 6.41 kB
'use strict'; var backendPluginApi = require('@backstage/backend-plugin-api'); var errors = require('@backstage/errors'); var cookie = require('cookie'); const FIVE_MINUTES_MS = 5 * 60 * 1e3; const BACKSTAGE_AUTH_COOKIE = "backstage-auth"; function getTokenFromRequest(req) { let token; const authHeader = req.headers.authorization; if (typeof authHeader === "string") { const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i); token = matches?.[1]; } return { token }; } function getCookieFromRequest(req) { const cookieHeader = req.headers.cookie; if (cookieHeader) { const cookies = cookie.parse(cookieHeader); const token = cookies[BACKSTAGE_AUTH_COOKIE]; if (token) { return token; } } return void 0; } function willExpireSoon(expiresAt) { return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime(); } const credentialsSymbol = Symbol("backstage-credentials"); const limitedCredentialsSymbol = Symbol("backstage-limited-credentials"); class DefaultHttpAuthService { #auth; #discovery; #pluginId; #getToken; constructor(auth, discovery, pluginId, getToken) { this.#auth = auth; this.#discovery = discovery; this.#pluginId = pluginId; this.#getToken = getToken ?? getTokenFromRequest; } static create(options) { return new DefaultHttpAuthService( options.auth, options.discovery, options.pluginId, options.getTokenFromRequest ); } async #extractCredentialsFromRequest(req) { const { token } = this.#getToken(req); if (!token) { return await this.#auth.getNoneCredentials(); } return await this.#auth.authenticate(token); } async #extractLimitedCredentialsFromRequest(req) { const { token } = this.#getToken(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) { return req[credentialsSymbol] ??= this.#extractCredentialsFromRequest(req); } async #getLimitedCredentials(req) { return req[limitedCredentialsSymbol] ??= this.#extractLimitedCredentialsFromRequest(req); } async credentials(req, options) { const credentials = options?.allowLimitedAccess ? await this.#getLimitedCredentials(req) : await this.#getCredentials(req); const allowed = options?.allow; if (!allowed) { return credentials; } if (this.#auth.isPrincipal(credentials, "none")) { if (allowed.includes("none")) { return credentials; } throw new errors.AuthenticationError("Missing credentials"); } else if (this.#auth.isPrincipal(credentials, "user")) { if (allowed.includes("user")) { return credentials; } throw new errors.NotAllowedError( `This endpoint does not allow 'user' credentials` ); } else if (this.#auth.isPrincipal(credentials, "service")) { if (allowed.includes("service")) { return credentials; } throw new errors.NotAllowedError( `This endpoint does not allow 'service' credentials` ); } throw new errors.NotAllowedError( "Unknown principal type, this should never happen" ); } async issueUserCookie(res, options) { if (res.headersSent) { throw new Error("Failed to issue user cookie, headers were already sent"); } let credentials; if (options?.credentials) { if (this.#auth.isPrincipal(options.credentials, "none")) { res.clearCookie( BACKSTAGE_AUTH_COOKIE, await this.#getCookieOptions(res.req) ); return { expiresAt: /* @__PURE__ */ new Date() }; } if (!this.#auth.isPrincipal(options.credentials, "user")) { throw new errors.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) { 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) { const existingCookie = getCookieFromRequest(req); if (!existingCookie) { return void 0; } try { const existingCredentials = await this.#auth.authenticate( existingCookie, { allowLimitedAccess: true } ); if (!this.#auth.isPrincipal(existingCredentials, "user")) { return void 0; } return existingCredentials.expiresAt; } catch (error) { if (error.name === "AuthenticationError") { return void 0; } throw error; } } } const httpAuthServiceFactory = backendPluginApi.createServiceFactory({ service: backendPluginApi.coreServices.httpAuth, deps: { auth: backendPluginApi.coreServices.auth, discovery: backendPluginApi.coreServices.discovery, plugin: backendPluginApi.coreServices.pluginMetadata }, async factory({ auth, discovery, plugin }) { return DefaultHttpAuthService.create({ auth, discovery, pluginId: plugin.getId() }); } }); exports.DefaultHttpAuthService = DefaultHttpAuthService; exports.httpAuthServiceFactory = httpAuthServiceFactory; //# sourceMappingURL=httpAuthServiceFactory.cjs.js.map