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 required token_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.