UNPKG

@criipto/verify-express

Version:

Accept MitID, NemID, Swedish BankID, Norwegian BankID and more logins in your Node.js app using Passport or plain Express.js

232 lines 10.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CriiptoVerifyRedirectPassportStrategy = exports.CriiptoVerifyExpressRedirect = exports.CriiptoVerifyJwtPassportStrategy = exports.CriiptoVerifyExpressJwt = void 0; require("./fetch-polyfill"); const oidc_1 = require("@criipto/oidc"); const jose_1 = require("jose"); const utils_1 = require("./utils"); const debug = require('debug')('@criipto/verify-express'); const errorDebug = require('debug')('@criipto/verify-express:error'); const clockTolerance = 5 * 60; class OAuth2Error extends Error { constructor(error, error_description, state) { super(error + (error_description ? ` (${error_description})` : '')); this.name = "OAuth2Error"; this.error = error; this.error_description = error_description; this.state = state; } } exports.default = OAuth2Error; class CriiptoVerifyExpressJwt { constructor(options) { this.options = options; this.jwks = (0, jose_1.createRemoteJWKSet)(new URL(`https://${options.domain}/.well-known/jwks`)); this.configurationManager = new oidc_1.OpenIDConfigurationManager(`https://${options.domain}`, options.clientID, utils_1.memoryStorage); } async process(req) { const jwt = (0, utils_1.extractBearerToken)(req); if (!jwt) return null; try { const { payload } = await (0, jose_1.jwtVerify)(jwt, this.jwks, { issuer: `https://${this.options.domain}`, audience: this.options.clientID, clockTolerance }); return payload; } catch (err) { errorDebug(`Error verifying JWT: ${err.toString()}`); if (err instanceof jose_1.errors.JWTClaimValidationFailed || err instanceof jose_1.errors.JWSInvalid || err instanceof jose_1.errors.JWTInvalid) { return null; } throw err; } } middleware() { return (req, res, next) => { this.process(req).then((payload) => { if (payload) { req.claims = payload; next(); return; } return res.sendStatus(401); }) .catch(err => { errorDebug(err); next(err); }); }; } } exports.CriiptoVerifyExpressJwt = CriiptoVerifyExpressJwt; class CriiptoVerifyJwtPassportStrategy { constructor(options, claimsToUser) { this.options = options; this.claimsToUser = claimsToUser; this.helper = new CriiptoVerifyExpressJwt(options); } authenticate(req) { this.helper.process(req) .then(this.claimsToUser.bind(this)) .then(this.success) .catch(err => { debug(err); this.fail(err); }); } } exports.CriiptoVerifyJwtPassportStrategy = CriiptoVerifyJwtPassportStrategy; class CriiptoVerifyExpressRedirect { constructor(options) { this.options = options; this.jwks = (0, jose_1.createRemoteJWKSet)(new URL(`https://${options.domain}/.well-known/jwks`)); this.configurationManager = new oidc_1.OpenIDConfigurationManager(`https://${options.domain}`, options.clientID, utils_1.memoryStorage); } async logout(req, res) { req.session.verifyClaims = null; const protocol = req.protocol; const strategyOptions = this.options; const postLogoutRedirectUri = strategyOptions.postLogoutRedirectUri ?? '/'; const redirectUri = new URL(postLogoutRedirectUri.startsWith('http') ? postLogoutRedirectUri : `${protocol}://${req.get('host')}${postLogoutRedirectUri}`); const configuration = await this.configurationManager.fetch(); const logoutUrl = (0, oidc_1.buildLogoutURL)(configuration, { post_logout_redirect_uri: redirectUri.href }); res.redirect(logoutUrl.href); } async handleCode(req, redirectUri) { if (req.query.error) { throw new OAuth2Error(req.query.error, req.query.error_description, req.query.state); } if (req.query.code) { const code = req.query.code; if (!redirectUri) throw new Error('Bad session state'); const configuration = await this.configurationManager.fetch(); const codeResponse = await (0, oidc_1.codeExchange)(configuration, { redirect_uri: redirectUri, code, client_secret: this.options.clientSecret }); if ("error" in codeResponse) { throw new OAuth2Error(codeResponse.error, codeResponse.error_description, codeResponse.state); } const { payload } = await (0, jose_1.jwtVerify)(codeResponse.id_token, this.jwks, { issuer: `https://${this.options.domain}`, audience: this.options.clientID, clockTolerance }); return payload; } return null; } async authorizeURL(req, returnTo) { const protocol = req.protocol; const redirectUri = new URL(this.options.redirectUri.startsWith('http') ? this.options.redirectUri : `${protocol}://${req.get('host')}${this.options.redirectUri}`); if (returnTo) { redirectUri.searchParams.set('returnTo', returnTo); } const configuration = await this.configurationManager.fetch(); const beforeAuthorize = this.options.beforeAuthorize ?? ((r, i) => i); const authorizeUrl = (0, oidc_1.buildAuthorizeURL)(configuration, beforeAuthorize(req, { scope: 'openid', redirect_uri: redirectUri.href, response_mode: 'query', response_type: 'code' })); authorizeUrl.searchParams.set('criipto_sdk', utils_1.CRIIPTO_SDK); return { authorizeUrl, redirectUri }; } middleware(options) { return (req, res, next) => { const strategyOptions = this.options; const force = options?.force || false; if (!req.session) throw new Error('express-session is required when using redirect'); Promise.resolve().then(async () => { const claimsJson = req.session.verifyClaims; if (claimsJson) { const claims = JSON.parse(claimsJson); req.claims = claims; if (!force) { return next(); } } const payload = await this.handleCode(req, req.session.verifyRedirectUri); if (payload) { req.claims = payload; req.session.verifyClaims = JSON.stringify(payload); req.session.touch(); if (options.successReturnToOrRedirect) { const returnTo = req.query.returnTo ?? options.successReturnToOrRedirect; return res.redirect(returnTo); } return next(); } const { authorizeUrl, redirectUri } = await this.authorizeURL(req, req.url !== strategyOptions.redirectUri ? undefined : req.url); req.session.verifyRedirectUri = redirectUri.href, req.session.touch(); res.redirect(authorizeUrl.href); }) .catch(err => { errorDebug(err); const failureRedirect = options.failureRedirect ?? '/'; if (err instanceof OAuth2Error) { return res.redirect(`${failureRedirect}?error=${encodeURIComponent(err.error)}&error_description=${encodeURIComponent(err.error_description || '')}&state=${err.state || ''}`); } return res.redirect(`${failureRedirect}?error=${encodeURIComponent(err.toString())}`); }); }; } } exports.CriiptoVerifyExpressRedirect = CriiptoVerifyExpressRedirect; class CriiptoVerifyRedirectPassportStrategy { constructor(options, claimsToUser) { this.options = options; this.claimsToUser = claimsToUser; this.jwks = (0, jose_1.createRemoteJWKSet)(new URL(`https://${options.domain}/.well-known/jwks`)); this.configurationManager = new oidc_1.OpenIDConfigurationManager(`https://${options.domain}`, options.clientID, utils_1.memoryStorage); this.helper = new CriiptoVerifyExpressRedirect(options); this.helper.configurationManager = this.configurationManager; } logout(req, res) { req.logout(async () => { this.helper.logout(req, res); }); } authenticate(req, options) { const strategyOptions = this.options; const force = options?.force || false; const isAuthenticated = req.isAuthenticated(); if (!force && isAuthenticated) return this.pass(); Promise.resolve().then(async () => { const protocol = req.protocol; const redirectUri = new URL(strategyOptions.redirectUri.startsWith('http') ? strategyOptions.redirectUri : `${protocol}://${req.get('host')}${strategyOptions.redirectUri}`); const payload = await this.helper.handleCode(req, redirectUri.href); if (payload) { const user = await this.claimsToUser(payload); return this.success(user); } const { authorizeUrl } = await this.helper.authorizeURL(req, undefined); this.redirect(authorizeUrl.href); }) .catch(err => { errorDebug(err); if (options.failureRedirect) { if (err instanceof OAuth2Error) { return this.redirect(`${options.failureRedirect}?error=${encodeURIComponent(err.error)}&error_description=${encodeURIComponent(err.error_description || '')}&state=${err.state || ''}`); } return this.redirect(`${options.failureRedirect}?error=${encodeURIComponent(err.toString())}`); } else { this.fail(err); } }); } } exports.CriiptoVerifyRedirectPassportStrategy = CriiptoVerifyRedirectPassportStrategy; //# sourceMappingURL=index.js.map