Small demo Node.js Express application using private keys and OpenID Connect
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
complete
generate_keys
initial_app
.gitignore
README.md

README.md

oidc_nodejs_demo

Small demo Node.js Express application using private keys and OpenID Connect

What you'll build

You'll build an Express web application with a login backed by OpenID Connect

What you'll need

Create an unsecured web application

Once Node.js is installed, we're ready to setup our project. We'll use the Express Generator to set up the scafold of our project.

Install the generator globally $ npm install express-generator -g

Next we'll generate our application using the Pug template engine (Which was formally called Jade). We'll change into the resulting directory and run npm install to finish setting up the dependencies.

 $ express --view=pug myapp
 $ cd myapp
 myapp$ npm install

At this point executing DEBUG=myapp:* npm start will start the server and you should be able to go to http://localhost:3000/ and see the Express landing page.

Generate RSA keys

If you are going to be using signed keys rather than a client-secret for authenticating your client to the OpenID Connect server, you'll need to gerenrate them. We'll be using the node-jose library to do this. NOTE: in order for the node-jose library to use RSA256 keys, it requires all the optional parameters for the private key, and not just the required d parameter. To accomidate this, we'll use the node-jose library to generate our keys.

Install the node-jose library while in your myapp directory with:

myapp$ npm install node-jose --save

Next you'll want to add the generate_keys file in myapp/bin and execute it to generate a public key in pub_key.jwk and the private key in full_key.jwk. Use the contents of the pub_key.jwk file when you register your client.

#!/usr/bin/env node
const jose = require('node-jose');
const fs = require('fs');

// Set the properties used in generating the keys
const props = {
	"kty": "RSA",
	"e": "AQAB",
	"kid": "my_rsa_key", // set this to whatever you'd like your key id to be
	"alg": "RS256"
};

const pub_key_file = '../pub_key.jwk';
const full_key_file = '../full_key.jwk';

// Create a keystore
keystore = jose.JWK.createKeyStore();

// Generate the key using 2048 bits
keystore.generate("RSA", 2048, props)
	.then(result => {
		key = result;
		// write out the public key
		fs.writeFileSync(pub_key_file, JSON.stringify({keys: [key.toJSON()]}));
		// write out the public/private key (DO NOT SHARE!)
		fs.writeFileSync(full_key_file, JSON.stringify({keys: [key.toJSON(true)]}));
	})
.catch(err => {console.log(err);})

Set up Passport and openid-client

Passport makes it easy to add authentication to an Express application with a variaty of backends. Since we'll also want to make sure we're not re-logging in a user on every page hit, we'll want to add server side sessions with express-session.

myapp $ npm install express-session --save
myapp $ npm install passport --save

Next we'll set these up by modifying app.js.

Near the top, add in the require statements.

...
var app = express();

// passport and sessions are required, so add them here.
// You'll have to 'npm install' these.
const passport = require('passport');
const session = require('express-session');
...

NOTE The express generator uses the var format from Node.js v6, which is ok, but we'll add in our new lines with v8 const syntax.

A bit further down in app.js we'll initialize things. First the session management with the app.use(session(...)); line. Then we'll initialize passport and tell it to use the sessions.

Lastly we'll tell passport how to serialize/deserialize users to the session, with a very simple function. (I imagine for more complicated applications these might be expanded.)

...
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// Here we'll set up using server side sessions
// this is using an in memory system. Use something like memcached or redis in production
app.use(session({ secret: 'ourapplicationsecret', // change this
		  resave: false,
		  saveUninitialized: false}));

// initialize passport here
app.use(passport.initialize());
app.use(passport.session());

// passport has to serialize/deserialize users to the sessions.
//  This is a simple case
passport.serializeUser((user, done) => { done(null, user);});
passport.deserializeUser((user, done) => { done( null, user);});

app.use('/', index);
app.use('/users', users);
...

Set up openid-client

Now we need to setup the openid-client which is how we'll connect to the openid-connect provider using the keys we generated earlier.

First we'll install it

myapp$ npm install openid-client --save

Next we'll add it to app.js and set up a Strategy for passport to use. Add the following after the passport require() statements you added before

// requirements for openid-client, as well as reading in the key we generated
const fs = require('fs'); // used to read in the key file
const jose = require('node-jose'); // used to parse the keystore
const Issue = require('openid-client').Issuer;
const Strategy = require('openid-client').Strategy;

// filename for the keys
const jwk_file = "./full_key.jwk";

// OpenID Connect provider url for discovery
const oidc_discover_url = "https://mitreid.org";

// Params for creating the oidc client
const client_params = {
        client_id: 'login-nodejs-govt-test',
        token_endpoint_auth_method: 'private_key_jwt'
};

// Params for the OIDC Passport Strategy
const strat_params = {
        redirect_uri: 'http://localhost:3000/openid-connect-login',
        scope: 'openid profile email phone address',
        response: ['userinfo']
};

Next we'll actually set up the OIDC Strategy. Add the following after the passport serialization code we added above

// load keystore and set up openid-client
const jwk_json = JSON.parse(fs.readFileSync(jwk_file, "utf-8"));
// load the keystore. returns a Promise
let load_keystore = jose.JWK.asKeyStore(jwk_json);
// discover the Issuer. returns a Promise
//   you can also set this up manually, see the project documentation
let discover_issuer = Issuer.discover(oidc_discover_url);

// Create a client, and use it set up a Strategy for passport to use
// since we need both the Issuer and the keystore, we'll use Promise.all()
Promise.all([load_keystore, discover_issuer])
        .then(([ks, myIssuer]) => {
                console.log("Found Issuer: ", myIssuer);
                const oidc_client = new myIssuer.Client(client_params, ks);
                console.log("Created client: ", oidc_client);

                // create a strategy along with the function that processes the results
                passport.use('oidc', new Strategy({client: oidc_client, params: strat_params}, (tokenset, userinfo, done) => {
                        // we're just loging out the tokens. Don't do this in production
                        console.log('tokenset', tokenset);
                        console.log('access_token', tokenset.access_token);
                        console.log('id_token', tokenset.id_token);
                        console.log('claims', tokenset.claims);
                        console.log('userinfo', userinfo);

                        // to do anything, we need to return a user. 
                        // if you are storing information in your application this would use some middlewhere and a database
                        // the call would typically look like
                        // User.findOrCreate(userinfo.sub, userinfo, (err, user) => { done(err, user); });
                        // we'll just pass along the userinfo object as a simple 'user' object
                        return done(null, userinfo);
                }));
        }) // close off the .then()
        .catch((err) => {console.log("Error in OIDC setup", err);});

Now all that is left is to set up the callback URL and add authentication requirements to protected URLs.

Setting up a protected page

We'll place all our authentication related routes in a single place routes/authenticated_routes.js. This file exports a single function that adds routes to the app, along with the required passport.autenticate(...) calls. There is a second function in the file isLoggedIn(req, res, next) which is a helper function for locking down a new route. (In this case the /profile route).

module.exports = function(app, passport) {
    app.get('/login', passport.authenticate('oidc'));

    // OIDC Callback
    app.get('/openid-connect-login', passport.authenticate('oidc', {successRedirect:'/profile', failureRedirect:'/'}));

    app.get('/profile', isLoggedIn, function(req, res){
        console.log("In profile");
        res.render('profile', {title: 'Express - profile', user: req.user.name});
    });

    app.get('/logout', function(req, res) {
        req.logout();
        res.redirect('/');
    });
};

function isLoggedIn(req, res, next) {
    if (req.isAuthenticated()) {
        console.log("Is authenticated");
        return next();
    }
    console.log("Not Authenticated");
    res.redirect('/');
}

Next we have to add this to the app.js file with the line:

// demo authentication routes
const authroutes = require('./routes/authenticated_routes')(app,passport);

The only restriction on that line is it has to be after app.use(passport.initialize()); is called, but it can be before we set up the Strategy.

Since we're adding a /profile page, we'll need a view. To keep things simple, we'll just pass in the name from the user object and put it in the text.

Add views/profile.pug:

extends layout

block content
  h1= title
  p Welcome, #{user}

  a(href="/logout") Logout

NOTE Logging out will only log you out of the Express app. If you are still logged into the OpenID Connect Provider, you'll just get automatically logged back in by clicking /login.

While we're at it, we'll add in a '/login' link to the index page. If we're sucessful on login, we'll get forwarded to the /profile.

Change views/index.pug to:

extends layout

block content
  h1= title
  p Welcome to #{title}

  a(href='/login') Login Here

Now start it up and hit http://localhost:3000/ with:

 DEBUG=myapp:* npm start

Summary

  • We set up an express app using the express generator (found in initial_app/)
  • We added the ability to generate secure keys for authenticating with an OpenID Connect provider using node-jose. (found in generate_keys/)
  • Lastly we integrated OpenID Connect with our express application using openid-client (found in complete/)