- Basic setup
- Require authentication for specific routes
- Route customization
- Obtaining access tokens to call external APIs
- Obtaining and using refresh tokens
- Calling userinfo
- Protect a route based on specific claims
- Logout from Identity Provider
- Validate Claims from an ID token before logging a user in
- Use a custom session store
- Back-Channel Logout
- Custom Token Exchange
The simplest use case for this middleware. By default all routes are protected. The middleware uses the Implicit Flow with Form Post to acquire an ID Token from the authorization server and an encrypted cookie session to persist it.
# .env
ISSUER_BASE_URL=https://YOUR_DOMAIN
CLIENT_ID=YOUR_CLIENT_ID
BASE_URL=https://YOUR_APPLICATION_ROOT_URL
SECRET=LONG_RANDOM_STRING
// basic.js
const express = require('express');
const { auth } = require('express-openid-connect');
const app = express();
app.use(auth());
app.get('/', (req, res) => {
res.send(`hello ${req.oidc.user.sub}`);
});What you get:
- Every route after the
auth()middleware requires authentication. - If a user tries to access a resource without being authenticated, the application will redirect the user to log in. After completion the user is redirected back to the resource.
- The application creates
/loginand/logoutGETroutes.
Full example at basic.js, to run it: npm run start:example -- basic
If your application has routes accessible to anonymous users, you can enable authorization per route:
const { auth, requiresAuth } = require('express-openid-connect');
app.use(
auth({
authRequired: false,
}),
);
// Anyone can access the homepage
app.get('/', (req, res) => {
res.send('<a href="/admin">Admin Section</a>');
});
// requiresAuth checks authentication.
app.get('/admin', requiresAuth(), (req, res) =>
res.send(`Hello ${req.oidc.user.sub}, this is the admin section.`),
);Full example at routes.js, to run it: npm run start:example -- routes
If you need to customize the provided login, logout, and callback routes, you can disable the default routes and write your own route handler and pass custom paths to mount the handler at that path.
When overriding the callback route you should pass a authorizationParams.redirect_uri value on res.oidc.login and a redirectUri value on your res.oidc.callback call.
app.use(
auth({
routes: {
// Override the default login route to use your own login route as shown below
login: false,
// Pass a custom path to redirect users to a different
// path after logout.
postLogoutRedirect: '/custom-logout',
// Override the default callback route to use your own callback route as shown below
callback: false,
},
}),
);
app.get('/login', (req, res) =>
res.oidc.login({
returnTo: '/profile',
authorizationParams: {
redirect_uri: 'http://localhost:3000/callback',
},
}),
);
app.get('/custom-logout', (req, res) => res.send('Bye!'));
app.get('/callback', (req, res) =>
res.oidc.callback({
redirectUri: 'http://localhost:3000/callback',
}),
);
app.post('/callback', express.urlencoded({ extended: false }), (req, res) =>
res.oidc.callback({
redirectUri: 'http://localhost:3000/callback',
}),
);
module.exports = app;Please note that the login and logout routes are not required. Trying to access any protected resource triggers a redirect directly to Auth0 to login. These are helpful if you need to provide user-facing links to login or logout.
Full example at custom-routes.js, to run it: npm run start:example -- custom-routes
If your application needs an access token for external APIs you can request one by adding code to your response_type. The Access Token will be available on the request context:
app.use(
auth({
authorizationParams: {
response_type: 'code', // This requires you to provide a client secret
audience: 'https://api.example.com/products',
scope: 'openid profile email read:products',
},
}),
);
app.get('/', async (req, res) => {
let { token_type, access_token } = req.oidc.accessToken;
const products = await request.get('https://api.example.com/products', {
headers: {
Authorization: `${token_type} ${access_token}`,
},
});
res.send(`Products: ${products}`);
});Full example at access-an-api.js, to run it: npm run start:example -- access-an-api
Refresh tokens can be requested along with access tokens using the offline_access scope during login. On a route that calls an API, check for an expired token and attempt a refresh:
app.use(
auth({
authorizationParams: {
response_type: 'code', // This requires you to provide a client secret
audience: 'https://api.example.com/products',
scope: 'openid profile email offline_access read:products',
},
}),
);
app.get('/', async (req, res) => {
let { token_type, access_token, isExpired, refresh } = req.oidc.accessToken;
if (isExpired()) {
({ access_token } = await refresh());
}
const products = await request.get('https://api.example.com/products', {
headers: {
Authorization: `${token_type} ${access_token}`,
},
});
res.send(`Products: ${products}`);
});Full example at access-an-api.js, to run it: npm run start:example -- access-an-api
If your application needs to call the /userinfo endpoint you can use the fetchUserInfo method on the request context:
app.use(auth());
app.get('/', async (req, res) => {
const userInfo = await req.oidc.fetchUserInfo();
// ...
});Full example at userinfo.js, to run it: npm run start:example -- userinfo
You can check a user's specific claims to determine if they can access a route:
const {
auth,
claimEquals,
claimIncludes,
claimCheck,
} = require('express-openid-connect');
app.use(
auth({
authRequired: false,
}),
);
// claimEquals checks if a claim equals the given value
app.get('/admin', claimEquals('isAdmin', true), (req, res) =>
res.send(`Hello ${req.oidc.user.sub}, this is the admin section.`),
);
// claimIncludes checks if a claim includes all the given values
app.get(
'/sales-managers',
claimIncludes('roles', 'sales', 'manager'),
(req, res) =>
res.send(`Hello ${req.oidc.user.sub}, this is the sales managers section.`),
);
// claimCheck takes a function that checks the claims and returns true to allow access
app.get(
'/payroll',
claimCheck(({ isAdmin, roles }) => isAdmin || roles.includes('payroll')),
(req, res) =>
res.send(`Hello ${req.oidc.user.sub}, this is the payroll section.`),
);When using an IDP, such as Auth0, the default configuration will only log the user out of your application session. When the user logs in again, they will be automatically logged back in to the IDP session. To have the user additionally logged out of the IDP session you will need to add idpLogout: true to the middleware configuration.
const { auth } = require('express-openid-connect');
app.use(
auth({
idpLogout: true,
// auth0Logout: true // if using custom domain with Auth0
}),
);The afterCallback hook can be used to do validation checks on claims after the ID token has been received in the callback phase.
app.use(
auth({
afterCallback: (req, res, session) => {
const claims = jose.JWT.decode(session.id_token); // using jose library to decode JWT
if (claims.org_id !== 'Required Organization') {
throw new Error('User is not a part of the Required Organization');
}
return session;
},
}),
);In this example, the application is validating the org_id to verify that the ID Token was issued to the correct Organization. Organizations is a set of features of Auth0 that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications.
If you don't know the Organization upfront, then your application should validate the claim to ensure that the value received is expected or known and that it corresponds to an entity your application trusts, such as a paying customer. If the claim cannot be validated, then the application should deem the token invalid. See https://auth0.com/docs/organizations/using-tokens for more info.
By default the session is stored in an encrypted cookie. But when the session gets too large it can bump up against the limits of the platform's max header size (16KB for Node >= 14, 8KB for Node <14). In these instances you can use a custom session store. The store should have get, set and destroy methods, making it compatible with express-session stores.
const { auth } = require('express-openid-connect');
const { createClient } = require('redis');
const RedisStore = require('connect-redis')(auth);
// redis@v4
let redisClient = createClient({ legacyMode: true });
redisClient.connect().catch(console.error);
// redis@v3
let redisClient = createClient();
app.use(
auth({
session: {
store: new RedisStore({ client: redisClient }),
},
}),
);Full example at custom-session-store.js, to run it: npm run start:example -- custom-session-store
Configure the SDK with backchannelLogout enabled. You will also need a session store (like Redis) - you can use any express-session compatible store.
// index.js
const { auth } = require('express-openid-connect');
const { createClient } = require('redis');
const RedisStore = require('connect-redis')(auth);
// redis@v4
let redisClient = createClient({ legacyMode: true });
redisClient.connect();
app.use(
auth({
idpLogout: true,
backchannelLogout: {
store: new RedisStore({ client: redisClient }),
},
}),
);If you're already using a session store for stateful sessions you can just reuse that.
app.use(
auth({
idpLogout: true,
session: {
store: new RedisStore({ client: redisClient }),
},
backchannelLogout: true,
}),
);- Create the handler
/backchannel-logoutthat you can register with your Identity Provider. - On receipt of a valid Logout Token, the SDK will store an entry by
sid(Session ID) and an entry bysub(User ID) in thebackchannelLogout.store- the expiry of the entry will be set to the duration of the session (this is customisable using the onLogoutToken config hook) - On all authenticated requests, the SDK will check the store for an entry that corresponds with the session's ID token's
sidorsub. If it finds a corresponding entry it will invalidate the session and clear the session cookie. (This is customisable using the isLoggedOut config hook) - If the user logs in again, the SDK will remove any stale
subentry in the Back-Channel Logout store to ensure they are not logged out immediately (this is customisable using the onLogin config hook)
The config options are documented here
When your app logs in with one API audience but needs to call a different downstream service, customTokenExchange() lets you swap the session's access token for one accepted by that service. This follows the OAuth 2.0 Token Exchange spec (RFC 8693).
const { auth, requiresAuth } = require('express-openid-connect');
const axios = require('axios');
app.use(
auth({
authorizationParams: {
response_type: 'code',
// Token issued at login is scoped to the upstream API
audience: 'https://api.example.com',
scope: 'openid profile offline_access',
},
}),
);
app.get('/reports', requiresAuth(), async (req, res, next) => {
try {
// Exchange the session token for one accepted by the reporting service.
// subject_token and subject_token_type are resolved automatically.
const { access_token } = await req.oidc.customTokenExchange({
audience: 'https://reports.internal.example.com',
scope: 'openid read:reports',
});
const { data } = await axios.get(
'https://reports.internal.example.com/v1/summary',
{ headers: { Authorization: `Bearer ${access_token}` } },
);
res.json(data);
} catch (err) {
// Authorization server rejections surface as HTTP 400 and 401
// with err.error and err.error_description set
next(err);
}
});subject_token is resolved automatically from the session's accessToken and subject_token_type defaults to urn:ietf:params:oauth:token-type:access_token
. The returned token is ephemeral — it is not stored in the session, so use it within the same request.
Vendor-specific parameters (e.g., Auth0 organization routing or Token Vault connection) can be passed through the extra option:
const { downstreamToken } = await req.oidc.customTokenExchange({
audience: 'https://downstream-api.example.com',
scope: 'read:data',
extra: {
organization: 'org_abc123',
connection: 'google-oauth2',
},
});