Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

express-openid-connect

Package Overview
Dependencies
Maintainers
38
Versions
44
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

express-openid-connect - npm Package Compare versions

Comparing version 1.0.2 to 2.0.0-beta.0

lib/cookies.js

18

CHANGELOG.md
# CHANGELOG
## [2.0.0-beta.0](https://github.com/auth0/express-openid-connect/tree/v2.0.0-beta.0) (2020-08-31)
[Full Changelog](https://github.com/auth0/express-openid-connect/compare/v1.0.2...v2.0.0-beta.0)
For a full list of breaking changes and migration guide, checkout https://github.com/auth0/express-openid-connect/blob/master/V2_MIGRATION_GUIDE.md
**Breaking Changes**
- postLogoutRedirect and response_type check [#123](https://github.com/auth0/express-openid-connect/pull/123) ([adamjmcgrath](https://github.com/adamjmcgrath))
- Logout returnTo param [#115](https://github.com/auth0/express-openid-connect/pull/115) ([adamjmcgrath](https://github.com/adamjmcgrath))
- Session duration behaviour [#114](https://github.com/auth0/express-openid-connect/pull/114) ([adamjmcgrath](https://github.com/adamjmcgrath))
- Update Session cookie [#111](https://github.com/auth0/express-openid-connect/pull/111) ([adamjmcgrath](https://github.com/adamjmcgrath))
- Configuration and API updates [#109](https://github.com/auth0/express-openid-connect/pull/109) ([adamjmcgrath](https://github.com/adamjmcgrath))
- Update token set [#108](https://github.com/auth0/express-openid-connect/pull/108) ([adamjmcgrath](https://github.com/adamjmcgrath))
**Added**
- attemptSilentLogin feature [#121](https://github.com/auth0/express-openid-connect/pull/121) ([adamjmcgrath](https://github.com/adamjmcgrath))
- Add refresh method to access token [#124](https://github.com/auth0/express-openid-connect/pull/124) ([adamjmcgrath](https://github.com/adamjmcgrath))
- Architecture [#128](https://github.com/auth0/express-openid-connect/pull/128) ([adamjmcgrath](https://github.com/adamjmcgrath))
## [v1.0.2](https://github.com/auth0/express-openid-connect/tree/v1.0.2) (2020-05-12)

@@ -4,0 +22,0 @@ [Full Changelog](https://github.com/auth0/express-openid-connect/compare/v1.0.1...v1.0.2)

718

index.d.ts
// Type definitions for express-openid-connect
import { AuthorizationParameters, TokenSet, UserinfoResponse } from 'openid-client';
import { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express';
import {
AuthorizationParameters,
IdTokenClaims,
UserinfoResponse,
} from 'openid-client';
import { Request, Response, RequestHandler } from 'express';
/**
* The Express.js Request with `oidc` context added by the `auth` middleware.
*
* ```js
* app.use(auth());
*
* app.get('/profile', (req, res) => {
* const user = req.oidc.user;
* ...
* })
* ```
*/
interface OpenidRequest extends Request {
/**
* Library namespace for methods and data.
* See RequestContext and ResponseContext for how this is used.
*/
openid: object;
/**
* Library namespace for authentication methods and data.
*/
oidc: RequestContext;
}
/**
* Decoded state for use in config.handleCallback().
*/
openidState: object;
/**
* The Express.js Response with `oidc` context added by the `auth` middleware.
*
* ```js
* app.use(auth());
*
* app.get('/login', (req, res) => {
* res.oidc.login();
* })
* ```
*/
interface OpenidResponse extends Response {
/**
* Library namespace for authentication methods and data.
*/
oidc: ResponseContext;
}
/**
* Tokens for use in config.handleCallback().
*/
openidTokens: TokenSet;
/**
* The request authentication context found on the Express request when
* OpenID Connect auth middleware is added to your application.
*
* ```js
* app.use(auth());
*
* app.get('/profile', (req, res) => {
* const user = req.oidc.user;
* ...
* })
* ```
*/
interface RequestContext {
/**
* Method to check the user's authenticated state, returns `true` if logged in.
*/
isAuthenticated: () => boolean;
/**
* The OpenID Connect ID Token.
*
* See: https://auth0.com/docs/protocols/oidc#id-tokens
*/
idToken?: string;
/**
* Credentials that can be used by an application to access an API.
*
* See: https://auth0.com/docs/protocols/oidc#access-tokens
*/
accessToken?: AccessToken;
/**
* Credentials that can be used to refresh an access token.
*
* See: https://auth0.com/docs/tokens/concepts/refresh-tokens
*/
refreshToken?: string;
/**
* An object containing all the claims of the ID Token.
*/
idTokenClaims?: IdTokenClaims;
/**
* An object containing all the claims of the ID Token with the claims
* specified in {@link ConfigParams.identityClaimFilter identityClaimFilter} removed.
*/
user?: object;
/**
* Fetches the OIDC userinfo response.
*
* ```js
* app.use(auth());
*
* app.get('/user-info', async (req, res) => {
* const userInfo = await req.oidc.fetchUserInfo();
* res.json(userInfo);
* })
* ```
*
*/
fetchUserInfo(): Promise<UserinfoResponse>;
}
/**
* Configuration parameters passed to the auth() middleware.
* The response authentication context found on the Express response when
* OpenID Connect auth middleware is added to your application.
*
* ```js
* app.use(auth());
*
* app.get('/admin-login', (req, res) => {
* res.openid.login({ returnTo: '/admin' })
* })
* ```
*/
interface ResponseContext {
/**
* Provided by default via the `/login` route. Call this to override or have other
* login routes with custom {@link ConfigParams.authorizationParams authorizationParams} or returnTo
*
* ```js
* app.get('/admin-login', (req, res) => {
* res.oidc.login({
* returnTo: '/admin',
* authorizationParams: {
* scope: 'openid profile email admin:user',
* }
* });
* });
* ```
*/
login: (opts?: LoginOptions) => Promise<void>;
/**
* Provided by default via the `/logout` route. Call this to override or have other
* logout routes with custom returnTo
*
* ```js
* app.get('/admin-logout', (req, res) => {
* res.oidc.logout({ returnTo: '/admin-welcome' })
* });
* ```
*/
logout: (opts?: LogoutOptions) => Promise<void>;
}
/**
* Custom options to pass to login.
*/
interface LoginOptions {
/**
* Override the default {@link ConfigParams.authorizationParams authorizationParams}
*/
authorizationParams?: AuthorizationParameters;
/**
* URL to return to after login, overrides the Default is {@link Request.originalUrl}
*/
returnTo?: string;
}
/**
* Custom options to pass to logout.
*/
interface LogoutOptions {
/**
* URL to returnTo after logout, overrides the Default in {@link ConfigParams.routes.postLogoutRedirect routes.postLogoutRedirect}
*/
returnTo?: string;
}
/**
* Configuration parameters passed to the `auth()` middleware.
*
* {@link ConfigParams.issuerBaseURL issuerBaseURL}, {@link ConfigParams.baseURL baseURL}, {@link ConfigParams.clientID clientID}
* and {@link ConfigParams.secret secret} are required but can be configured with environmental variables:
*
* ```js
* ISSUER_BASE_URL=https://YOUR_DOMAIN
* CLIENT_ID=YOUR_CLIENT_ID
* BASE_URL=https://YOUR_APPLICATION_ROOT_URL
* SECRET=LONG_RANDOM_VALUE
* ```
*/
interface ConfigParams {
/**
* Object defining application session cookie attributes.
*/
appSession: boolean | AppSessionConfigParams;
/**
* REQUIRED. The secret(s) used to derive an encryption key for the user identity in a session cookie and
* to sign the transient cookies used by the login callback.
* Use a single string key or array of keys for an encrypted session cookie.
* Can use env key SECRET instead.
*/
secret?: string | Array<string>;
/**
* Boolean value to enable Auth0's logout feature.
*/
auth0Logout?: boolean;
/**
* Object defining application session cookie attributes.
*/
session?: boolean | SessionConfigParams;
/**
* URL parameters used when redirecting users to the authorization server to log in.
*/
authorizationParams?: AuthorizationParameters
/**
* Boolean value to enable Auth0's logout feature.
*/
auth0Logout?: boolean;
/**
* REQUIRED. The root URL for the application router.
* Can use env key BASE_URL instead.
*/
baseURL?: string;
/**
* URL parameters used when redirecting users to the authorization server to log in.
*
* If this property is not provided by your application, its default values will be:
*
* ```js
* {
* response_type: 'id_token',
* response_mode: 'form_post,
* scope: openid profile email'
* }
* ```
*
* New values can be passed in to change what is returned from the authorization server depending on your specific scenario.
*
* For example, to receive an access token for an API, you could initialize like the sample below. Note that `response_mode` can be omitted because the OAuth2 default mode of `query` is fine:
*
* ```js
* app.use(
* auth({
* authorizationParams: {
* response_type: 'code',
* scope: 'openid profile email read:reports',
* audience: 'https://your-api-identifier',
* },
* })
* );
* ```
*
* Additional custom parameters can be added as well:
*
* ```js
* app.use(auth({
* authorizationParams: {
* // Note: you need to provide required parameters if this object is set.
* response_type: "id_token",
* response_mode: "form_post",
* scope: "openid profile email"
* // Additional parameters
* acr_value: "tenant:test-tenant",
* custom_param: "custom-value"
* }
* }));
* ```
*/
authorizationParams?: AuthorizationParameters;
/**
* REQUIRED. The Client ID for your application.
* Can use env key CLIENT_ID instead.
*/
clientID?: string;
/**
* REQUIRED. The root URL for the application router, eg https://localhost
* Can use env key BASE_URL instead.
*/
baseURL?: string;
/**
* The Client Secret for your application.
* Required when requesting access tokens.
* Can use env key CLIENT_SECRET instead.
*/
clientSecret?: string;
/**
* REQUIRED. The Client ID for your application.
* Can use env key CLIENT_ID instead.
*/
clientID?: string;
/**
* Integer value for the system clock's tolerance (leeway) in seconds for ID token verification.
*/
clockTolerance?: number;
/**
* The Client Secret for your application.
* Required when requesting access tokens.
* Can use env key CLIENT_SECRET instead.
*/
clientSecret?: string;
/**
* Opt-in to sending the library and node version to your authorization server
* via the `Auth0-Client` header.
*/
enableTelemetry?: boolean;
/**
* Integer value for the system clock's tolerance (leeway) in seconds for ID token verification.`
* Default is 60
*/
clockTolerance?: number;
/**
* Throw a 401 error instead of triggering the login process for routes that require authentication.
*/
errorOnRequiredAuth?: boolean;
/**
* To opt-out of sending the library and node version to your authorization server
* via the `Auth0-Client` header. Default is `true
*/
enableTelemetry?: boolean;
/**
* Function that returns a URL-safe state value for `res.openid.login()`.
*/
getLoginState?: (req: OpenidRequest, config: object) => object;
/**
* Throw a 401 error instead of triggering the login process for routes that require authentication.
* Default is `false`
*/
errorOnRequiredAuth?: boolean;
/**
* Function that returns the profile for `req.openid.user`.
*/
getUser?: (req: OpenidRequest, config: ConfigParams) => undefined | UserinfoResponse;
/**
* Attempt silent login (`prompt: 'none'`) on the first unauthenticated route the user visits.
* For protected routes this can be useful if your Identity Provider does not default to
* `prompt: 'none'` and you'd like to attempt this before requiring the user to interact with a login prompt.
* For unprotected routes this can be useful if you want to check the user's logged in state on their IDP, to
* show them a login/logout button for example.
* Default is `false`
*/
attemptSilentLogin?: boolean;
/**
* Function that runs on the callback route, after callback processing but before redirection.
*/
handleCallback?: (req: OpenidRequest, res: Response, next: NextFunction) => void;
/**
* Function that returns an object with URL-safe state values for `res.oidc.login()`.
* Used for passing custom state parameters to your authorization server.
*
* ```js
* app.use(auth({
* ...
* getLoginState(req, options) {
* return {
* returnTo: options.returnTo || req.originalUrl,
* customState: 'foo'
* };
* }
* }))
* ``
*/
getLoginState?: (req: OpenidRequest, options: LoginOptions) => object;
/**
* Default options object used for all HTTP calls made by the library.
*/
httpOptions?: object;
/**
* Array value of claims to remove from the ID token before storing the cookie session.
* Default is `['aud', 'iss', 'iat', 'exp', 'nbf', 'nonce', 'azp', 'auth_time', 's_hash', 'at_hash', 'c_hash' ]`
*/
identityClaimFilter?: string[];
/**
* Array value of claims to remove from the ID token before storing the cookie session.
*/
identityClaimFilter?: string[];
/**
* Boolean value to log the user out from the identity provider on application logout. Default is `false`
*/
idpLogout?: boolean;
/**
* Boolean value to log the user out from the identity provider on application logout.
*/
idpLogout?: boolean;
/**
* String value for the expected ID token algorithm. Default is 'RS256'
*/
idTokenSigningAlg?: string;
/**
* String value for the expected ID token algorithm.
*/
idTokenAlg?: string;
/**
* REQUIRED. The root URL for the token issuer with no trailing slash.
* Can use env key ISSUER_BASE_URL instead.
*/
issuerBaseURL?: string;
/**
* REQUIRED. The root URL for the token issuer with no trailing slash.
* Can use env key ISSUER_BASE_URL instead.
*/
issuerBaseURL?: string;
/**
* Set a fallback cookie with no SameSite attribute when response_mode is form_post.
* Default is true
*/
legacySameSiteCookie?: boolean;
/**
* Set a fallback cookie with no SameSite attribute when response_mode is form_post.
*/
legacySameSiteCookie?: boolean;
/**
* Require authentication for all routes.
*/
authRequired?: boolean;
/**
* Boolean value to automatically install the login and logout routes.
*/
routes?: {
/**
* Relative path to application login.
*/
loginPath?: string;
login?: string | false;

@@ -132,3 +371,3 @@ /**

*/
logoutPath?: string;
logoutPath?: string | false;

@@ -140,3 +379,3 @@ /**

*/
postLogoutRedirectUri?: string;
postLogoutRedirect?: string;

@@ -146,13 +385,4 @@ /**

*/
redirectUriPath?: string;
/**
* Require authentication for all routes.
*/
required?: boolean | ((request: Request) => boolean);
/**
* Boolean value to automatically install the login and logout routes.
*/
routes?: boolean;
callback?: string;
};
}

@@ -163,59 +393,237 @@

*/
interface AppSessionConfigParams {
/**
* REQUIRED. The secret(s) used to derive an encryption key for the user identity in a session cookie.
* Use a single string key or array of keys for an encrypted session cookie.
* Can use env key APP_SESSION_SECRET instead.
*/
secret?: string | Array<string>;
interface SessionConfigParams {
/**
* String value for the cookie name used for the internal session.
* This value must only include letters, numbers, and underscores.
* Default is `appSession`.
*/
name?: string;
/**
* String value for the cookie name used for the internal session.
* This value must only include letters, numbers, and underscores.
* Default is `appSession`.
*/
name?: string;
/**
* If you want your session duration to be rolling, eg reset everytime the
* user is active on your site, set this to a `true`. If you want the session
* duration to be absolute, where the user is logged out a fixed time after login,
* regardless of activity, set this to `false`
* Default is `true`.
*/
rolling?: boolean;
/**
* Integer value, in seconds, for application session duration.
* Default is 86400 seconds (1 day).
*/
duration?: number
/**
* Integer value, in seconds, for application session rolling duration.
* The amount of time for which the user must be idle for then to be logged out.
* Default is 86400 seconds (1 day).
*/
rollingDuration?: number;
/**
* Domain name for the cookie.
*/
cookieDomain?: string;
/**
* Integer value, in seconds, for application absolute rolling duration.
* The amount of time after the user has logged in that they will be logged out.
* Set this to `false` if you don't want an absolute duration on your session.
* Default is 604800 seconds (7 days).
*/
absoluteDuration?: boolean | number;
/**
* Set to true to use a transient cookie (cookie without an explicit expiration).
* Defaults to `false` which will use appSession.duration as the cookie expiration.
*/
cookieTransient?: boolean;
/**
* Domain name for the cookie.
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `domain`
*/
domain?: string;
/**
* Flags the cookie to be accessible only by the web server.
* Defaults to `true`.
*/
cookieHttpOnly?: boolean;
/**
* Path for the cookie.
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `path`
*/
path?: string;
/**
* Path for the cookie.
*/
cookiePath?: string;
/**
* Set to true to use a transient cookie (cookie without an explicit expiration).
* Default is `false`
*/
transient?: boolean;
/**
* Marks the cookie to be used over secure channels only.
*/
cookieSecure?: boolean;
/**
* Flags the cookie to be accessible only by the web server.
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `httponly`.
* Defaults to `true`.
*/
httpOnly?: boolean;
/**
* Value of the SameSite Set-Cookie attribute.
* Defaults to "Lax" but will be adjusted based on response_type.
*/
cookieSameSite?: string;
/**
* Marks the cookie to be used over secure channels only.
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `secure`.
* Defaults to {@link Request.secure}.
*/
secure?: boolean;
/**
* Value of the SameSite Set-Cookie attribute.
* Passed to the [Response cookie](https://expressjs.com/en/api.html#res.cookie) as `samesite`.
* Defaults to "Lax" but will be adjusted based on {@link AuthorizationParameters.response_type}.
*/
sameSite?: string;
}
interface AccessToken {
/**
* The access token itself, can be an opaque string, JWT, or non-JWT token.
*/
access_token: string;
/**
* The type of access token, Usually "Bearer".
*/
token_type: string;
/**
* Number of seconds until the access token expires.
*/
expires_in: number;
/**
* Returns `true` if the access_token has expired.
*/
isExpired: () => boolean;
/**
* Performs refresh_token grant type exchange and updates the session's access token.
*
* ```js
* let accessToken = req.oidc.accessToken;
* if (accessToken.isExpired()) {
* accessToken = await accessToken.refresh();
* }
* ```
*/
refresh(): Promise<AccessToken>;
}
/**
* Express JS middleware implementing sign on for Express web apps using OpenID Connect.
*
* The `auth()` middleware requires {@link ConfigParams.secret secret}, {@link ConfigParams.baseURL baseURL}, {@link ConfigParams.clientID clientID}
* and {@link ConfigParams.issuerBaseURL issuerBaseURL}.
*
* If you are using a response type that includes `code`, you will also need: {@link ConfigParams.clientSecret clientSecret}
* ```
* const express = require('express');
* const { auth } = require('express-openid-connect');
*
* const app = express();
*
* app.use(
* auth({
* issuerBaseURL: 'https://YOUR_DOMAIN',
* baseURL: 'https://YOUR_APPLICATION_ROOT_URL',
* clientID: 'YOUR_CLIENT_ID',
* secret: 'LONG_RANDOM_STRING',
* })
* );
*
* app.get('/', (req, res) => {
* res.send(`hello ${req.oidc.user.name}`);
* });
*
* app.listen(3000, () => console.log('listening at http://localhost:3000'))
* ```
*/
export function auth(params?: ConfigParams): RequestHandler;
export function requiresAuth(): RequestHandler;
export function unauthorizedHandler(): ErrorRequestHandler;
/**
* Set {@link ConfigParams.authRequired authRequired} to `false` then require authentication
* on specific routes.
*
* ```js
* const { auth, requiresAuth } = require('express-openid-connect');
*
* app.use(
* auth({
* ...
* authRequired: false
* })
* );
*
* app.get('/profile', requiresAuth(), (req, res) => {
* res.send(`hello ${req.oidc.user.name}`);
* });
*
* ```
*/
export function requiresAuth(
requiresLoginCheck?: (req: OpenidRequest) => boolean
): RequestHandler;
/**
* Use this MW to protect a route based on the value of a specific claim.
*
* ```js
* const { claimEquals } = require('express-openid-connect');
*
* app.get('/admin', claimEquals('isAdmin', true), (req, res) => {
* res.send(...);
* });
*
* ```
*
* @param claim The name of the claim
* @param value The value of the claim, should be a primitive
*/
export function claimEquals(
claim: string,
value: boolean | number | string | null
): RequestHandler;
/**
* Use this MW to protect a route, checking that _all_ values are in a claim.
*
* ```js
* const { claimIncludes } = require('express-openid-connect');
*
* app.get('/admin/delete', claimIncludes('roles', 'admin', 'superadmin'), (req, res) => {
* res.send(...);
* });
*
* ```
*
* @param claim The name of the claim
* @param args Claim values that must all be included
*/
export function claimIncludes(
claim: string,
...args: (boolean | number | string | null)[]
): RequestHandler;
/**
* Use this MW to protect a route, providing a custom function to check.
*
* ```js
* const { claimCheck } = require('express-openid-connect');
*
* app.get('/admin/community', claimCheck((req, claims) => {
* return claims.isAdmin && claims.roles.includes('community');
* }), (req, res) => {
* res.send(...);
* });
*
* ```
*/
export function claimCheck(
checkFn: (req: OpenidRequest, claims: IdTokenClaims) => boolean
): RequestHandler;
/**
* Use this MW to attempt silent login (`prompt=none`) but not require authentication.
*
* See {@link ConfigParams.attemptSilentLogin attemptSilentLogin}
*
* ```js
* const { attemptSilentLogin } = require('express-openid-connect');
*
* app.get('/', attemptSilentLogin(), (req, res) => {
* res.render('homepage', {
* isAuthenticated: req.isAuthenticated() // show a login or logout button
* });
* });
*
* ```
*/
export function attemptSilentLogin(): RequestHandler;

@@ -1,11 +0,9 @@

const ResponseMode = require('./lib/ResponseMode');
const auth = require('./middleware/auth');
const requiresAuth = require('./middleware/requiresAuth');
const unauthorizedHandler = require('./middleware/unauthorizedHandler');
const attemptSilentLogin = require('./middleware/attemptSilentLogin');
module.exports = {
auth,
requiresAuth,
unauthorizedHandler,
ResponseMode,
...requiresAuth,
attemptSilentLogin,
};

@@ -1,25 +0,53 @@

const { strict: assert } = require('assert');
const { JWK, JWKS, JWE } = require('jose');
const { strict: assert, AssertionError } = require('assert');
const {
JWK,
JWKS,
JWE,
errors: { JOSEError },
} = require('jose');
const onHeaders = require('on-headers');
const cookie = require('cookie');
const hkdf = require('futoin-hkdf');
const COOKIES = require('./cookies');
const { encryption: deriveKey } = require('./hkdf');
const debug = require('./debug')('appSession');
const { sessionNameDefault, sessionDurationDefault } = require('./config');
const epoch = () => (Date.now() / 1000) | 0;
const CHUNK_BYTE_SIZE = 4000;
const deriveKey = (secret) => hkdf(secret, 32, { info: 'JWE CEK', hash: 'SHA-256' });
const epoch = () => Date.now() / 1000 | 0;
function attachSessionObject(req, sessionName, value) {
Object.defineProperty(req, sessionName, {
enumerable: true,
get() {
return value;
},
set(arg) {
if (arg === null || arg === undefined) {
value = arg;
} else {
throw new TypeError('session object cannot be reassigned');
}
return undefined;
},
});
}
module.exports = (sessionConfig) => {
module.exports = (config) => {
let current;
const COOKIES = Symbol('cookies');
const alg = 'dir';
const enc = 'A256GCM';
const sessionSecrets = Array.isArray(sessionConfig.secret) ? sessionConfig.secret : [sessionConfig.secret];
const sessionName = sessionConfig.name || sessionNameDefault;
const sessionDuration = sessionConfig.duration || sessionDurationDefault;
const secrets = Array.isArray(config.secret)
? config.secret
: [config.secret];
const sessionName = config.session.name;
const cookieConfig = config.session.cookie;
const {
absoluteDuration,
rolling: rollingEnabled,
rollingDuration,
} = config.session;
let keystore = new JWKS.KeyStore();
sessionSecrets.forEach((secretString, i) => {
secrets.forEach((secretString, i) => {
const key = JWK.asKey(deriveKey(secretString));

@@ -36,28 +64,74 @@ if (i === 0) {

function encrypt (payload, headers) {
return JWE.encrypt(payload, current, { alg, enc, zip: 'DEF', ...headers });
function encrypt(payload, headers) {
return JWE.encrypt(payload, current, { alg, enc, ...headers });
}
function decrypt (jwe) {
function decrypt(jwe) {
return JWE.decrypt(jwe, keystore, { complete: true, algorithms: [enc] });
}
function setCookie (req, res, { uat = epoch(), iat = uat, exp = uat + sessionDuration }) {
const cookieOptions = {};
Object.keys(sessionConfig).filter(key => /^cookie/.test(key)).forEach((key) => {
const cookieOptionKey = key.replace(/^cookie([A-Z])/, (match, p1) => p1.toLowerCase());
cookieOptions[cookieOptionKey] = sessionConfig[key];
});
const expires = cookieOptions.transient ? 0 : new Date(exp * 1000);
function calculateExp(iat, uat) {
if (!rollingEnabled) {
return iat + absoluteDuration;
}
return Math.min(
...[uat + rollingDuration, iat + absoluteDuration].filter(Boolean)
);
}
function setCookie(
req,
res,
{ uat = epoch(), iat = uat, exp = calculateExp(iat, uat) }
) {
const cookieOptions = {
...cookieConfig,
expires: cookieConfig.transient ? 0 : new Date(exp * 1000),
secure:
typeof cookieConfig.secure === 'boolean'
? cookieConfig.secure
: req.secure,
};
delete cookieOptions.transient;
if ((!req[sessionName] || !Object.keys(req[sessionName]).length) && sessionName in req[COOKIES]) {
res.clearCookie(sessionName, cookieOptions);
return;
}
// session was deleted or is empty, this matches all session cookies (chunked or unchunked)
// and clears them, essentially cleaning up what we've set in the past that is now trash
if (!req[sessionName] || !Object.keys(req[sessionName]).length) {
debug(
'session was deleted or is empty, clearing all matching session cookies'
);
for (const cookieName of Object.keys(req[COOKIES])) {
if (cookieName.match(`^${sessionName}(?:\\.\\d)?$`)) {
res.clearCookie(cookieName, {
domain: cookieOptions.domain,
path: cookieOptions.path,
});
}
}
} else {
debug(
'found session, creating signed session cookie(s) with name %o(.i)',
sessionName
);
const value = encrypt(JSON.stringify(req[sessionName]), {
iat,
uat,
exp,
});
if (req[sessionName] && Object.keys(req[sessionName]).length > 0) {
const value = encrypt(JSON.stringify(req[sessionName]), { iat, uat, exp });
res.cookie(sessionName, value, { expires, ...cookieOptions });
const chunkCount = Math.ceil(value.length / CHUNK_BYTE_SIZE);
if (chunkCount > 1) {
debug('cookie size greater than %d, chunking', CHUNK_BYTE_SIZE);
for (let i = 0; i < chunkCount; i++) {
const chunkValue = value.slice(
i * CHUNK_BYTE_SIZE,
(i + 1) * CHUNK_BYTE_SIZE
);
const chunkCookieName = `${sessionName}.${i}`;
res.cookie(chunkCookieName, chunkValue, cookieOptions);
}
} else {
res.cookie(sessionName, value, cookieOptions);
}
}

@@ -67,27 +141,94 @@ }

return (req, res, next) => {
if (!req.hasOwnProperty(COOKIES)) {
req[COOKIES] = cookie.parse(req.get('cookie') || '');
}
if (req.hasOwnProperty(sessionName)) {
return next();
debug(
'request object (req) already has %o property, this is indicative of a middleware setup problem',
sessionName
);
return next(
new Error(
`req[${sessionName}] is already set, did you run this middleware twice?`
)
);
}
req[COOKIES] = cookie.parse(req.get('cookie') || '');
let iat;
let uat;
let exp;
let existingSessionValue;
try {
if (req[COOKIES].hasOwnProperty(sessionName)) {
const { protected: header, cleartext } = decrypt(req[COOKIES][sessionName]);
({ iat, exp } = header);
assert(exp > epoch());
req[sessionName] = JSON.parse(cleartext);
// get JWE from unchunked session cookie
debug('reading session from %s cookie', sessionName);
existingSessionValue = req[COOKIES][sessionName];
} else if (req[COOKIES].hasOwnProperty(`${sessionName}.0`)) {
// get JWE from chunked session cookie
// iterate all cookie names
// match and filter for the ones that match sessionName.<number>
// sort by chunk index
// concat
existingSessionValue = Object.entries(req[COOKIES])
.map(([cookie, value]) => {
const match = cookie.match(`^${sessionName}\\.(\\d+)$`);
if (match) {
return [match[1], value];
}
})
.filter(Boolean)
.sort(([a], [b]) => {
return parseInt(a, 10) - parseInt(b, 10);
})
.map(([i, chunk]) => {
debug('reading session chunk from %s.%d cookie', sessionName, i);
return chunk;
})
.join('');
}
} finally {
if (!req.hasOwnProperty(sessionName) || !req[sessionName]) {
req[sessionName] = {};
if (existingSessionValue) {
const { protected: header, cleartext } = decrypt(existingSessionValue);
({ iat, uat, exp } = header);
// check that the existing session isn't expired based on options when it was established
assert(
exp > epoch(),
'it is expired based on options when it was established'
);
// check that the existing session isn't expired based on current rollingDuration rules
if (rollingDuration) {
assert(
uat + rollingDuration > epoch(),
'it is expired based on current rollingDuration rules'
);
}
// check that the existing session isn't expired based on current absoluteDuration rules
if (absoluteDuration) {
assert(
iat + absoluteDuration > epoch(),
'it is expired based on current absoluteDuration rules'
);
}
attachSessionObject(req, sessionName, JSON.parse(cleartext));
}
} catch (err) {
if (err instanceof AssertionError) {
debug('existing session was rejected because', err.message);
} else if (err instanceof JOSEError) {
debug(
'existing session was rejected because it could not be decrypted',
err
);
} else {
debug('unexpected error handling session', err);
}
}
if (!req.hasOwnProperty(sessionName) || !req[sessionName]) {
attachSessionObject(req, sessionName, {});
}
onHeaders(res, setCookie.bind(undefined, req, res, { iat }));

@@ -94,0 +235,0 @@

@@ -6,2 +6,3 @@ const { Issuer, custom } = require('openid-client');

const pkg = require('../package.json');
const debug = require('./debug')('client');

@@ -12,30 +13,61 @@ const telemetryHeader = {

env: {
node: process.version
}
node: process.version,
},
};
function spacedStringsToAlphabetical(string) {
function sortSpaceDelimitedString(string) {
return string.split(' ').sort().join(' ');
}
const getIssuer = memoize((issuer) => Issuer.discover(issuer));
async function get(config) {
const defaultHttpOptions = (options) => {
options.headers = {
...options.headers,
'User-Agent': `${pkg.name}/${pkg.version}`,
...(config.enableTelemetry
? {
'Auth0-Client': Buffer.from(
JSON.stringify(telemetryHeader)
).toString('base64'),
}
: undefined),
};
options.timeout = 5000;
return options;
};
const applyHttpOptionsCustom = (entity) =>
(entity[custom.http_options] = defaultHttpOptions);
const issuer = await Issuer.discover(config.issuerBaseURL);
applyHttpOptionsCustom(Issuer);
const issuer = await getIssuer(config.issuerBaseURL);
applyHttpOptionsCustom(issuer);
const issuerTokenAlgs = Array.isArray(issuer.id_token_signing_alg_values_supported) ?
issuer.id_token_signing_alg_values_supported : [];
if (!issuerTokenAlgs.includes(config.idTokenAlg)) {
throw new Error(
`ID token algorithm "${config.idTokenAlg}" is not supported by the issuer. ` +
`Supported ID token algorithms are: "${issuerTokenAlgs.join('", "')}". `
const issuerTokenAlgs = Array.isArray(
issuer.id_token_signing_alg_values_supported
)
? issuer.id_token_signing_alg_values_supported
: [];
if (!issuerTokenAlgs.includes(config.idTokenSigningAlg)) {
debug(
'ID token algorithm %o is not supported by the issuer. Supported ID token algorithms are: %o.',
config.idTokenSigningAlg,
issuerTokenAlgs
);
}
const configRespType = spacedStringsToAlphabetical(config.authorizationParams.response_type);
const issuerRespTypes = Array.isArray(issuer.response_types_supported) ? issuer.response_types_supported : [];
issuerRespTypes.map(spacedStringsToAlphabetical);
const configRespType = sortSpaceDelimitedString(
config.authorizationParams.response_type
);
const issuerRespTypes = Array.isArray(issuer.response_types_supported)
? issuer.response_types_supported
: [];
issuerRespTypes.map(sortSpaceDelimitedString);
if (!issuerRespTypes.includes(configRespType)) {
throw new Error(
`Response type "${configRespType}" is not supported by the issuer. ` +
`Supported response types are: "${issuerRespTypes.join('", "')}". `
debug(
'Response type %o is not supported by the issuer. ' +
'Supported response types are: %o.',
configRespType,
issuerRespTypes
);

@@ -45,7 +77,11 @@ }

const configRespMode = config.authorizationParams.response_mode;
const issuerRespModes = Array.isArray(issuer.response_modes_supported) ? issuer.response_modes_supported : [];
if (configRespMode && ! issuerRespModes.includes(configRespMode)) {
throw new Error(
`Response mode "${configRespMode}" is not supported by the issuer. ` +
`Supported response modes are "${issuerRespModes.join('", "')}". `
const issuerRespModes = Array.isArray(issuer.response_modes_supported)
? issuer.response_modes_supported
: [];
if (configRespMode && !issuerRespModes.includes(configRespMode)) {
debug(
'Response mode %o is not supported by the issuer. ' +
'Supported response modes are %o.',
configRespMode,
issuerRespModes
);

@@ -57,33 +93,27 @@ }

client_secret: config.clientSecret,
id_token_signed_response_alg: config.idTokenAlg,
id_token_signed_response_alg: config.idTokenSigningAlg,
});
applyHttpOptionsCustom(client);
client[custom.clock_tolerance] = config.clockTolerance;
if (config.idpLogout && !issuer.end_session_endpoint) {
if (config.auth0Logout || url.parse(issuer.issuer).hostname.match('auth0.com$')) {
client.endSessionUrl = function(params) {
const parsedUrl = url.parse(urlJoin(issuer.issuer, '/v2/logout'));
parsedUrl.query = {
returnTo: params.post_logout_redirect_uri,
client_id: client.client_id
};
return url.format(parsedUrl);
};
if (
config.auth0Logout ||
url.parse(issuer.issuer).hostname.match('\\.auth0\\.com$')
) {
Object.defineProperty(client, 'endSessionUrl', {
value(params) {
const parsedUrl = url.parse(urlJoin(issuer.issuer, '/v2/logout'));
parsedUrl.query = {
returnTo: params.post_logout_redirect_uri,
client_id: client.client_id,
};
return url.format(parsedUrl);
},
});
} else {
throw new Error("The issuer doesn't support session management.");
debug('the issuer does not support RP-Initiated Logout');
}
}
let httpOptions = config.httpOptions || {};
httpOptions.headers = Object.assign(
// Allow configuration to override user agent header.
{'User-Agent': `${pkg.name}/${pkg.version}`},
httpOptions.headers || {},
// Do not allow overriding telemetry, but allow it to be omitted.
config.enableTelemetry && {'Auth0-Client': Buffer.from(JSON.stringify(telemetryHeader)).toString('base64')}
);
custom.setHttpOptionsDefaults(httpOptions);
client[custom.clock_tolerance] = config.clockTolerance;
return client;

@@ -90,0 +120,0 @@ }

const Joi = require('@hapi/joi');
const clone = require('clone');
const { defaultState: getLoginState } = require('./hooks/getLoginState');
const getUser = require('./hooks/getUser');
const handleCallback = require('./hooks/handleCallback');
const sessionDurationDefault = (24 * 60 * 60); // 1 day
const sessionNameDefault = 'appSession';
const paramsSchema = Joi.object({
appSession: Joi.alternatives([
Joi.boolean().valid(false),
Joi.object({
secret: Joi.alternatives([
Joi.string().min(8),
Joi.array().items(Joi.string().min(8))
]).required(),
duration: Joi.number().integer().optional().default(sessionDurationDefault),
name: Joi.string().token().optional().default(sessionNameDefault),
cookieDomain: Joi.string().optional(),
cookieTransient: Joi.boolean().optional().default(false),
cookieHttpOnly: Joi.boolean().optional().default(true),
cookiePath: Joi.string().optional(),
cookieSameSite: Joi.string().valid('Lax', 'Strict', 'None').optional().default('Lax'),
cookieSecure: Joi.boolean().optional()
}).unknown(false)
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().token().optional().default('appSession'),
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.boolean().optional(),
path: Joi.string().uri({ relativeOnly: true }).optional(),
})
.default()
.unknown(false),
})
.default()
.unknown(false),
auth0Logout: Joi.boolean().optional().default(false),
authorizationParams: Joi.object({
response_type: Joi.string().optional().default('id_token'),
scope: Joi.string().optional().default('openid profile email'),
response_mode: Joi.alternatives([
Joi.string().optional(),
Joi.allow(null).optional()
]).default(function(parent) {
const responseType = parent.response_type.split(' ');
const responseIncludesTokens = responseType.includes('id_token') || responseType.includes('token');
return responseIncludesTokens ? 'form_post' : undefined;
}),
}).optional().unknown(true).default(),
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(),
baseURL: Joi.string().uri().required(),
clientID: Joi.string().required(),
clientSecret: Joi.string().when(
Joi.ref('authorizationParams.response_type', {adjust: (value) => value && value.split(' ').includes('code')}),
{
is: true,
then: Joi.string().required().messages({
'any.required': '"clientSecret" is required for response_type code'
clientSecret: Joi.string()
.when(
Joi.ref('authorizationParams.response_type', {
adjust: (value) => value && value.includes('code'),
}),
otherwise: Joi.when(
Joi.ref('idTokenAlg', {adjust: (value) => value && 'HS' === value.substring(0,2)}),
{
is: true,
then: Joi.string().required().messages({
'any.required': '"clientSecret" is required for ID tokens with HS algorithms'
})
}
)
}
),
{
is: true,
then: Joi.string().required().messages({
'any.required':
'"clientSecret" is required for a response_type that includes code',
}),
}
)
.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),
getLoginState: Joi.function().optional().default(() => getLoginState),
getUser: Joi.function().optional().default(() => getUser),
handleCallback: Joi.function().optional().default(() => handleCallback),
httpOptions: Joi.object().optional(),
identityClaimFilter: Joi.array().optional().default(['aud', 'iss', 'iat', 'exp', 'nonce', 'azp', 'auth_time']),
idpLogout: Joi.boolean().optional().default((parent) => parent.auth0Logout || false),
idTokenAlg: Joi.string().not('none').optional().default('RS256'),
issuerBaseURL: Joi.alternatives([
Joi.string().uri(),
Joi.string().hostname()
]).required(),
attemptSilentLogin: Joi.boolean().optional().default(false),
getLoginState: Joi.function()
.optional()
.default(() => getLoginState),
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),
loginPath: Joi.string().uri({relativeOnly: true}).optional().default('/login'),
logoutPath: Joi.string().uri({relativeOnly: true}).optional().default('/logout'),
postLogoutRedirectUri: Joi.string().uri({allowRelative: true}).optional().default(''),
redirectUriPath: Joi.string().uri({relativeOnly: true}).optional().default('/callback'),
required: Joi.alternatives([ Joi.function(), Joi.boolean()]).optional().default(true),
routes: Joi.boolean().optional().default(true),
authRequired: Joi.boolean().optional().default(true),
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.string().uri({ relativeOnly: true }).default('/callback'),
postLogoutRedirect: Joi.string().uri({ allowRelative: true }).default(''),
})
.default()
.unknown(false),
});
module.exports.get = function(params) {
let config = (typeof params == 'object' ? clone(params) : {});
config = Object.assign({
module.exports.get = function (params) {
let config = typeof params === 'object' ? clone(params) : {};
config = {
secret: process.env.SECRET,
issuerBaseURL: process.env.ISSUER_BASE_URL,

@@ -91,12 +161,8 @@ baseURL: process.env.BASE_URL,

clientSecret: process.env.CLIENT_SECRET,
appSession: {},
}, config);
...config,
};
if (process.env.APP_SESSION_SECRET && typeof config.appSession === 'object') {
config.appSession.secret = config.appSession.secret || process.env.APP_SESSION_SECRET;
}
const paramsValidation = paramsSchema.validate(config);
if (paramsValidation.error) {
throw new Error(paramsValidation.error.details[0].message);
throw new TypeError(paramsValidation.error.details[0].message);
}

@@ -106,4 +172,1 @@

};
module.exports.sessionDurationDefault = sessionDurationDefault;
module.exports.sessionNameDefault = sessionNameDefault;

@@ -5,61 +5,180 @@ const cb = require('cb');

const { TokenSet } = require('openid-client');
const clone = require('clone');
const { strict: assert } = require('assert');
const transient = require('./transientHandler');
const { get: getClient } = require('./client');
const debug = require('./debug')('context');
const { get: getClient } = require('./client');
const { encodeState } = require('../lib/hooks/getLoginState');
const { cancelSilentLogin } = require('../middleware/attemptSilentLogin');
const weakRef = require('./weakCache');
function isExpired() {
return tokenSet.call(this).expired();
}
async function refresh() {
let { config, req } = weakRef(this);
const client = await getClient(config);
const oldTokenSet = tokenSet.call(this);
const newTokenSet = await client.refresh(oldTokenSet);
// Update the session
const session = req[config.session.name];
Object.assign(session, {
id_token: newTokenSet.id_token,
access_token: newTokenSet.access_token,
// If no new refresh token assume the current refresh token is valid.
refresh_token: newTokenSet.refresh_token || oldTokenSet.refresh_token,
token_type: newTokenSet.token_type,
expires_at: newTokenSet.expires_at,
});
// Delete the old token set
const cachedTokenSet = weakRef(session);
delete cachedTokenSet.value;
return this.accessToken;
}
function tokenSet() {
const contextCache = weakRef(this);
const session = contextCache.req[contextCache.config.session.name];
if (!session || !('id_token' in session)) {
return undefined;
}
const cachedTokenSet = weakRef(session);
if (!('value' in cachedTokenSet)) {
const {
id_token,
access_token,
refresh_token,
token_type,
expires_at,
} = session;
cachedTokenSet.value = new TokenSet({
id_token,
access_token,
refresh_token,
token_type,
expires_at,
});
}
return cachedTokenSet.value;
}
class RequestContext {
constructor(config, req, res, next) {
this._config = config;
this._req = req;
this._res = res;
this._next = next;
Object.assign(weakRef(this), { config, req, res, next });
}
get isAuthenticated() {
return !!this.user;
isAuthenticated() {
return !!this.idTokenClaims;
}
makeTokenSet(tokenSet) {
return new TokenSet(tokenSet);
get idToken() {
try {
return tokenSet.call(this).id_token;
} catch (err) {
return undefined;
}
}
async load() {
if (!this.client) {
this.client = await getClient(this._config);
get refreshToken() {
try {
return tokenSet.call(this).refresh_token;
} catch (err) {
return undefined;
}
}
this.user = await this._config.getUser(this._req, this._config);
get accessToken() {
try {
const { access_token, token_type, expires_in } = tokenSet.call(this);
if (!access_token || !token_type || typeof expires_in !== 'number') {
return undefined;
}
return {
access_token,
token_type,
expires_in,
isExpired: isExpired.bind(this),
refresh: refresh.bind(this),
};
} catch (err) {
return undefined;
}
}
get idTokenClaims() {
try {
return clone(tokenSet.call(this).claims());
} catch (err) {
return undefined;
}
}
get user() {
try {
const {
config: { identityClaimFilter },
} = weakRef(this);
const { idTokenClaims } = this;
const user = clone(idTokenClaims);
identityClaimFilter.forEach((claim) => {
delete user[claim];
});
return user;
} catch (err) {
return undefined;
}
}
async fetchUserInfo() {
const { config } = weakRef(this);
const client = await getClient(config);
return client.userinfo(tokenSet.call(this));
}
}
class ResponseContext {
constructor(config, req, res, next) {
this._config = config;
this._req = req;
this._res = res;
this._next = next;
constructor(config, req, res, next, transient) {
Object.assign(weakRef(this), { config, req, res, next, transient });
}
get errorOnRequiredAuth() {
return this._config.errorOnRequiredAuth;
return weakRef(this).config.errorOnRequiredAuth;
}
getRedirectUri() {
return urlJoin(this._config.baseURL, this._config.redirectUriPath);
const { config } = weakRef(this);
return urlJoin(config.baseURL, config.routes.callback);
}
silentLogin(options) {
return this.login({
...options,
prompt: 'none',
});
}
async login(options = {}) {
const next = cb(this._next).once();
const req = this._req;
const res = this._res;
const config = this._config;
const client = req.openid.client;
let { config, req, res, next, transient } = weakRef(this);
next = cb(next).once();
const client = await getClient(config);
// Set default returnTo value, allow passed-in options to override or use originalUrl on GET
let returnTo = this._config.baseURL;
let returnTo = config.baseURL;
if (options.returnTo) {
returnTo = options.returnTo;
debug('req.oidc.login() called with returnTo: %s', returnTo);
} else if (req.method === 'GET' && req.originalUrl) {
returnTo = req.originalUrl;
debug('req.oidc.login() without returnTo, using: %s', returnTo);
}

@@ -70,3 +189,3 @@

returnTo,
...options
...options,
};

@@ -78,20 +197,25 @@

...config.authorizationParams,
...options.authorizationParams
...options.authorizationParams,
};
const transientOpts = {
legacySameSiteCookie: config.legacySameSiteCookie,
sameSite: options.authorizationParams.response_mode === 'form_post' ? 'None' : 'Lax'
sameSite:
options.authorizationParams.response_mode === 'form_post'
? 'None'
: 'Lax',
};
let stateValue = await config.getLoginState(req, options);
if ( typeof stateValue !== 'object' ) {
next(new Error( 'Custom state value must be an object.' ));
const stateValue = await config.getLoginState(req, options);
if (typeof stateValue !== 'object') {
next(new Error('Custom state value must be an object.'));
}
stateValue.nonce = transient.createNonce();
stateValue.nonce = transient.generateNonce();
const stateTransientOpts = {
...transientOpts,
value: encodeState(stateValue)
};
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();
}

@@ -101,7 +225,38 @@ try {

...options.authorizationParams,
nonce: transient.store('nonce', res, transientOpts),
state: transient.store('state', res, stateTransientOpts)
nonce: transient.store('nonce', req, res, transientOpts),
state: transient.store('state', req, res, {
...transientOpts,
value: encodeState(stateValue),
}),
...(usePKCE
? {
code_challenge: transient.calculateCodeChallenge(
transient.store('code_verifier', req, res, transientOpts)
),
code_challenge_method: 'S256',
}
: undefined),
};
const validResponseTypes = ['id_token', 'code id_token', 'code'];
assert(
validResponseTypes.includes(authParams.response_type),
`response_type should be one of ${validResponseTypes.join(', ')}`
);
assert(
/\bopenid\b/.test(authParams.scope),
'scope should contain "openid"'
);
// TODO: hook here
if (authParams.max_age) {
transient.store('max_age', req, res, {
...transientOpts,
value: authParams.max_age,
});
}
const authorizationUrl = client.authorizationUrl(authParams);
debug('redirecting to %s', authorizationUrl);
res.redirect(authorizationUrl);

@@ -114,8 +269,8 @@ } catch (err) {

async logout(params = {}) {
const next = cb(this._next).once();
const req = this._req;
const res = this._res;
const config = this._config;
let { config, req, res, next } = weakRef(this);
next = cb(next).once();
const client = await getClient(config);
let returnURL = params.returnTo || config.postLogoutRedirectUri;
let returnURL = params.returnTo || config.routes.postLogoutRedirect;
debug('req.oidc.logout() with return url: %s', returnURL);

@@ -126,29 +281,31 @@ if (url.parse(returnURL).host === null) {

if (!req.isAuthenticated()) {
cancelSilentLogin(req, res);
if (!req.oidc.isAuthenticated()) {
debug('end-user already logged out, redirecting to %s', returnURL);
return res.redirect(returnURL);
}
if (config.appSession) {
req[config.appSession.name] = undefined;
}
const { idToken: id_token_hint } = req.oidc;
req[config.session.name] = undefined;
if (!config.idpLogout) {
debug('performing a local only logout, redirecting to %s', returnURL);
return res.redirect(returnURL);
}
const client = req.openid.client;
try {
returnURL = client.endSessionUrl({
post_logout_redirect_uri: returnURL,
id_token_hint: req.openid.tokens,
id_token_hint,
});
} catch(err) {
} catch (err) {
return next(err);
}
debug('logging out of identity provider, redirecting to %s', returnURL);
res.redirect(returnURL);
}
}
module.exports = { RequestContext, ResponseContext };

@@ -1,9 +0,8 @@

const { encode: base64encode, decode: base64decode } = require('base64url');
const base64url = require('base64url');
const debug = require('../debug')('getLoginState');
module.exports.defaultState = defaultState;
module.exports.encodeState = encodeState;
module.exports.decodeState = decodeState;
/**
* Generate a unique state value for use during login transactions.
* Generate the state value for use during login transactions. It is used to store the intended
* return URL after the user authenticates. State is not used to carry unique PRNG values here
* because the library utilizes either nonce or PKCE for CSRF protection.
*

@@ -16,5 +15,5 @@ * @param {RequestHandler} req

function defaultState(req, options) {
return {
returnTo: options.returnTo || req.originalUrl
};
const state = { returnTo: options.returnTo || req.originalUrl };
debug('adding default state %O', state);
return state;
}

@@ -29,4 +28,7 @@

*/
function encodeState(stateObject) {
return base64encode(JSON.stringify(stateObject));
function encodeState(stateObject = {}) {
// this filters out nonce, code_verifier, and max_age from the state object so that the values are
// only stored in its dedicated transient cookie
const { nonce, code_verifier, max_age, ...filteredState } = stateObject; // eslint-disable-line no-unused-vars
return base64url.encode(JSON.stringify(filteredState));
}

@@ -42,3 +44,7 @@

function decodeState(stateValue) {
return JSON.parse(base64decode(stateValue));
return JSON.parse(base64url.decode(stateValue));
}
module.exports.defaultState = defaultState;
module.exports.encodeState = encodeState;
module.exports.decodeState = decodeState;

@@ -1,90 +0,201 @@

const crypto = require('crypto');
const { generators } = require('openid-client');
const { JWKS, JWS, JWK } = require('jose');
const { signing: deriveKey } = require('./hkdf');
exports.store = store;
exports.getOnce = getOnce;
exports.createNonce = createNonce;
const header = { alg: 'HS256', b64: false, crit: ['b64'] };
const getPayload = (cookie, value) => Buffer.from(`${cookie}=${value}`);
const flattenedJWSFromCookie = (cookie, value, signature) => ({
protected: Buffer.from(JSON.stringify(header))
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_'),
payload: getPayload(cookie, value),
signature,
});
const generateSignature = (cookie, value, key) => {
const payload = getPayload(cookie, value);
return JWS.sign.flattened(payload, key, header).signature;
};
const verifySignature = (cookie, value, signature, keystore) => {
try {
return !!JWS.verify(
flattenedJWSFromCookie(cookie, value, signature),
keystore,
{ algorithm: 'HS256', crit: ['b64'] }
);
} catch (err) {
return false;
}
};
const getCookieValue = (cookie, value, keystore) => {
if (!value) {
return undefined;
}
let signature;
[value, signature] = value.split('.');
if (verifySignature(cookie, value, signature, keystore)) {
return value;
}
/**
* Set a cookie with a value or a generated nonce.
*
* @param {String} key Cookie name to use.
* @param {Object} res Express Response object.
* @param {Object} opts Options object.
* @param {String} opts.sameSite SameSite attribute of "None," "Lax," or "Strict". Default is "None."
* @param {String} opts.value Cookie value. Omit this key to store a generated value.
* @param {Boolean} opts.legacySameSiteCookie Should a fallback cookie be set? Default is true.
* @param {Boolean} opts.maxAge Cookie MaxAge value, in milliseconds. Default is 600000 (10 minutes).
*
* @return {String} Cookie value that was set.
*/
function store(key, res, opts = {}) {
const sameSiteAttr = opts.sameSite || 'None';
const isSameSiteNone = sameSiteAttr === 'None';
const value = opts.value || createNonce();
const fallbackCookie = 'legacySameSiteCookie' in opts ? opts.legacySameSiteCookie : true;
return undefined;
};
const basicAttr = {
httpOnly: true,
maxAge: 'maxAge' in opts ? parseInt(opts.maxAge, 10) : 600 * 1000 // 10 minutes
};
const generateCookieValue = (cookie, value, key) => {
const signature = generateSignature(cookie, value, key);
return `${value}.${signature}`;
};
// Set the cookie with the SameSite attribute and, if needed, the Secure flag.
res.cookie(key, value, Object.assign({}, basicAttr, {sameSite: sameSiteAttr, secure: isSameSiteNone}));
const COOKIES = require('./cookies');
if (isSameSiteNone && fallbackCookie) {
// Set the fallback cookie with no SameSite or Secure attributes.
res.cookie('_' + key, value, basicAttr);
class TransientCookieHandler {
constructor({ secret, session, legacySameSiteCookie }) {
let current;
const secrets = Array.isArray(secret) ? secret : [secret];
let keystore = new JWKS.KeyStore();
secrets.forEach((secretString, i) => {
const key = JWK.asKey(deriveKey(secretString));
if (i === 0) {
current = key;
}
keystore.add(key);
});
if (keystore.size === 1) {
keystore = current;
}
this.currentKey = current;
this.keyStore = keystore;
this.sessionCookieConfig = (session && session.cookie) || {};
this.legacySameSiteCookie = legacySameSiteCookie;
}
return value;
}
/**
* Set a cookie with a value or a generated nonce.
*
* @param {String} key Cookie name to use.
* @param {Object} req Express Request object.
* @param {Object} res Express Response object.
* @param {Object} opts Options object.
* @param {String} opts.sameSite SameSite attribute of "None," "Lax," or "Strict". Default is "None."
* @param {String} opts.value Cookie value. Omit this key to store a generated value.
* @param {Boolean} opts.legacySameSiteCookie Should a fallback cookie be set? Default is true.
*
* @return {String} Cookie value that was set.
*/
store(
key,
req,
res,
{ sameSite = 'None', value = this.generateNonce() } = {}
) {
const isSameSiteNone = sameSite === 'None';
const { domain, path, secure } = this.sessionCookieConfig;
const basicAttr = {
httpOnly: true,
secure: typeof secure === 'boolean' ? secure : req.secure,
domain,
path,
};
/**
* Get a cookie value then delete it.
*
* @param {String} key Cookie name to use.
* @param {Object} req Express Request object.
* @param {Object} res Express Response object.
* @param {Object} opts Options object.
* @param {Boolean} opts.legacySameSiteCookie Should a fallback cookie be checked? Default is true.
*
* @return {String|undefined} Cookie value or undefined if cookie was not found.
*/
function getOnce(key, req, res, opts = {}) {
{
const cookieValue = generateCookieValue(key, value, this.currentKey);
// Set the cookie with the SameSite attribute and, if needed, the Secure flag.
res.cookie(key, cookieValue, {
...basicAttr,
sameSite,
secure: isSameSiteNone ? true : basicAttr.secure,
});
}
if (!req.cookies) {
return undefined;
if (isSameSiteNone && this.legacySameSiteCookie) {
const cookieValue = generateCookieValue(
`_${key}`,
value,
this.currentKey
);
// Set the fallback cookie with no SameSite or Secure attributes.
res.cookie(`_${key}`, cookieValue, basicAttr);
}
return value;
}
let value = req.cookies[key];
delete req.cookies[key];
deleteCookie(key, res);
/**
* Get a cookie value then delete it.
*
* @param {String} key Cookie name to use.
* @param {Object} req Express Request object.
* @param {Object} res Express Response object.
*
* @return {String|undefined} Cookie value or undefined if cookie was not found.
*/
getOnce(key, req, res) {
if (!req[COOKIES]) {
return undefined;
}
if ('legacySameSiteCookie' in opts ? opts.legacySameSiteCookie : true) {
const fallbackKey = '_' + key;
value = value || req.cookies[fallbackKey];
delete req.cookies[fallbackKey];
deleteCookie(fallbackKey, res);
let value = getCookieValue(key, req[COOKIES][key], this.keyStore);
this.deleteCookie(key, res);
if (this.legacySameSiteCookie) {
const fallbackKey = `_${key}`;
if (!value) {
value = getCookieValue(
fallbackKey,
req[COOKIES][fallbackKey],
this.keyStore
);
}
this.deleteCookie(fallbackKey, res);
}
return value;
}
return value;
}
/**
* Generates a nonce value.
*
* @return {String}
*/
generateNonce() {
return generators.nonce();
}
/**
* Generates a nonce value.
*
* @return {String}
*/
function createNonce() {
return crypto.randomBytes(16).toString('hex');
/**
* Generates a code_verifier value.
*
* @return {String}
*/
generateCodeVerifier() {
return generators.codeVerifier();
}
/**
* Calculates a code_challenge value for a given codeVerifier
*
* @param {String} codeVerifier Code Verifier to calculate the code_challenge value from.
*
* @return {String}
*/
calculateCodeChallenge(codeVerifier) {
return generators.codeChallenge(codeVerifier);
}
/**
* Clears the cookie from the browser by setting an empty value and an expiration date in the past
*
* @param {String} name Cookie name
* @param {Object} res Express Response object
*/
deleteCookie(name, res) {
const { domain, path } = this.sessionCookieConfig;
res.clearCookie(name, {
domain,
path,
});
}
}
/**
* Sets a blank value and zero max age cookie.
*
* @param {String} name Cookie name
* @param {Object} res Express Response object
*/
function deleteCookie(name, res) {
res.cookie(name, '', {maxAge: 0});
}
module.exports = TransientCookieHandler;
const express = require('express');
const cb = require('cb');
const createError = require('http-errors');
const cookieParser = require('cookie-parser');
const debug = require('../lib/debug')('auth');
const { get: getConfig } = require('../lib/config');
const { get: getClient } = require('../lib/client');
const requiresAuth = require('./requiresAuth');
const transient = require('../lib/transientHandler');
const { requiresAuth } = require('./requiresAuth');
const attemptSilentLogin = require('./attemptSilentLogin');
const TransientCookieHandler = require('../lib/transientHandler');
const { RequestContext, ResponseContext } = require('../lib/context');

@@ -15,124 +16,143 @@ const appSession = require('../lib/appSession');

const enforceLeadingSlash = (path) => {
return '/' === path.split('')[0] ? path : '/' + path;
return path.split('')[0] === '/' ? path : '/' + path;
};
/**
* Returns a router with two routes /login and /callback
*
* @param {Object} [params] The parameters object; see index.d.ts for types and descriptions.
*
* @returns {express.Router} the router
*/
* Returns a router with two routes /login and /callback
*
* @param {Object} [params] The parameters object; see index.d.ts for types and descriptions.
*
* @returns {express.Router} the router
*/
module.exports = function (params) {
const config = getConfig(params);
const authorizeParams = config.authorizationParams;
const router = express.Router();
const useAppSession = config.appSession && config.appSession.secret;
debug('configuration object processed, resulting configuration: %O', config);
const router = new express.Router();
const transient = new TransientCookieHandler(config);
// Only use the internal cookie-based session if appSession secret is provided.
if (useAppSession) {
router.use(appSession(config.appSession));
}
router.use(appSession(config));
// Express context and OpenID Issuer discovery.
router.use(async (req, res, next) => {
req.openid = new RequestContext(config, req, res, next);
try {
await req.openid.load();
} catch(err) {
next(err);
}
res.openid = new ResponseContext(config, req, res, next);
req.isAuthenticated = () => req.openid.isAuthenticated;
req.oidc = new RequestContext(config, req, res, next);
res.oidc = new ResponseContext(config, req, res, next, transient);
next();
});
if (config.routes) {
// Login route, configurable with loginPath.
router.get(
enforceLeadingSlash(config.loginPath),
express.urlencoded({ extended: false }),
(req, res) => {
res.openid.login({ returnTo: config.baseURL });
}
// Login route, configurable with routes.login
if (config.routes.login) {
const path = enforceLeadingSlash(config.routes.login);
debug('adding GET %s route', path);
router.get(path, express.urlencoded({ extended: false }), (req, res) =>
res.oidc.login({ returnTo: config.baseURL })
);
} else {
debug('login handling route not applied');
}
// Logout route, configured with logoutPath.
router.get(
enforceLeadingSlash(config.logoutPath),
(req, res) => res.openid.logout()
);
// Logout route, configurable with routes.logout
if (config.routes.logout) {
const path = enforceLeadingSlash(config.routes.logout);
debug('adding GET %s route', path);
router.get(path, (req, res) => res.oidc.logout());
} else {
debug('logout handling route not applied');
}
const callbackMethod = ('form_post' === authorizeParams.response_mode ? 'post' : 'get');
const transientOpts = { legacySameSiteCookie: config.legacySameSiteCookie };
// Callback route, configured with routes.callback.
{
let client;
const path = enforceLeadingSlash(config.routes.callback);
const callbackStack = [
(req, res, next) => {
debug('%s %s called', req.method, path);
next();
},
async (req, res, next) => {
next = cb(next).once();
// Callback route, configured with redirectUriPath.
router[callbackMethod](
enforceLeadingSlash(config.redirectUriPath),
express.urlencoded({ extended: false }),
cookieParser(),
async (req, res, next) => {
next = cb(next).once();
try {
const redirectUri = res.openid.getRedirectUri();
const client = req.openid.client;
client =
client ||
(await getClient(config).catch((err) => {
next(err);
}));
const returnedState = transient.getOnce('state', req, res, transientOpts);
if (!client) {
return;
}
let tokenSet;
try {
const callbackParams = client.callbackParams(req);
tokenSet = await client.callback(redirectUri, callbackParams, {
nonce: transient.getOnce('nonce', req, res, transientOpts),
state: returnedState,
response_type: authorizeParams.response_type,
});
} catch (err) {
throw createError.BadRequest(err.message);
}
const redirectUri = res.oidc.getRedirectUri();
req.openidState = decodeState(returnedState);
req.openidTokens = tokenSet;
let expectedState;
let tokenSet;
try {
const callbackParams = client.callbackParams(req);
expectedState = transient.getOnce('state', req, res);
const max_age = parseInt(
transient.getOnce('max_age', req, res),
10
);
const code_verifier = transient.getOnce('code_verifier', req, res);
const nonce = transient.getOnce('nonce', req, res);
if (useAppSession) {
let identityClaims = tokenSet.claims();
tokenSet = await client.callback(redirectUri, callbackParams, {
max_age,
code_verifier,
nonce,
state: expectedState,
});
} catch (err) {
throw createError.BadRequest(err.message);
}
config.identityClaimFilter.forEach(claim => {
delete identityClaims[claim];
// TODO:?
req.openidState = decodeState(expectedState);
// intentional clone of the properties on tokenSet
Object.assign(req[config.session.name], {
id_token: tokenSet.id_token,
access_token: tokenSet.access_token,
refresh_token: tokenSet.refresh_token,
token_type: tokenSet.token_type,
expires_at: tokenSet.expires_at,
});
req[config.appSession.name].claims = identityClaims;
attemptSilentLogin.resumeSilentLogin(req, res);
next();
} catch (err) {
next(err);
}
},
(req, res) => res.redirect(req.openidState.returnTo || config.baseURL),
];
next();
} catch (err) {
next(err);
}
},
config.handleCallback,
function (req, res) {
res.redirect(req.openidState.returnTo || config.baseURL);
}
);
debug('adding GET %s route', path);
router.get(path, ...callbackStack);
debug('adding POST %s route', path);
router.post(
path,
express.urlencoded({ extended: false }),
...callbackStack
);
}
if (config.required) {
const requiresAuthMiddleware = requiresAuth();
if (typeof config.required === 'function') {
router.use((req, res, next) => {
if (!config.required(req)) { return next(); }
requiresAuthMiddleware(req, res, next);
});
} else {
router.use(requiresAuthMiddleware);
}
if (config.authRequired) {
debug(
'authentication is required for all routes this middleware is applied to'
);
router.use(requiresAuth());
} else {
debug(
'authentication is not required for any of the routes this middleware is applied to ' +
'see and apply `requiresAuth` middlewares to your protected resources'
);
}
if (config.attemptSilentLogin) {
debug("silent login will be attempted on end-user's initial HTML request");
router.use(attemptSilentLogin());
}
// Fail on initialization if config is invalid.
getClient(config);
return router;
};
const createError = require('http-errors');
const debug = require('../lib/debug')('requiresAuth');
const defaultRequiresLogin = (req) => !req.oidc.isAuthenticated();
/**
* Returns a middleware that verifies the existence of req.openid.user.
* If "user" is not in the session it will redirect to /login,
* otherwise continue to the next middleware in the stack.
*/
module.exports = function() {
return async function(req, res, next) {
const requiresLogin = !req.openid ||
!req.openid.user;
* Returns a middleware that checks whether an end-user is authenticated.
* If end-user is not authenticated `res.oidc.login()` is triggered for an HTTP
* request that can perform a redirect.
*/
async function requiresLoginMiddleware(requiresLoginCheck, req, res, next) {
if (!req.oidc) {
next(
new Error('req.oidc is not found, did you include the auth middleware?')
);
return;
}
if (requiresLogin) {
if (res.openid && res.openid.login && !res.openid.errorOnRequiredAuth) {
return res.openid.login();
}
return next(createError.Unauthorized('Authentication is required for this route.'));
if (requiresLoginCheck(req)) {
if (!res.oidc.errorOnRequiredAuth && req.accepts('html')) {
debug(
'authentication requirements not met with errorOnRequiredAuth() returning false, calling res.oidc.login()'
);
return res.oidc.login();
}
debug(
'authentication requirements not met with errorOnRequiredAuth() returning true, calling next() with an Unauthorized error'
);
next(
createError.Unauthorized('Authentication is required for this route.')
);
return;
}
next();
debug('authentication requirements met, calling next()');
next();
}
module.exports.requiresAuth = function requiresAuth(
requiresLoginCheck = defaultRequiresLogin
) {
return requiresLoginMiddleware.bind(undefined, requiresLoginCheck);
};
function checkJSONprimitive(value) {
if (
typeof value !== 'string' &&
typeof value !== 'number' &&
typeof value !== 'boolean' &&
value !== null
) {
throw new TypeError('"expected" must be a string, number, boolean or null');
}
}
module.exports.claimEquals = function claimEquals(claim, expected) {
// check that claim is a string value
if (typeof claim !== 'string') {
throw new TypeError('"claim" must be a string');
}
// check that expected is a JSON supported primitive
checkJSONprimitive(expected);
const authenticationCheck = (req) => {
if (defaultRequiresLogin(req)) {
return true;
}
const { idTokenClaims } = req.oidc;
if (!(claim in idTokenClaims)) {
return true;
}
const actual = idTokenClaims[claim];
if (actual !== expected) {
return true;
}
return false;
};
return requiresLoginMiddleware.bind(undefined, authenticationCheck);
};
module.exports.claimIncludes = function claimIncludes(claim, ...expected) {
// check that claim is a string value
if (typeof claim !== 'string') {
throw new TypeError('"claim" must be a string');
}
// check that all expected are JSON supported primitives
expected.forEach(checkJSONprimitive);
const authenticationCheck = (req) => {
if (defaultRequiresLogin(req)) {
return true;
}
const { idTokenClaims } = req.oidc;
if (!(claim in idTokenClaims)) {
return true;
}
let actual = idTokenClaims[claim];
if (typeof actual === 'string') {
actual = actual.split(' ');
} else if (!Array.isArray(actual)) {
debug(
'unexpected claim type. expected array or string, got %o',
typeof actual
);
return true;
}
actual = new Set(actual);
return !expected.every(Set.prototype.has.bind(actual));
};
return requiresLoginMiddleware.bind(undefined, authenticationCheck);
};
module.exports.claimCheck = function claimCheck(func) {
// check that func is a function
if (typeof func !== 'function' || func.constructor.name !== 'Function') {
throw new TypeError('"claimCheck" expects a function');
}
const authenticationCheck = (req) => {
if (defaultRequiresLogin(req)) {
return true;
}
const { idTokenClaims } = req.oidc;
return !func(req, idTokenClaims);
};
return requiresLoginMiddleware.bind(undefined, authenticationCheck);
};

@@ -7,7 +7,7 @@ /**

* routes.
*/
module.exports = function() {
*/
module.exports = function () {
return (err, req, res, next) => {
if (err.statusCode === 401) {
return res.openid.login();
return res.oidc.login();
}

@@ -14,0 +14,0 @@ next(err);

{
"name": "express-openid-connect",
"version": "1.0.2",
"version": "2.0.0-beta.0",
"description": "Express middleware to protect web applications using OpenID Connect.",

@@ -15,43 +15,62 @@ "homepage": "https://github.com/auth0/express-openid-connect",

"scripts": {
"lint": "eslint . --ignore-path .gitignore",
"lint": "eslint .",
"start:example": "node ./examples/run_example.js",
"test": "mocha",
"test:ci": "nyc --reporter=lcov npm test"
"test:ci": "nyc --reporter=lcov npm test",
"docs": "typedoc --options typedoc.js index.d.ts",
"test:end-to-end": "mocha end-to-end"
},
"peerDependencies": {
"express": ">= 4.17.0"
"mocha": {
"exit": true,
"file": "./test/setup.js",
"timeout": 10000
},
"dependencies": {
"@hapi/joi": "^16.1.8",
"@hapi/joi": "^17.1.1",
"cb": "^0.1.0",
"clone": "^2.1.2",
"cookie": "^0.4.0",
"cookie-parser": "^1.4.4",
"futoin-hkdf": "^1.3.1",
"cookie": "^0.4.1",
"debug": "^4.1.1",
"futoin-hkdf": "^1.3.2",
"http-errors": "^1.7.3",
"jose": "^1.24.0",
"jose": "^1.27.1",
"on-headers": "^1.0.2",
"openid-client": "^3.14.1",
"p-memoize": "^3.1.0",
"openid-client": "^3.15.3",
"p-memoize": "^4.0.0",
"url-join": "^4.0.1"
},
"devDependencies": {
"@types/express": "^4.17.2",
"@types/express": "^4.17.6",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"dotenv": "^8.2.0",
"eslint": "^5.16.0",
"express": "^4.17.1",
"jsdom": "^13.2.0",
"jsonwebtoken": "^8.5.1",
"mocha": "^6.2.2",
"express-oauth2-bearer": "^0.4.0",
"husky": "^4.2.5",
"lodash": "^4.17.15",
"mocha": "^7.2.0",
"nock": "^11.9.1",
"nyc": "^15.0.0",
"pem-jwk": "^2.0.0",
"proxyquire": "^2.1.3",
"nyc": "^15.1.0",
"oidc-provider": "^6.27.0",
"prettier": "^2.0.5",
"pretty-quick": "^2.0.1",
"puppeteer": "^5.2.0",
"request": "^2.88.2",
"request-promise-native": "^1.0.8",
"selfsigned": "^1.10.7",
"sinon": "^7.5.0"
"sinon": "^7.5.0",
"typedoc": "^0.17.8",
"typescript": "^3.9.6"
},
"peerDependencies": {
"express": ">= 4.17.0"
},
"engines": {
"node": "^10.13.0 || >=12.0.0"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
}
}

@@ -5,7 +5,2 @@ # Express OpenID Connect

This library requires:
- Node v10.13 or higher
- Express v4.17 or higher
[![CircleCI](https://img.shields.io/circleci/build/github/auth0/express-openid-connect/master?style=flat-square)](https://circleci.com/gh/auth0/express-openid-connect/tree/master)

@@ -18,5 +13,7 @@ [![codecov](https://img.shields.io/codecov/c/github/auth0/express-openid-connect?style=flat-square)](https://codecov.io/gh/auth0/express-openid-connect)

- [Documentation](#documentation)
- [Installation](#installation)
- [Install](#install)
- [Getting Started](#getting-started)
- [Architecture](./ARCHITECTURE.md)
- [Contributing](#contributing)
- [Troubleshooting](./TROUBLESHOOTING.md)
- [Support + Feedback](#support--feedback)

@@ -31,12 +28,12 @@ - [Vulnerability Reporting](#vulnerability-reporting)

- Use the [Examples for common configurations](https://github.com/auth0/express-openid-connect/blob/master/EXAMPLES.md) for use cases beyond the basics.
- The [API documentation](https://github.com/auth0/express-openid-connect/blob/master/API.md) details all configuration options, methods, and data that this library provides.
- The [API documentation](https://auth0.github.io/express-openid-connect) details all configuration options, methods, and data that this library provides.
- You can [run the sample application](https://github.com/auth0-samples/auth0-express-webapp-sample/tree/master) to see how this SDK functions without writing your own integration.
## Installation
## Install
This library is installed with [npm](https://npmjs.org/package/express-openid-connect):
Node.js version **>=12.0.0** is recommended, but **^10.13.0** lts/dubnium is also supported.
```bash
npm install express-openid-connect
```
npm i express-openid-connect --save
```

@@ -47,3 +44,3 @@ ## Getting Started

The library needs [the following required configuration keys](https://github.com/auth0/express-openid-connect/blob/master/API.md#required-keys) to request and accept authentication. These can be configured with environmental variables:
The library needs [issuerBaseURL](https://auth0.github.io/express-openid-connect/interfaces/configparams.html#issuerbaseurl), [baseURL](https://auth0.github.io/express-openid-connect/interfaces/configparams.html#baseurl), [clientID](https://auth0.github.io/express-openid-connect/interfaces/configparams.html#clientid) and [secret](https://auth0.github.io/express-openid-connect/interfaces/configparams.html#secret) to request and accept authentication. These can be configured with environmental variables:

@@ -54,3 +51,3 @@ ```text

BASE_URL=https://YOUR_APPLICATION_ROOT_URL
APP_SESSION_SECRET=LONG_RANDOM_VALUE
SECRET=LONG_RANDOM_VALUE
```

@@ -63,11 +60,9 @@

const { auth } = require("express-openid-connect");
const { auth } = require('express-openid-connect');
app.use(
auth({
issuerBaseURL: "https://YOUR_DOMAIN",
baseURL: "https://YOUR_APPLICATION_ROOT_URL",
clientID: "YOUR_CLIENT_ID",
appSession: {
secret: "LONG_RANDOM_STRING"
}
issuerBaseURL: 'https://YOUR_DOMAIN',
baseURL: 'https://YOUR_APPLICATION_ROOT_URL',
clientID: 'YOUR_CLIENT_ID',
secret: 'LONG_RANDOM_STRING',
})

@@ -81,3 +76,3 @@ );

See the [API documentation](API.md) for additional configuration possibilities and provided methods.
See the [API documentation](https://auth0.github.io/express-openid-connect) for additional configuration possibilities and provided methods.

@@ -84,0 +79,0 @@ ## A note on error handling

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc