UNPKG

@atlassian/atlassian-connect-express

Version:

Library for building Atlassian Add-ons on top of Express

114 lines (103 loc) 3.74 kB
const jose = require("jose"); const _ = require("lodash"); function authenticate(addon) { if (!addon.config.appId()) { throw new Error( "In order to use Forge authentication middleware, the app ID must be configured under" + "\n`appId` in config.json, or via the `AC_APP_ID` environment variable." + "\nThe app ID can be found in your `manifest.yml`" ); } return async function (req, res, next) { function sendError(msg) { const code = 401; addon.logger.error("Authentication failed:", code, msg); if (addon.config.expressErrorHandling()) { next({ code, message: msg }); } else { res.status(code).send(_.escape(msg)); } } function getJwksUrl(forgeInvocationToken) { const commercialStageBaseUrl = "https://api.stg.atlassian.com/"; const agcProdBaseUrl = "https://api.atlassian-us-gov-mod.com/"; const agcStageBaseUrl = "https://api.stg.atlassian-us-gov-mod.com/"; const decodedClaims = jose.decodeJwt(forgeInvocationToken, { complete: true }); let apiBaseUrl; if (decodedClaims && decodedClaims.app && decodedClaims.app.apiBaseUrl) { apiBaseUrl = decodedClaims.app.apiBaseUrl; } else { apiBaseUrl = ""; } if (apiBaseUrl.startsWith(commercialStageBaseUrl)) { return "https://forge.cdn.stg.atlassian-dev.net/.well-known/jwks.json"; } else if (apiBaseUrl.startsWith(agcProdBaseUrl)) { return "https://forge.cdn.prod.atlassian-dev-us-gov-mod.net/.well-known/jwks.json"; } else if (apiBaseUrl.startsWith(agcStageBaseUrl)) { return "https://forge.cdn.stg.atlassian-dev-us-gov-mod.net/.well-known/jwks.json"; } else { return "https://forge.cdn.prod.atlassian-dev.net/.well-known/jwks.json"; } } const verifyToken = async (req, appId) => { const ariPrefix = "ari:cloud:ecosystem::app/"; const appIdList = (Array.isArray(appId) ? appId : appId.split(" ")).map( id => (id.startsWith(ariPrefix) ? id : ariPrefix + id) ); let forgeInvocationToken; const authHeader = req.headers.authorization; if (authHeader) { if (authHeader.toLowerCase().startsWith("bearer")) { [, forgeInvocationToken] = authHeader.split(" "); } } if (!forgeInvocationToken) { throw new Error("No valid auth token provided in the request"); } const jwksUrl = getJwksUrl(forgeInvocationToken); const JWKS = jose.createRemoteJWKSet(new URL(jwksUrl)); const payload = await jose.jwtVerify(forgeInvocationToken, JWKS, { audience: appIdList, issuer: "forge/invocation-token", clockTolerance: addon.config.clockTolerance() }); return payload; }; try { const payload = await verifyToken(req, addon.config.appId()); const innerPayload = payload.payload || {}; const { installationId, apiBaseUrl } = innerPayload.app; const { "x-forge-oauth-system": appToken, "x-forge-oauth-user": userToken } = req.headers; await addon.forgeAppTokenCache.setForgeAppToken( installationId, appToken, apiBaseUrl ); req.context = req.context || {}; req.context.forge = {}; Object.assign(req.context.forge, { tokens: { app: appToken, user: userToken }, ...innerPayload, apiBaseUrl }); next(); } catch (err) { console.error("Error while verifying Forge Invocation Token:", err); sendError("Forge Invocation Token verification was unsuccessful"); } }; } module.exports = { authenticate };