OpenID Connect Client by Example

John Tucker
codeburst
Published in
6 min readAug 20, 2020

--

A walk-through of a concrete implementation of an OpenID Connect Client.

In this article we will walk through the code of an example Client participating in an OAuth 2.0, with OpenID Connect, Authorization Code Grant Flow. The Authorization Server in this example is the Google Identity Platform. The example client consists of an Express (Node.js) backend (download) and React frontend (download). This article is inspired by the excellent material found in An Illustrated Guide to OAuth and OpenID Connect by David Neal which I would recommend reading before diving in further here.

Step 1: The resource owner wants the client to use the identity from the authorization server

Note: This and the following illustrations are from the material An Illustrated Guide to OAuth and OpenID Connect.

The most relevant code snippet is from the frontend module src/api/oidc.js that prepares the application for login and loads the login screen.

Points to observe:

  • The login-screen endpoint is provided by the backend application as we will see in the next step
  • We will discuss the purpose of the uniquely generated strings, state, and nonce, in a later section; an important observation here, however, is that we persist in using them in the browser using localStorage

Step 2: Client redirects the browser to the authorization server

The most relevant code snippet is from the backend module index.js which provides the login-screen endpoint to redirect the browser to Google’s authorization server.

Point to observe:

  • The generateAuthUrl method simplifies the construction of Google’s OAuth 2.0 with OpenID Connect Authentication URI, e.g.:
https://accounts.google.com/o/oauth2/v2/auth?
access_type=offline&
prompt=consent&
nonce=[OBMITTED]&
scope=[OBMITTED]&
state=[OBMITTED]&
response_type=code&
client_id=[OBMITTED]&
redirect_uri=[OBMITTED]

Note: While the OAuth 2.0 maintainers recommend using the PKCE extension for additional security with the Authorization Code Grant Flow, Google does not mention it in their OpenID Connect documentation. Google, however, does utilize it in their OAuth 2.0 for Mobile & Desktop documentation. For simplicity and consistency with Google, the example in this article does not attempt to use the PKCE extension.

Steps 3 and 4: Authorization Server Verifies Identity and Consent

As this part of the flow is managed by the authorization server, i.e., Google, there is no client code supporting it.

Step 5: The authorization server redirects back to the client

The authorization server, Google, redirects the browser to the configured callback URI with two key parameters: state and code. In this case to the frontend application (running on http://localhost:3000 during development):

http://localhost:3000/?
state=[OBMITTED]&
code=[OBMITTED]&
scope=[OBMITTED]&
authuser=0&
hd=[OBMITTED]
&prompt=consent#

The most relevant code snippet is from the frontend module src/api/oidc.js:

Points to observe:

  • Because the backend application is stateless, in that it does not maintain client sessions, the callback URI is the frontend application that can maintain state (e.g., using localStorage)
  • Here we see how we are using one, state, of the two, uniquely generated persisted strings in Step 1. By comparing the persisted state (in localStorage) to the state parameter from the redirection we are protecting against a Cross-Site Request Forgery attack specific to OAuth 2.0 (and thus OpenID Connect)

Steps 6 and 7: Client Contacts Authorization Server to Exchange Code for Tokens

The first relevant code snippet is from the frontend module src/api/oidc.js; supplying the authorization code (code) to the backend application and receiving tokens in return.

Note: We will come back to the nonce validation in a bit.

Point to observe:

  • Here the jwt_decode function does not validate the id_token; it simply decodes it. This is safe because the id_token is securely passed through trusted parties, i.e., from Google and through the backend application.

The second relevant code snippet is from the backend module index.js accepting the code from the frontend application, contacting the authorization server to exchange it for tokens (id_token and refresh_token), and returning the tokens to the frontend application.

Points to observe:

  • The getToken method simplifies the API call to Google’s OAuth 2.0, with OpenID Connect, token endpoint, e.g.:
POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
code=[OBMITTED]&
client_id=[OBMITTED]&
client_secret=[OBMITTED]&
redirect_uri=[OBMITTED]&
grant_type=authorization_code
  • Because the Client Secret is not to be broadly shared, e.g., to the Browser, the backend application, and not the frontend application, must obtain the tokens from the Authorization Server

The last relevant code snippet is from the frontend module src/api/oidc.js; validates the nonce in the returned id_token matches the second, nonce, of the two uniquely generated persisted strings in the first step.

Point to observe:

  • Much like, and actually redundant to, the use of the state persisted string, the use of the nonce persisted string protects against a Cross-Site Request Forgery attack specific to OAuth 2.0 (and thus OpenID Connect). This countermeasure, however, is specific to OpenID Connect

Step 8: Client Accesses Protected Data on Resource Server

In this example the client does not interact with a Resource Server; we only are interested in authentication using the id_token. While we do get an access_token from the Authorization Server, we do not use it.

Authentication using an id_token

Once the OAuth 2.0, with OpenID Connect, Authorization Grant Flow completes, the frontend application has an id_token; specifically stored in localStorage. When the frontend application needs to access a protected backend application endpoint, it supplies the id_token in an Authorization header as we can see in the relevant src/api/hello.js module:

Note: This same module includes additional logic around using the refresh_token to obtain an updated id_token when it expires one hour after it is issued.

The protected backend application endpoint extracts the id_token from the Authorization header and verifies (and decodes) it before supplying the intended response; returning a 401 status code response if the id_token is missing or invalid. This is implemented in the backend module index.js:

Points to observe:

  • Verifying the id_token involves first using an appropriate Google public key to verify its signature; then verifying its iss and aud claims, and finally ensuring it is not expired
  • This same module includes the logic to obtain the appropriate Google public key

Conclusion

A walk-through of a concrete implementation of an OpenID Connect Client. There are quite a few nuances to this practice so, while I tried to keep the code as simple as possible, it is a bit longer than I originally hoped for. At the same time you can rest assured that it is fully functional. Sharing information shouldn’t feel like drawing blood from a stone, however, there is a level of security that we want to maintain as we do so. I hope you have found this article helpful, please feel free to leave feedback in the comments!

--

--