- 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
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
/login
and/logout
GET
routes.
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
},
})
);
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-logout
that 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
sid
orsub
. 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
sub
entry 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