Let's make OpenID Connect crystal-clear!

Yet another OAuth extension?

Well, I must admit that I’ve been considering this protocol as a simple extension of OAuth for quiet a long time.
Like, "yeah it’s just another endpoint accessible through '/userinfo' that gives you some claims about the authenticated user. And you can also request this chunck of info by adding the openid scope to your OAuth request".

That’s not wrong.

But I like to use the right vocable when I’m dealing with security concerns.

So, I would like to start with this simple definition:

"OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 [RFC6749] protocol. It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner."

— https://openid.net/specs/openid-connect-discovery-1_0.html

Yes, Authorization Servers deal with Authentication.
Most of the time, they don’t do it themselves, they delegate this part to some Identity Providers. Actually, the OAuth 2 RFC states that…​

"…​the [AS] MUST first verify the identity of the resource owner. The way in which the authorization server authenticates the resource owner (e.g., username and password login, session cookies) is beyond the scope of this specification"

— https://tools.ietf.org/html/rfc6749#section-3.1

Of course, here I’m talking about OAuth flows with the notion of Resource Owner present (Code flow, Implicit Flow). Otherwise, it would be a non-sense.

Authorization Servers (AS) implementing the OpenID Connect protocol are also named OpenID Providers.

As stated in the first quote, the OIDC protocol permits authentication in a "REST-like manner", thanks to the underlying OAuth protocol. So, this can be very convenient to have a common way of working for all Native apps, JS apps and backend apps.

Basics of OpenID requests and responses

A typical workflow for a webapp willing to authenticate an end-user via OIDC is to use the Authorization Code flow. So the webapp first invites the Resource Owner to grab an authorization code from the AS by authenticating himself. To do so, the webapp sends a redirect URL to the end-user browser, like the following :

https://<authorization server>/authorize?
    client_id=<Client ID>
    &grant_type=code
    &scope=openid
    &redirect_uri=<redirect URI>

When authentication succeeds, the AS returns a code as a parameter to the redirect_uri. Generally, this URI is a callback URL of the webapp.

The webapp will then exchange that code for an access_token AND an id_token, because the scope openid was included in the initial request.

curl -X POST https://<authorization server>/token?
    grant_type=authorization_code
    &code=<returned code>
    &redirect_uri=<redirect URI>
{
  "access_token" : "eyJraWQiOiJ...",
  "token_type" : "...",
  "expires_in" : 7199,
  "scope" : "openid",
  "id_token" : "eyJraWQ..."
}

So, here is what you can do with those two tokens:

  • id_token first verify its signature and validity, see below. Then consume its claims that can contain some profile information

  • access_token: can be used later by the Client (our webapp) to fetch some more information about the profile, by requesting the UserInfo endpoint, typically /userinfo on the AS.

A few words about scopes

There are a few other standardized scopes that can be part of the first call to /authorize, as described in the spec, like profile, email, address and a few more.

adding such particular scopes like 'profile', 'email', etc. will bring back additional claims if they exist. For instance, if the 'profile' scope were added to the initial request, then you’ll get 'Scoped claims' like 'preferred_username', 'name', 'given_name', etc. as long as they are available.

The UserInfo endpoint

This endpoint can be a source of information about your end-user, with potentially much more claims returned as a standard JSON object or a JWT.

In order to consume this endpoint, the Client must use the access_token.

Here is an example of request:

GET /userinfo HTTP/1.1
Host: <authorization server>
Authorization: Bearer <access_token>

ID Token verification

Checking the JWT signature

Depending on the hash algorithm that was configured on the AS, you will have to use either the "Client Secret" or a "Public Key" in order to verify the signature of the returned id_token JWT.

The hash algorithm is specified via the alg header attribute.

For example, RS256 stands for "RSA Signature with SHA-256", so it’s an asymmetric encryption and you will need to fetch the Public Key from the AS (publicly exposed, see below) in order to validate the encryption.

The counter-example is HS256, which stands for "HMAC with SHA-256", so it’s a symmetric algorithm and you will need your private key, aka. the Client Secret which was provided when you first registered your application.

So, in the case of the Public Key usage, you can fetch it by browsing the JWKS (Json Web KeyStore) exposed by the AS.

If you don’t know where this JWKS stuff is exposed, just cross your fingers and hope that the AS you are using actually implements the Open ID Connect Discovery extension and thus exposes a .well-known URL.

URI pattern working with most of AS:

GET https://<authz server>/.well-known/openid-configuration HTTP/1.1

This will give you a lot of information about your OpenID Connect server, including the URI to the JWKS.

Make sure to use the right public key from the JWKS by comparing the kid field to the one inserted in your JWT headers.

Checking the JWT content

Among the list of claims returned in a id_token JWT, some of them are required, others are optionals.

That permits to a Client to verify that the JWT is the one expected and is valid. The specification defines a few key points for these checkings.

Also, it’s good to know that a fresh new BCP (Best Current Practices, a special kind of RFC) is currently under validation by the OAuth workgroup at the IETF. It suggests a nice a list of importants checkings to do as a Client receiving a JWT: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bcp-07

OpenID Connect Discovery

An OpenID Provider (OP) must publish its configuration somewhere behind this URL suffix: /.well-known/openid-configuration.

The response contains configuration information specific to OpenID Connect, like signature algorithms, supported claims, etc.

It’s really similar to the OAuth 2.0 Authorization Server Metadata endpoint (/.well-known/oauth-authorization-server).

The OIDC playground

Maybe you’ve heard about the Google OAuth playground https://developers.google.com/oauthplayground/, which is fantastic when you want to verify that your fresh new AS is properly setup. Or some other one, like the one embedded with any PingFederate instance.

Also, OIDC comes with very similar playgrounds, just for you :)
https://openidconnect.net/ or https://oidcdebugger.com/

Dynamic Client Registration

This is another extension to OpenID Connect, enabling Client to self-register to an OP (OpenID Provider).

A new Client is supposed to provide some information about itself in the registration request.

See this example:

POST /connect/register HTTP/1.1
  Content-Type: application/json
  Host: <OP>

  {
   "application_type": "web", (1)
   "redirect_uris": (2)
     ["https://client.example.org/callback",
      "https://client.example.org/callback2"],
   "client_name": "My Example" (3)
  }
1 defines the Client type: "web" or "native".
2 REQUIRED. Whitelist of redirect URIs. Only those ones will be accepted in further Client requests.
3 Label shown to end-users

The OP returns this kind of HTTP response:

 HTTP/1.1 201 Created
  Content-Type: application/json

  {
   "client_id": "<Client ID>", # REQUIRED
   "client_secret": "<Client Secret>",
   "registration_access_token": "<registration token>", (1)
   "registration_client_uri": (2)
     "https://server.example.com/connect/register?client_id=ABCDEF",
   "application_type": "web",
   "redirect_uris":
     ["https://client.example.org/callback",
      "https://client.example.org/callback2"],
   "client_name": "My Example",
  }
1 this token can be later used to read Client information from the OP
2 this is the unique endpoint accessible to read information from the OP, with the registration token above

Identity exchange between security domains

As we have seen, OIDC is a authentication layer on top of OAuth. So, given an end-user is authenticated through an OAuth flow, one can assume that the returned id_token JWT is valid (see the 'ID Token verification' paragraphs above).

Then, once you have a signed JWT, you can use that as an assertion of a valid authentication (also called an Authorization Grant, like the code in the Authorization Code flow) that can be transmitted to another Authorization Server, as long as this latter AS trusts the Public Key used to sign the JWT.

That permits a kind of chain of trust between two different AS and thereby security domains.

For example, if the division of the company I’m working for has its own AS and if I want to be recognized by another division of my company hosting its own AS, I can ask the second one to trust the first one.

My identity will be propagated via the JWT, acting as an Authorization Grant between them.

The RFC 7523 defines this behavior, namely "JWT Bearer".

The value of the grant_type becomes urn:ietf:params:oauth:grant-type:jwt-bearer

Final words

I hope you enjoyed as much as I did this long list of RFC references 😉 and that you now feel ready to tackle modern authentication !! 🔑