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:
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 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 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.
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.
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.
The flow to obtain a token in the frontend will look like this:
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:
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.
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
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.
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.