@backstage/backend-defaults
Version:
Backend defaults used by Backstage backend apps
206 lines (202 loc) • 6.41 kB
JavaScript
'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