Implementing Oauth2 and OpenID Connect in your application

January 27, 2022
Approximately a 
00
 
minutes read
Tutorial
Oauth2, OIDC, PKCE etc. can be daunting to implement. Let's take a step back and describe these protocols, from a developers point of view.

Understand how to secure your application with Oauth2

Most companies develop internal or customer-facing APIs, using external authentication providers such as Azure Active Directory (Azure AD), Auth0, Google, Keycloak etc.
This article will teach you a few concepts that we need to secure an application with Oauth2 or OpenID connect, specifically Azure AD, but everything you learn from this article can be translated into other authentication providers.

This article will answer these questions:

  • What are Oauth2 and OpenID Connect?
  • What are JSON Web Tokens?
  • How can we authenticate and get a signed JSON web token (JWT) from a single-page application (SPA)?
  • How can I validate this JWT in the backend?

To explain this from a developers point of view, we'll write some simple Python code and focus on the implementation part of the protocols.

Oauth2 and OpenID Connect

Oauth2 is a protocol (RFC6749) that handles authentication and authorization. Azure AD, Google, GitHub etc. use Oauth2 to authenticate their users, and when you want your user to log into an app through these sites, Oauth2 is what's used behind the scenes. In addition to Oauth2, Azure (and most other third party sites) has an additional layer on top of which is called OpenID connect (OIDC). OIDC has a few extra standards and is in general easier to use as a developer than pure Oauth2. It also allow for some extra information to be sent, such as user information.

JSON Web Tokens

JSON Web Token (JWT) is what the user get back from our auth provider after authenticating. The OIDC JWT will contain so-called claims. These claims contains information such as the family name, who issued the JWT, how long this token is valid for etc.

Fetching tokens

There are many ways to fetch tokens, and which way is the best will vary depending on use case and the stack. Modern web applications are (mostly) written with a SPA frontend and a separate backend which communicate through APIs. In single-page applications the tokens are safely fetched from the authentication provider without any involvement of the backend.

If you've implemented Oauth2 or OIDC from a single-page application previously, you may have heard of implicit flow. The implicit flow unfortunately has a few weaknesses (though most can be avoided), such as redirect-interception or for malicious apps to access the token, and is no longer a recommended flow (Implicit Flow with Form Post is still secure). Single-page applications should use a flow called Authorization Code Grant Flow, with Proof Key for Code Exchange. We'll refer to this as the PKCE flow, which is pronounced pixy flow.

  • asd

The Authorization Code Grant flow (Chapter 4.1 in RFC6749) is described as a method to obtain access_tokens through confidential clients. Since our frontend is a SPA, it is not a confidential client, and that's why we have the PKCE "extension". PKCE is described in RFC7636, but in short, it works by creating a long random string and hashing this string with the SHA256 algorithm.

In Python this can be done like this:
import secrets
import hashlib
code_verifier = secrets.token_urlsafe(96)
code_challenge = hashlib.sha256(code_verifier.encode('ascii')).digest()

The flow to obtain a token in the frontend will look like this:

  1. The user clicks on the sign-in button in our single-page application.
  2. The SPA generates a cryptographically-random code_verifier, and hashes this string with SHA256 to generate a code_challenge (as seen in the snippet above).
  3. The SPA redirect the user to Azure AD's /oauth2/v2.0/authorize endpoint, together with the code_challenge.
  4. The user logs in through Azure AD and consent to the permissions the application requires.
  5. Azure AD stores the code_challenge and redirect the user back to our application together with an authorization code, which can only be used once.
  6. The SPA POSTs the authorization code and the code_verifier to the oauth2/v2.0/token endpoint.
  7. Azure AD use the code_verifier and generates their own hash using the same algorithm, ensuring that it's equal to first code_challenge. If it's equal, Azure AD will respond with an JWT, which we'll refer to as the access_token.
  8. We can use this access_token to authenticate to our backend API, by attaching it in the request headers.

Validating tokens

The user can now securely fetch their access_token and send requests to our backend with this token attached in the request header. All we now need to do now is to verify it. It's important to differ between verify and decode here. The access_token consists of three parts, separated by a dot (.): (header).(claims).(signature), in base64 format. This means we can decode the token without verifying it. Here, we decode the header of a token:

>>> base64.b64decode('eyJhbGciOiJSUzI1NiIsImtpZCI6InNvbWVrZXlpZCIsInR5cCI6IkpXVCJ9')
b'{
  "alg": "RS256", 
  "kid":"somekeyid",
  "typ": "JWT",
}'

We have a token which is encoded and we can read the claims of, but we also need to verify that it's a legitimate token. This is possible with a few libraries in Python (a good library overview for most languages can be found at jwt.io), and we'll use python-jose here.

The access_token headers above said it was signed with the the RS256 asymmetric algorithm. This means that the JWT was signed with a private key, and we can validate it using a public key. The HS256 is a symmetric algorithm, where a common key has to be shared between the services. Azure AD uses RS256 by default.

<info>The trained eye might spot that the JWT is signed and not encrypted. A digital signature is when we add a cryptographic hash to the message and then sign that hash with our private key. This generates a digital signature that is attached to the token.<info>

To fetch the public key(s) needed to validate the token, we can use the v2.0/.well-known/openid-configuration endpoint. This OpenID connect endpoint responds with a JSON body that will tell us a few things about the configuration, including where to find its public keys. We get the URL by looking at the key jwks_uri. In my case that's at discovery/v2.0/keys.

When fetching this endpoint, we get a list of keys:
{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "somekeyid",
      "x5t": "some value",
      "n": "long value",
      "e": "some value",
      "x5c": [
        "very long value"
      ],
      "issuer": "https://login.microsoftonline.com/(my tenant ID)/v2.0"
    },
    ...
  ]
}

The x5c and kid are the two values we really care about. We'll use the kid to match with the access_token's kid, and use that x509 certificate chain (under the x5c key). In RFC7515, section 4.1.6 we can see the first value in this list is the key we need.

The certificate containing the public key corresponding to the key used to digitally sign the JWS MUST be the first certificate
So, now that we know that this is a x509 certificate chain, we can load this into Python:
from cryptography.hazmat.backends.openssl.backend import backend
from cryptography.x509 import load_der_x509_certificate
for key in keys:
    if key.get('use') == 'sig':  # Only care about keys that are used for signatures, not encryption
        cert_obj = load_der_x509_certificate(base64.b64decode(key['x5c'][0]), backend)
        key_to_use = cert_obj.public_key()
And with all that, we have the pieces to verify our token, using a JWT library.
pip install python-jose[cryptography]
And as a full example script, we can do this
import base64
import json
from cryptography.hazmat.backends.openssl.backend import backend
from cryptography.x509 import load_der_x509_certificate
from jose import jwt

# Obtain your access token

access_token = "(header).(payload/claims).(signature)"  

# Decode the header
header, claims, signature = access_token.split(".")
header_as_dict = json.loads(base64.b64decode(header))

for key in keys:
    if key.get("kid") == header_as_dict.get("kid"):
        token = jwt.decode(
            access_token,
            key=load_der_x509_certificate(base64.b64decode(key["x5c"][0]), backend).public_key(),
            algorithms=["RS256"],
            audience="api://oauth299-9999-9999-abcd-efghijkl1234567890",
            issuer="https://sts.windows.net/our_tenant_ID/"
        )

I strongly recommend setting options for the jwt.decode() manually to require audience etc. For your own implementation, you can be inspired by our FastAPI-Azure-Auth package.

Summary

OpenID Connect can be confusing at first, but the implementation is actually pretty simple. OpenID Connect uses access_tokens, which are JSON Web Tokens with three different parts: header, claims and the signature. The token can be retrieved safely from a SPA by using the PKCE flow, and passed to the backend as a header. The backend will then validate the token, using a public key provided by the authentication provider.

If you want to implement Azure AD authentication for your FastAPI application, we've built a package that handles everything for you, including configuring your OpenAPI documentation as a SPA with a login button. The documentation also walks you through how to configure Azure AD. If you're creating an application from scratch, create.intility.app is a good place to start or get inspiration - we support multiple programming languages, including react for your SPA.

If you have any questions or comments, you can contact me at jonas.svensson@intility.no.

Table of contents

if want_updates == True

follow_intility_linkedin

Other articles