express-openid-connect
Advanced tools
Comparing version 2.11.0 to 2.12.0
@@ -178,2 +178,14 @@ // Type definitions for express-openid-connect | ||
logout: (opts?: LogoutOptions) => Promise<void>; | ||
/** | ||
* Provided by default via the `/callback` route. Call this to override or have other | ||
* callback routes with | ||
* | ||
* ```js | ||
* app.get('/callback', (req, res) => { | ||
* res.oidc.callback({ redirectUri: 'https://example.com/callback' }); | ||
* }); | ||
* ``` | ||
*/ | ||
callback: (opts?: CallbackOptions) => Promise<void>; | ||
} | ||
@@ -201,3 +213,5 @@ | ||
/** | ||
* Override the default {@link ConfigParams.authorizationParams authorizationParams} | ||
* Override the default {@link ConfigParams.authorizationParams authorizationParams}, if also passing a custom callback | ||
* route then {@link AuthorizationParameters.redirect_uri redirect_uri} must be provided here or in | ||
* {@link ConfigParams.authorizationParams config} | ||
*/ | ||
@@ -232,2 +246,15 @@ authorizationParams?: AuthorizationParameters; | ||
interface CallbackOptions { | ||
/** | ||
* This is useful to specify in addition to {@link ConfigParams.baseURL} when your app runs on multiple domains, | ||
* it should match {@link LoginOptions.authorizationParams.redirect_uri} | ||
*/ | ||
redirectUri: string; | ||
/** | ||
* Additional request body properties to be sent to the `token_endpoint. | ||
*/ | ||
tokenEndpointParams?: TokenParameters; | ||
} | ||
/** | ||
@@ -661,3 +688,3 @@ * Configuration parameters passed to the `auth()` middleware. | ||
*/ | ||
signSessionStoreCookie: boolean; | ||
signSessionStoreCookie?: boolean; | ||
@@ -677,3 +704,3 @@ /** | ||
*/ | ||
requireSignedSessionStoreCookie: boolean; | ||
requireSignedSessionStoreCookie?: boolean; | ||
@@ -680,0 +707,0 @@ /** |
@@ -21,4 +21,2 @@ const { Issuer, custom } = require('openid-client'); | ||
const getIssuer = memoize((issuer) => Issuer.discover(issuer)); | ||
async function get(config) { | ||
@@ -45,3 +43,3 @@ const defaultHttpOptions = (options) => { | ||
applyHttpOptionsCustom(Issuer); | ||
const issuer = await getIssuer(config.issuerBaseURL); | ||
const issuer = await Issuer.discover(config.issuerBaseURL); | ||
applyHttpOptionsCustom(issuer); | ||
@@ -103,2 +101,5 @@ | ||
token_endpoint_auth_method: config.clientAuthMethod, | ||
...(config.clientAssertionSigningAlg && { | ||
token_endpoint_auth_signing_alg: config.clientAssertionSigningAlg, | ||
}), | ||
}, | ||
@@ -105,0 +106,0 @@ jwks |
@@ -197,3 +197,6 @@ const Joi = require('joi'); | ||
]).default('/logout'), | ||
callback: Joi.string().uri({ relativeOnly: true }).default('/callback'), | ||
callback: Joi.alternatives([ | ||
Joi.string().uri({ relativeOnly: true }), | ||
Joi.boolean().valid(false), | ||
]).default('/callback'), | ||
postLogoutRedirect: Joi.string().uri({ allowRelative: true }).default(''), | ||
@@ -200,0 +203,0 @@ }) |
@@ -6,2 +6,3 @@ const url = require('url'); | ||
const { strict: assert } = require('assert'); | ||
const createError = require('http-errors'); | ||
@@ -11,5 +12,12 @@ const debug = require('./debug')('context'); | ||
const { get: getClient } = require('./client'); | ||
const { encodeState } = require('../lib/hooks/getLoginState'); | ||
const { cancelSilentLogin } = require('../middleware/attemptSilentLogin'); | ||
const { encodeState, decodeState } = require('../lib/hooks/getLoginState'); | ||
const { | ||
cancelSilentLogin, | ||
resumeSilentLogin, | ||
} = require('../middleware/attemptSilentLogin'); | ||
const weakRef = require('./weakCache'); | ||
const { | ||
regenerateSessionStoreId, | ||
replaceSession, | ||
} = require('../lib/appSession'); | ||
@@ -165,3 +173,5 @@ function isExpired() { | ||
const { config } = weakRef(this); | ||
return urlJoin(config.baseURL, config.routes.callback); | ||
if (config.routes.callback) { | ||
return urlJoin(config.baseURL, config.routes.callback); | ||
} | ||
} | ||
@@ -211,3 +221,3 @@ | ||
} | ||
stateValue.nonce = transient.generateNonce(); | ||
if (options.silent) { | ||
@@ -217,11 +227,2 @@ stateValue.attemptingSilentLogin = true; | ||
const usePKCE = | ||
options.authorizationParams.response_type.includes('code'); | ||
if (usePKCE) { | ||
debug( | ||
'response_type includes code, the authorization request will use PKCE' | ||
); | ||
stateValue.code_verifier = transient.generateCodeVerifier(); | ||
} | ||
const validResponseTypes = ['id_token', 'code id_token', 'code']; | ||
@@ -246,2 +247,3 @@ assert( | ||
}; | ||
const authParams = { | ||
@@ -252,4 +254,9 @@ ...options.authorizationParams, | ||
const usePKCE = | ||
options.authorizationParams.response_type.includes('code'); | ||
if (usePKCE) { | ||
authVerification.code_verifier = transient.generateNonce(); | ||
debug( | ||
'response_type includes code, the authorization request will use PKCE' | ||
); | ||
authVerification.code_verifier = transient.generateCodeVerifier(); | ||
@@ -319,4 +326,75 @@ authParams.code_challenge_method = 'S256'; | ||
} | ||
async callback(options = {}) { | ||
let { config, req, res, transient, next } = weakRef(this); | ||
next = once(next); | ||
try { | ||
const client = await getClient(config); | ||
const redirectUri = options.redirectUri || this.getRedirectUri(); | ||
let tokenSet; | ||
try { | ||
const callbackParams = client.callbackParams(req); | ||
const authVerification = transient.getOnce( | ||
config.transactionCookie.name, | ||
req, | ||
res | ||
); | ||
const checks = authVerification ? JSON.parse(authVerification) : {}; | ||
req.openidState = decodeState(checks.state); | ||
tokenSet = await client.callback(redirectUri, callbackParams, checks, { | ||
exchangeBody: { | ||
...(config && config.tokenEndpointParams), | ||
...options.tokenEndpointParams, | ||
}, | ||
}); | ||
} catch (error) { | ||
throw createError(400, error.message, { | ||
error: error.error, | ||
error_description: error.error_description, | ||
}); | ||
} | ||
let session = Object.assign({}, tokenSet); // Remove non-enumerable methods from the TokenSet | ||
if (config.afterCallback) { | ||
session = await config.afterCallback( | ||
req, | ||
res, | ||
session, | ||
req.openidState | ||
); | ||
} | ||
if (req.oidc.isAuthenticated()) { | ||
if (req.oidc.user.sub === tokenSet.claims().sub) { | ||
// If it's the same user logging in again, just update the existing session. | ||
Object.assign(req[config.session.name], session); | ||
} else { | ||
// If it's a different user, replace the session to remove any custom user | ||
// properties on the session | ||
replaceSession(req, session, config); | ||
// And regenerate the session id so the previous user wont know the new user's session id | ||
regenerateSessionStoreId(req, config); | ||
} | ||
} else { | ||
// If a new user is replacing an anonymous session, update the existing session to keep | ||
// any anonymous session state (eg. checkout basket) | ||
Object.assign(req[config.session.name], session); | ||
// But update the session store id so a previous anonymous user wont know the new user's session id | ||
regenerateSessionStoreId(req, config); | ||
} | ||
resumeSilentLogin(req, res); | ||
} catch (err) { | ||
if (!req.openidState || !req.openidState.attemptingSilentLogin) { | ||
return next(err); | ||
} | ||
} | ||
res.redirect(req.openidState.returnTo || config.baseURL); | ||
} | ||
} | ||
module.exports = { RequestContext, ResponseContext }; |
const express = require('express'); | ||
const createError = require('http-errors'); | ||
const debug = require('../lib/debug')('auth'); | ||
const { once } = require('../lib/once'); | ||
const { get: getConfig } = require('../lib/config'); | ||
const { get: getClient } = require('../lib/client'); | ||
const { requiresAuth } = require('./requiresAuth'); | ||
@@ -13,4 +10,2 @@ const attemptSilentLogin = require('./attemptSilentLogin'); | ||
const appSession = require('../lib/appSession'); | ||
const { regenerateSessionStoreId, replaceSession } = appSession; | ||
const { decodeState } = require('../lib/hooks/getLoginState'); | ||
@@ -64,118 +59,12 @@ const enforceLeadingSlash = (path) => { | ||
// Callback route, configured with routes.callback. | ||
{ | ||
let client; | ||
if (config.routes.callback) { | ||
const path = enforceLeadingSlash(config.routes.callback); | ||
const callbackStack = [ | ||
(req, res, next) => { | ||
debug('%s %s called', req.method, path); | ||
next(); | ||
}, | ||
async (req, res, next) => { | ||
next = once(next); | ||
client = | ||
client || | ||
(await getClient(config).catch((err) => { | ||
next(err); | ||
})); | ||
if (!client) { | ||
return; | ||
} | ||
try { | ||
const redirectUri = res.oidc.getRedirectUri(); | ||
let tokenSet; | ||
try { | ||
const callbackParams = client.callbackParams(req); | ||
const authVerification = transient.getOnce( | ||
config.transactionCookie.name, | ||
req, | ||
res | ||
); | ||
const { max_age, code_verifier, nonce, state } = authVerification | ||
? JSON.parse(authVerification) | ||
: {}; | ||
req.openidState = decodeState(state); | ||
const checks = { | ||
max_age, | ||
code_verifier, | ||
nonce, | ||
state, | ||
}; | ||
let extras; | ||
if (config.tokenEndpointParams) { | ||
extras = { exchangeBody: config.tokenEndpointParams }; | ||
} | ||
tokenSet = await client.callback( | ||
redirectUri, | ||
callbackParams, | ||
checks, | ||
extras | ||
); | ||
} catch (err) { | ||
throw createError(400, err.message, { | ||
error: err.error, | ||
error_description: err.error_description, | ||
}); | ||
} | ||
let session = Object.assign({}, tokenSet); // Remove non-enumerable methods from the TokenSet | ||
if (config.afterCallback) { | ||
session = await config.afterCallback( | ||
req, | ||
res, | ||
session, | ||
req.openidState | ||
); | ||
} | ||
if (req.oidc.isAuthenticated()) { | ||
if (req.oidc.user.sub === tokenSet.claims().sub) { | ||
// If it's the same user logging in again, just update the existing session. | ||
Object.assign(req[config.session.name], session); | ||
} else { | ||
// If it's a different user, replace the session to remove any custom user | ||
// properties on the session | ||
replaceSession(req, session, config); | ||
// And regenerate the session id so the previous user wont know the new user's session id | ||
regenerateSessionStoreId(req, config); | ||
} | ||
} else { | ||
// If a new user is replacing an anonymous session, update the existing session to keep | ||
// any anonymous session state (eg. checkout basket) | ||
Object.assign(req[config.session.name], session); | ||
// But update the session store id so a previous anonymous user wont know the new user's session id | ||
regenerateSessionStoreId(req, config); | ||
} | ||
attemptSilentLogin.resumeSilentLogin(req, res); | ||
next(); | ||
} catch (err) { | ||
// Swallow errors if this is a silentLogin | ||
if (req.openidState && req.openidState.attemptingSilentLogin) { | ||
next(); | ||
} else { | ||
next(err); | ||
} | ||
} | ||
}, | ||
(req, res) => res.redirect(req.openidState.returnTo || config.baseURL), | ||
]; | ||
debug('adding GET %s route', path); | ||
router.get(path, ...callbackStack); | ||
router.get(path, (req, res) => res.oidc.callback()); | ||
debug('adding POST %s route', path); | ||
router.post( | ||
path, | ||
express.urlencoded({ extended: false }), | ||
...callbackStack | ||
router.post(path, express.urlencoded({ extended: false }), (req, res) => | ||
res.oidc.callback() | ||
); | ||
} else { | ||
debug('callback handling route not applied'); | ||
} | ||
@@ -182,0 +71,0 @@ |
{ | ||
"name": "express-openid-connect", | ||
"version": "2.11.0", | ||
"version": "2.12.0", | ||
"description": "Express middleware to protect web applications using OpenID Connect.", | ||
@@ -19,2 +19,3 @@ "homepage": "https://github.com/auth0/express-openid-connect", | ||
"test:ci": "nyc --reporter=lcov npm test", | ||
"test:types": "tsd .", | ||
"docs": "typedoc --options typedoc.js index.d.ts", | ||
@@ -55,3 +56,3 @@ "test:end-to-end": "mocha end-to-end" | ||
"memorystore": "^1.6.4", | ||
"mocha": "^7.2.0", | ||
"mocha": "^10.2.0", | ||
"nock": "^11.9.1", | ||
@@ -67,2 +68,3 @@ "nyc": "^15.1.0", | ||
"sinon": "^7.5.0", | ||
"tsd": "^0.25.0", | ||
"typedoc": "^0.23.18", | ||
@@ -69,0 +71,0 @@ "typescript": "^4.8.4" |
91719
2550
26