Getting Started with oidc-provider

OpenID Connect

OpenID Connect

oidc-provider is an OpenID Connect provider for node.js, providing us with a secure authentication mechanism for our applications, and protection for our APIs. In this article, we’re going to walk through setting up oidc-provider and interacting with it using a couple of different ways.

Why oidc-provider?

It’s a Certified OpenID Provider Library and it’s a framework, unlike some providers which you can only mount and then modify select areas. Whilst this can be good for ensuring expected behaviour (you may be less likely to create security flaws or break functionality), it can be infuriating if you need custom logic or even grant types. The library is certified for all 5 OpenID Provider conformance profiles.

Project Setup

If, like me, you are a complete newbie to node.js and express.js, then the below commands are how we setup a new app:

npm init

Will setup our package.json for us.

npm install express --save

Will install Express, our web application framework.

npm install oidc-provider --save

Will install oidc-provider, our OpenID Connect Provider framework. At time of writing oidc-provider was at version 2.0.0.

oidc-provider 2.0 requires node v8.x due to its use of ES2015, async functions and utils.promisify.

Implementing oidc-provider using Express

Now that we have the required packages, let’s configure our node application to use express and oidc-provider. We can do this by simply creating a JavaScript file containing this following:

const express = require('express');
const Provider = require('oidc-provider');

const app = express();

const oidc = new Provider('http://localhost:3000');
oidc.initialize().then(function () {
    app.use('/', oidc.callback);
    app.listen(3000);
});

What we’ve done here is imported the two packages we need, created an Express application, created our OpenID Provider, initialised it, and then finally setup our Express app to use the oidc-provider’s callback property as its root request handler and listen on port 3000. oidc-provider also works fine in a different path (e.g. /oidc or /identity).

The string provided to the constructor of Provider is the name of our issuer, the authority that will issue tokens to client applications. In this case we have simply used the url we intend to publish the app on.

We can now run the application using (where app.js is the name of my JavaScript file we just created):

node app.js

OpenID Connect Discovery Document

We can see that the application is running correctly by navigating to the OpenID Connect Discovery Document found on the path: /.well-known/openid-configuration.

The OpenID Connect Discovery Document (affectionately known as the disco doc) allows client applications to automatically integrate with an OpenID Provider, without the need for manual configuration or formal metadata exchange. Instead, this document is always available on the same path on every authority, and allows client applications to find the location of the various OpenID Connect endpoints and what configurations it supports such as grant types, response types, claims types and scopes.

{
    "authorization_endpoint": "http://localhost:3000/auth",
    "claims_parameter_supported": false,
    "claims_supported": [
        "auth_time",
        "iss",
        "sub"
    ],
    "grant_types_supported": [
        "implicit",
        "authorization_code",
        "refresh_token"
    ],
    "id_token_signing_alg_values_supported": [
        "none",
        "HS256",
        "HS384",
        "HS512",
        "RS256"
    ],
    "issuer": "http://localhost:3000",
    "jwks_uri": "http://localhost:3000/certs",
    "request_object_signing_alg_values_supported": [
        "none",
        "HS256",
        "HS384",
        "HS512",
        "RS256",
        "RS384",
        "RS512",
        "PS256",
        "PS384",
        "PS512",
        "ES256",
        "ES384",
        "ES512"
    ],
    "request_parameter_supported": false,
    "request_uri_parameter_supported": true,
    "require_request_uri_registration": true,
    "response_modes_supported": [
        "form_post",
        "fragment",
        "query"
    ],
    "response_types_supported": [
        "code id_token token",
        "code id_token",
        "code token",
        "code",
        "id_token token",
        "id_token",
        "none"
    ],
    "scopes_supported": [
        "openid",
        "offline_access"
    ],
    "subject_types_supported": [
        "public"
    ],
    "token_endpoint": "http://localhost:3000/token",
    "token_endpoint_auth_methods_supported": [
        "none",
        "client_secret_basic",
        "client_secret_jwt",
        "client_secret_post",
        "private_key_jwt"
    ],
    "token_endpoint_auth_signing_alg_values_supported": [
        "HS256",
        "HS384",
        "HS512",
        "RS256",
        "RS384",
        "RS512",
        "PS256",
        "PS384",
        "PS512",
        "ES256",
        "ES384",
        "ES512"
    ],
    "userinfo_endpoint": "http://localhost:3000/me",
    "userinfo_signing_alg_values_supported": [
        "none",
        "HS256",
        "HS384",
        "HS512",
        "RS256"
    ],
    "code_challenge_methods_supported": [
        "plain",
        "S256"
    ],
    "claim_types_supported": [
        "normal"
    ]
}

Signing Certificate

To verify that a JSON Web Token (JWT) hasn’t been tampered with, we digitally sign the token with a private key and then later verify that signature using a public key. This certificate should be separate from your TLS certificate, and doesn’t need to be publicly verifiable (i.e. not issued by a public CA). By default oidc-provider uses the dev_keystore, which includes a hard-coded key for signing tokens. This private key isn’t suitable for production because, as the name suggests, it should be private. With the key on GitHub it is no longer private, meaning anyone with this key will be able to issue tokens as if they were your identity provider.

So, before you enter production, be sure to have implemented a keystore that loads your own certificates.

We can view the public keys using for token signing on the jwks_uri endpoint defined in our discovery document.

Clients, Scopes and Users

Whilst we have oidc-provider working, we can’t do much with it. What we need to configure now is some client applications, some scopes (representing our protected resources), and some users.

Clients

A client represents an application that uses our identity provider (a client is also known in other specifications as a relying party or service provider). OpenID Connect uses a whitelist style system, so all applications must first be registered before they can request access our protected resources.

So, let’s configure a client application that uses the implicit grant type, suitable for SPAs or other client-side applications that cannot keep a secret. When using the implicit grant, all tokens will pass via the browser, so treat your tokens accordingly.

const clients = [{
    client_id: 'test_implicit_app',
    grant_types: ['implicit'],
    response_types: ['id_token'],
    redirect_uris: ['https://testapp/signin-oidc'],
    token_endpoint_auth_method: 'none'
}];

Our redirect uri is where any tokens will be sent to once authorized by a user.

When using the implicit flow, oidc-provider has a hardcoded check against the use of http & localhost. We must also ensure that the token endpoint is disabled for the client. Whilst this is a good security feature, it makes demos awkward. So, when integrating with your client application, make sure you are using the https scheme and anything other than localhost (something configured via your hosts file works fine), and set the clients token_endpoint_auth_method property to none.

We can then pass this collection of clients into our initialize method:

oidc.initialize({clients}).then(function () {
    app.use('/', oidc.callback);
    app.listen(3000);
});

Using this in-memory style causes the clients to be verified as soon as the application starts.

You can find a full list of client configuration options in the client_schema.js file.

Scopes

Scopes can be added to oidc-provider in two different ways, either by declaring them manually in the scopes configuration property, or by creating them implicitly in the claims configuration property.

By default, only the openid and offline_access scopes are supported for authentication and refresh token functionality respectively. We can add some other identity scopes specified in the OpenID Connect specification by updating our Provider to the following:

const oidc = new Provider('http://localhost:3000', {
    claims: {
        email: ['email', 'email_verified'],
        phone: ['phone_number', 'phone_number_verified'],
        profile: ['birthdate', 'family_name', 'gender', 'given_name', 'locale', 'middle_name', 'name', 'nickname', 'picture', 'preferred_username', 'profile', 'updated_at', 'website', 'zoneinfo']
    }
});

Here we are specifying our scope, for example profile, and then listing the claim types they can include. If this scope hasn’t already been created, then it will be added to the list of supported scopes. Any claim types included here will also be added to the list of claims_supported in the discovery document.

We can also add scopes like so:

const oidc = new Provider('http://localhost:3000', {
    scopes: ['api1']
});

I would typically use this for scopes that represent an API and don’t need a particular set of claims to be included in its access token.

Users

By default, oidc-provider offers stub functionality for user authentication, suitable for test purposes only. This is in the form of a method that simply takes a user name provided and sets that as the subject claim (the users unique identifier). There is no out of the box user store or credential checking.

User store integration is handled by the findById configuration property of our Provider. By default, it is:

const oidc = new Provider('http://localhost:3000', {
    async findById(ctx, id) {
        return {
            accountId: id,
            async claims() { return { sub: id }; },
        };
    }
});

Where accountId is the unique identifier for that account (I imagine this would typically be the subject claim or a username). Claims can be returned as a Promise, and resolved at a later date. To switch to a full user store, you would simply modify the logic in this function to call a service containing your user management functionality.

User Interface

By default, oidc-provider gives us some very basic UI for demo’s and development purposes. The use of this UI is driven by the devInteractions configuration property. This property also controls the use of the stub user store we mentioned previously. This is set to true by default.

We won’t change the UI as part of this tutorial, so check out the interactions section of the GitHub docs for details on changing this.

OpenID Connect

Now let’s test our OpenID Provider with a test client application that we’ve already configured within oidc-provider. We’re not going to implement this client application in any particular technology, I’m going to leave that up to you, however, when creating client-side applications, I can recommend the oidc-client library from the IdentityModel library or the openid-client library from the same creator as oidc-provider. Both libraries are certified, however oidc-client is designed solely for client-side apps, whilst openid-client is designed for node apps.

So, no matter the library, we’re expecting a request that looks like the following (encoding removed for readability):

http://localhost:3000/auth
    ?client_id=test_implicit_app
    &redirect_uri=https://testapp/signin-oidc
    &response_type=id_token
    &scope=openid profile
    &nonce=123
    &state=321

Assuming everything checks out, we’ll be taken to a login page:

oidc-provider Development Login Page

Identity Token

If all checks out then we’ll be returned an identity token in the following format (where the sub of scott was the username I used):

"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleXN0b3JlLUNIQU5HRS1NRSJ9.eyJzdWIiOiJzY290dCIsIm5vbmNlIjoiNjM2MzYwODUwNjc3NjYzNjMxLk4yUmlOVFppWVdJdFptWmhZaTAwWXpVeExXRmtNV1F0WXpKaVlUWmpNR1ZoWlRJMVlUZGxaV0pqWVRJdFpUbG1OaTAwTldVekxXSmtaamt0TVRVd09XRTVPV000TVRReCIsImF0X2hhc2giOiJ6VXd5VVptaUttQzNYNjZoc01GbVdBIiwiaWF0IjoxNTAwNDg4MjY3LCJleHAiOjE1MDA0OTE4NjcsImF1ZCI6InRlc3RfaW1wbGljaXRfYXBwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIn0.n_pZWnVHCIFYKmRzHdpxcGDNidv3DGgc6eKZRm2lCJy6ECjFj08LIB7ubtkPYjWCnpkd_jL-a4p6b5BbHtEMFqdK7k5wCARaII3o0CQcbfR6CfFzmFIRtBCQfrqNGYBCRdVijwHL5HJOfGDHoEvfgAaInrMk6huwrONh7-nk_1VyBBld_9zziY5M7YsrXzKR352kWKmNSWh8mOG3NwpYZzGHT7MCx6N5NS5dnNezn72CaKKqs3af25DvaPn5aVk7X7YrkmrtVylk1YYZ8Z80XmKd-MFHd3IlEuLVj3rHgqIXsiXLM2z7ytFQlwlB4D86O-yUF_ghiVV8IklB6DLE4A"
"alg": "RS256",
"typ": "JWT",
"kid": "keystore-CHANGE-ME"
"sub": "scott",
"nonce": "636360850677663631.N2RiNTZiYWItZmZhYi00YzUxLWFkMWQtYzJiYTZjMGVhZTI1YTdlZWJjYTItZTlmNi00NWUzLWJkZjktMTUwOWE5OWM4MTQx",
"at_hash": "zUwyUZmiKmC3X66hsMFmWA",
"iat": 1500488267,
"exp": 1500491867,
"aud": "test_implicit_app",
"iss": "http://localhost:3000"

You’ll notice that the key ID (kid) is set to “keystore-CHANGE-ME”, another friendly reminder that this keystore and signing certificate is not suitable for production!

Consent

You’ll notice that our user wasn’t challenged to give consent to the requested scopes. This is definitely something you’ll want to implement, as currently a client application will be able to get request and gain a token with any scope that you provider supports. Unlike other OpenID Connect frameworks, oidc-provider doesn’t let you choose which scopes a client application is allowed to request, instead this functionality is solely up to the resource owner (the user) to delegate permission.

OAuth

Now let’s see how we can support a purely OAuth flow, for machine-to-machine communication, using the client credentials grant type. With this flow, we are only going to be able to request access tokens directly from the provider’s token endpoint, and there will be no user interaction.

First, we need to configure oidc-provider to allow the client credentials grant type. We’ll also want to enable the introspection endpoint so that we can validate our access tokens. By default, both of these are disabled:

const oidc = new Provider('http://localhost:3000', {
    features: {
        clientCredentials: true,
        introspection: true
    }
});

Now if we go to the discovery document, we can see both the token endpoint and the introspection endpoint (by default these are on the paths /token and /token/introspection respectively).

We then need to configure a client within oidc-provider. When using the client credentials grant type we need to use a slightly different configuration style than before, where this time we don’t enter a redirect uri or response type. After all, we’re not going to be calling the authorization endpoint or using the browser. So, our client will need to look something like this:

{
    client_id: 'test_oauth_app',
    client_secret: 'super_secret',
    grant_types: ['client_credentials'],
    redirect_uris: [],
    response_types: [],
}

We can now make a request to our token endpoint using the below, where the client id and secret are sent using the HTTP Basic authentication (I did this using Postman):

POST /token
Headers:
Content-Type: application/x-www-form-urlencoded
Authorization: Basic dGVzdF9vYXV0aF9hcHA6c3VwZXJfc2VjcmV0
Body:
grant_type=client_credentials&scopes=api1

In return, we get an access token, a time until expiry (in seconds) and the token type:

{
    "access_token": "NDE1NWZkMTctOGRiYS00N2VkLThjOTUtNTk5ZDhlZDcxN2VjBWpT5czOdOJSGSNKSo6_0FPBsZh7tB9CfWLXC-wmzvWiP9yrpNx_RVG9BWUBMyfKzmyalTOO1bSfp2-pAIyTmQ",
    "expires_in": 600,
    "token_type": "Bearer"
}

If we then take this token to the introspection endpoint (using the same client credentials for demo purposes):

POST /token/introspection
Headers:
Authorization: Basic dGVzdF9vYXV0aF9hcHA6c3VwZXJfc2VjcmV0
Content-Type: application/x-www-form-urlencoded
Body:
token= NDE1NWZkMTctOGRiYS00N2VkLThjOTUtNTk5ZDhlZDcxN2VjBWpT5czOdOJSGSNKSo6_0FPBsZh7tB9CfWLXC-wmzvWiP9yrpNx_RVG9BWUBMyfKzmyalTOO1bSfp2-pAIyTmQ

This will return us some information about our token, including whether or not it is valid, when it will expire, and what scopes the token has been authorized to access:

{
    "active": true,
    "token_type": "client_credentials",
    "client_id": "test_oauth_app",
    "exp": 1500498611,
    "iat": 1500498011,
    "iss": "http://localhost:3000",
    "jti": "OWUyMzE2MzEtZjgzMS00ODZiLWI5MDktODUzMzc4OTRlMDgw",
    "scope": "api1"
}

Logout

Again, ending a session is not supported by default, however we can easily enable session features using the sessionManagement configuration property:

const oidc = new Provider('http://localhost:3000', {
    features: {
        sessionManagement: true
    }
});

This enables both the check_session_iframe and end_session_endpoint features.

The check_session_iframe allows a client-side application to detect when the user session within oidc-provider changes, for instance when they log out of the provider, and trigger some code, maybe invalidating the session within that client application to facilitate single logout.

The end_session_endpoint allows us to trigger a log out request to our identity provider and can be initiated by a client application. This can then be used in combination with features such as check_session_iframe to enable single sign out.

Summary

So, this has been a fairly high-level overview of the oidc-provider library. We’ve already mentioned a couple of things to do before deploying an oidc-provider solution into production, such as configuring the UI, adding a consent screen, and loading in your own token signing key, however there are a couple of other things you might want to address or investigate.

Token Persistence

Currently any tokens we generate are being stored in memory within oidc-provider. Unfortunately, that means when we restart the app or if a request hits another instance of the app (for example in a load balanced environment), then any unstructured data such as access tokens or authorization codes issued will be invalidated, as they are only stored in memory. This also includes user sessions.

We can solve this by persisting them to something a little less volatile. You know, a database or something. Out of the box, oidc-provider provides reference implementations for Redis and MongoDB. See the persistence section of the docs for more information on how to set this up.

Client Persistence

Another limitation we currently have, is that we need to redeploy our application every time we update or add a client application. This is not ideal if this is a common occurrence, however it’s one you might be able to live with for small deployments. If you require an external store for clients, this can again be configured via an adaptor.

Default Settings

You can find the full list of default settings in the default.js file. Make sure you review these and confirm there isn’t anything you’ve missed or don’t require.