Lab - Simple OIDC Client using Passport.js
Context To help understanding how an OIDC client works quickly.
Preparation
Create a folder and initialize a Node.js project
$ mkdir oidccli
$ cd oidccli
$ yarn init -y
$ yarn add express express-session passport openid-client
Add the Code
Add index.js
in the top folder:
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const { Issuer, Strategy } = require('openid-client');
const app = express();
app.use(session({ secret: 'secret', resave: false, saveUninitialized: true }));
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, done) => {
console.log('serializeUser:', user);
done(null, user);
});
passport.deserializeUser((user, done) => {
console.log('deserializeUser:', user);
done(null, user);
});
var oidcClient;
Issuer.discover('http://localhost:3000/oidc')
.then(issuer => {
oidcClient = new issuer.Client({
client_id: 'oidccli_id',
client_secret: 'oidccli_secret',
redirect_uris: ['http://localhost:8080/login/callback'],
post_logout_redirect_uris: ['http://localhost:8080/'],
response_types: ['code'],
});
passport.use('oidc', new Strategy(
{ client: oidcClient, passReqToCallback: true },
(req, tokenSet, userinfo, done) => {
const claims = tokenSet.claims();
console.log('claims:', claims);
console.log('userinfo:', userinfo);
return done(null, {
...claims,
name: claims.sub,
email: claims.sub + '@example.com',
id_token: tokenSet.id_token,
});
},
));
});
app.get('/login', passport.authenticate('oidc', { scope: 'openid' }));
app.get('/login/callback', (req, res, next) => {
passport.authenticate('oidc', {
successRedirect: '/profile',
failureRedirect: '/'
})(req, res, next);
});
app.get('/', (req, res) => {
if (req.user) {
res.send(`Hi, ${req.user.name} (${req.user.email}),<br>
you can go to <a href="/profile">profile</a>, or<br>
<a href="/logout">logout locally</a>, or<br>
<a href="/logout?oidc=1">logout globally</a>`);
} else {
res.send('<a href="/login">Log in with OIDC Provider</a>')
}
});
app.get('/profile', (req, res) =>{
res.send(`<pre>${JSON.stringify(req.user, null, 2)}</pre><a href="/">home</a>`);
});
app.get('/logout', (req, res) =>{
if (!req.session.passport?.user) return res.redirect('/');
let { token_id } = req.session.passport.user;
req.logout(err => {
if (err) { return next(err); }
if (!req.query.oidc) return res.redirect('/');
const url = oidcClient.endSessionUrl({
token_id_hint: token_id,
});
res.redirect(url);
});
});
app.listen(8080, () => {
console.log('Server running at http://localhost:8080');
});
Launch the Client App
Launch the client with node index.js
:
$ node index.js
Server running at http://localhost:8080
Notes
- The issuer is at a specific URI
http://localhost:3000/oidc
- The Strategy provided by the
openid-client
library is used using the Passport.js middleware - When logging out "locally," only the client's session is cleared
- When logging out "globally," the OIDC provider is involved via the
endSessionUrl()
call, hence the requiredtoken_id
needs to be kept somewhere. Here it is saved in the session for convenience. A better place is needed in actual work. Also, the session is cleared first, even though the user decline the logout request from the OIDC provider. This is to be revised.