const Joi = require('joi'); const crypto = require('crypto'); const { defaultState: getLoginState } = require('./hooks/getLoginState'); const isHttps = /^https:/i; const defaultSessionIdGenerator = () => crypto.randomBytes(16).toString('hex'); const paramsSchema = Joi.object({ secret: Joi.alternatives([ Joi.string().min(8), Joi.binary().min(8), Joi.array().items(Joi.string().min(8), Joi.binary().min(8)), ]).required(), session: Joi.object({ rolling: Joi.boolean().optional().default(true), rollingDuration: Joi.when(Joi.ref('rolling'), { is: true, then: Joi.number().integer().messages({ 'number.base': '"session.rollingDuration" must be provided an integer value when "session.rolling" is true', }), otherwise: Joi.boolean().valid(false).messages({ 'any.only': '"session.rollingDuration" must be false when "session.rolling" is disabled', }), }) .optional() .default((parent) => (parent.rolling ? 24 * 60 * 60 : false)), // 1 day when rolling is enabled, else false absoluteDuration: Joi.when(Joi.ref('rolling'), { is: false, then: Joi.number().integer().messages({ 'number.base': '"session.absoluteDuration" must be provided an integer value when "session.rolling" is false', }), otherwise: Joi.alternatives([ Joi.number().integer(), Joi.boolean().valid(false), ]), }) .optional() .default(7 * 24 * 60 * 60), // 7 days, name: Joi.string() .pattern(/^[0-9a-zA-Z_.-]+$/, { name: 'cookie name' }) .optional() .default('appSession'), store: Joi.object() .optional() .when(Joi.ref('/backchannelLogout'), { not: false, then: Joi.when('/backchannelLogout.store', { not: Joi.exist(), then: Joi.when('/backchannelLogout.isLoggedOut', { not: Joi.exist(), then: Joi.object().required().messages({ 'any.required': `Back-Channel Logout requires a "backchannelLogout.store" (you can also reuse "session.store" if you have stateful sessions) or custom hooks for "isLoggedOut" and "onLogoutToken".`, }), }), }), }), genid: Joi.function() .maxArity(1) .optional() .default(() => defaultSessionIdGenerator), signSessionStoreCookie: Joi.boolean().optional().default(false), requireSignedSessionStoreCookie: Joi.boolean() .optional() .default(Joi.ref('signSessionStoreCookie')), cookie: Joi.object({ domain: Joi.string().optional(), transient: Joi.boolean().optional().default(false), httpOnly: Joi.boolean().optional().default(true), sameSite: Joi.string() .valid('Lax', 'Strict', 'None') .optional() .default('Lax'), secure: Joi.when(Joi.ref('/baseURL'), { is: Joi.string().pattern(isHttps), then: Joi.boolean() .default(true) .custom((value, { warn }) => { if (!value) warn('insecure.cookie'); return value; }) .messages({ 'insecure.cookie': "Setting your cookie to insecure when over https is not recommended, I hope you know what you're doing.", }), otherwise: Joi.boolean().valid(false).default(false).messages({ 'any.only': 'Cookies set with the `Secure` property wont be attached to http requests', }), }), path: Joi.string().uri({ relativeOnly: true }).optional(), }) .default() .unknown(false), }) .default() .unknown(false), transactionCookie: Joi.object({ sameSite: Joi.string() .valid('Lax', 'Strict', 'None') .optional() .default(Joi.ref('...session.cookie.sameSite')), name: Joi.string().optional().default('auth_verification'), }) .default() .unknown(false), auth0Logout: Joi.boolean().optional(), tokenEndpointParams: Joi.object().optional(), authorizationParams: Joi.object({ response_type: Joi.string() .optional() .valid('id_token', 'code id_token', 'code') .default('id_token'), scope: Joi.string() .optional() .pattern(/\bopenid\b/, 'contains openid') .default('openid profile email'), response_mode: Joi.string() .optional() .when('response_type', { is: 'code', then: Joi.valid('query', 'form_post'), otherwise: Joi.valid('form_post').default('form_post'), }), }) .optional() .unknown(true) .default(), logoutParams: Joi.object().optional(), backchannelLogout: Joi.alternatives([ Joi.object({ store: Joi.object().optional(), onLogin: Joi.alternatives([ Joi.function(), Joi.boolean().valid(false), ]).optional(), isLoggedOut: Joi.alternatives([ Joi.function(), Joi.boolean().valid(false), ]).optional(), onLogoutToken: Joi.function().optional(), }), Joi.boolean(), ]).default(false), baseURL: Joi.string() .uri() .required() .when(Joi.ref('authorizationParams.response_mode'), { is: 'form_post', then: Joi.string().pattern(isHttps).rule({ warn: true, message: `Using 'form_post' for response_mode may cause issues for you logging in over http, see https://github.com/auth0/express-openid-connect/blob/master/FAQ.md`, }), }), clientID: Joi.string().required(), clientSecret: Joi.string() .when( Joi.ref('clientAuthMethod', { adjust: (value) => value && value.includes('client_secret'), }), { is: true, then: Joi.string().required().messages({ 'any.required': `"clientSecret" is required for the "clientAuthMethod" "{{clientAuthMethod}}"`, }), } ) .when( Joi.ref('idTokenSigningAlg', { adjust: (value) => value && value.startsWith('HS'), }), { is: true, then: Joi.string().required().messages({ 'any.required': '"clientSecret" is required for ID tokens with HMAC based algorithms', }), } ), clockTolerance: Joi.number().optional().default(60), enableTelemetry: Joi.boolean().optional().default(true), errorOnRequiredAuth: Joi.boolean().optional().default(false), attemptSilentLogin: Joi.boolean().optional().default(false), getLoginState: Joi.function() .optional() .default(() => getLoginState), afterCallback: Joi.function().optional(), identityClaimFilter: Joi.array() .optional() .default([ 'aud', 'iss', 'iat', 'exp', 'nbf', 'nonce', 'azp', 'auth_time', 's_hash', 'at_hash', 'c_hash', ]), idpLogout: Joi.boolean() .optional() .default((parent) => parent.auth0Logout || false), idTokenSigningAlg: Joi.string() .insensitive() .not('none') .optional() .default('RS256'), issuerBaseURL: Joi.string().uri().required(), legacySameSiteCookie: Joi.boolean().optional().default(true), authRequired: Joi.boolean().optional().default(true), pushedAuthorizationRequests: Joi.boolean().optional().default(false), routes: Joi.object({ login: Joi.alternatives([ Joi.string().uri({ relativeOnly: true }), Joi.boolean().valid(false), ]).default('/login'), logout: Joi.alternatives([ Joi.string().uri({ relativeOnly: true }), Joi.boolean().valid(false), ]).default('/logout'), callback: Joi.alternatives([ Joi.string().uri({ relativeOnly: true }), Joi.boolean().valid(false), ]).default('/callback'), postLogoutRedirect: Joi.string().uri({ allowRelative: true }).default(''), backchannelLogout: Joi.string() .uri({ allowRelative: true }) .default('/backchannel-logout'), }) .default() .unknown(false), clientAuthMethod: Joi.string() .valid( 'client_secret_basic', 'client_secret_post', 'client_secret_jwt', 'private_key_jwt', 'none' ) .optional() .default((parent) => { if ( parent.authorizationParams.response_type === 'id_token' && !parent.pushedAuthorizationRequests ) { return 'none'; } if (parent.clientAssertionSigningKey) { return 'private_key_jwt'; } return 'client_secret_basic'; }) .when( Joi.ref('authorizationParams.response_type', { adjust: (value) => value && value.includes('code'), }), { is: true, then: Joi.string().invalid('none').messages({ 'any.only': 'Public code flow clients are not supported.', }), } ) .when(Joi.ref('pushedAuthorizationRequests'), { is: true, then: Joi.string().invalid('none').messages({ 'any.only': 'Public PAR clients are not supported.', }), }), clientAssertionSigningKey: Joi.any() .optional() .when(Joi.ref('clientAuthMethod'), { is: 'private_key_jwt', then: Joi.any().required().messages({ 'any.required': '"clientAssertionSigningKey" is required for a "clientAuthMethod" of "private_key_jwt"', }), }), // | | | , clientAssertionSigningAlg: Joi.string() .valid( 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES256K', 'ES384', 'ES512', 'EdDSA' ) .optional(), discoveryCacheMaxAge: Joi.number() .optional() .min(0) .default(10 * 60 * 1000), httpTimeout: Joi.number().optional().min(500).default(5000), httpUserAgent: Joi.string().optional(), httpAgent: Joi.object().optional(), }); module.exports.get = function (config = {}) { config = { secret: process.env.SECRET, issuerBaseURL: process.env.ISSUER_BASE_URL, baseURL: process.env.BASE_URL, clientID: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, ...config, }; const { value, error, warning } = paramsSchema.validate(config); if (error) { throw new TypeError(error.details[0].message); } if (warning) { console.warn(warning.message); } return value; };