Disclaimer - This post is NOT sponsored by Auth0 or any other IdP which has been mentioned in this post.
Introduction
There is a lot of confusion around integrating an OAuth 2.0/OIDC compliant IdP or authorization server into different types of web applications, whether these are built using a client-side framework (e.g., React, Vue) or a server-side framework (e.g., Express.js, Django).
When you register an application with an authorization server, you are given a client ID and a client secret. The client ID is a unique identifier for the application, similar to how an email ID uniquely identifies a person. The client secret is like a password for the application. Together with the client ID, it verifies the application's identity to the authorization server, since the client ID, like an email ID, may be known to many third parties.
While the Authorization Code Flow with Proof Key for Code Exchange (PKCE) is recommended for client-side applications or SPAs, we will explore how to use the Authorization Code Flow with these applications using oauth2-proxy, which offers additional benefits. This approach simplifies the complexities of integrating an OAuth 2.0/OIDC compliant authorization server with a SPA (SaaS) application.
In this post, we will focus only on OIDC-compliant authorization servers, as this is the most common integration for web applications. Every OIDC-compliant authorization server is also an OAuth 2.0-compliant authorization server, but the reverse is not true. From this point onward, we will refer to an OIDC-compliant authorization server simply as an authorization server.
OAuth 2.0 and OIDC
OAuth2 (Open Authorization 2.0) is a widely adopted framework that allows third-party applications to obtain limited access to a user's resources without exposing their credentials. It operates through a series of token exchanges, ensuring secure and delegated access. OpenID Connect (OIDC) is an identity layer built on top of OAuth2, providing a standardized way to verify user identities. OIDC extends OAuth2 by introducing an ID token, which contains user information and authentication details, enabling seamless single sign-on (SSO) experiences. Together, OAuth2 and OIDC facilitate secure, scalable, and user-friendly authentication and authorization processes in modern web applications.
OAuth 2.0 flows
OAuth 2.0 defines several flows, or grant types, to accommodate different use cases and client types.
Authorization Code Flow: This is the most common flow, used by server-side applications to exchange an authorization code for an access token securely.
Implicit Flow: Designed for client-side applications, where the access token is returned directly in the URL fragment. It is less secure and generally discouraged.
Resource Owner Password Credentials Flow: Allows users to provide their credentials directly to the client, suitable for trusted applications but not recommended for public clients.
Client Credentials Flow: Used for machine-to-machine communication, where the client authenticates itself to obtain an access token without user involvement.
What is a JWT token?
A JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. JWTs are commonly used for authentication and authorization purposes, allowing secure information exchange. They consist of three parts: a header, a payload, and a signature. The header typically consists of the type of token (JWT) and the signing algorithm being used. The payload contains the claims, which are statements about an entity (typically, the user) and additional data. The signature is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way.
Many people use terms like JWT authentication, which is not correct. JWT is simply a data exchange format used for authentication and authorization purposes. It is utilized for access and ID tokens in various OAuth 2.0 flows and has built-in security to ensure its integrity.
What is oauth2-proxy?
oauth2-proxy is an open-source reverse proxy and static file server that provides authentication using OAuth 2.0 and OpenID Connect (OIDC) providers. It acts as an intermediary between the client and the backend server, ensuring that only authenticated requests are forwarded. By integrating with various identity providers like Google, GitHub, and many others, oauth2-proxy simplifies the process of adding authentication to web applications. It supports multiple authentication flows, including the Authorization Code Flow with Proof Key for Code Exchange (PKCE), making it suitable for both client-side and server-side applications. Additionally, oauth2-proxy can handle session management, token validation, and user information retrieval, providing a robust and flexible solution for securing web applications.
The problem
Let's say you want to integrate one or more SPAs with an OIDC-compliant IdP or authorization server (e.g., Auth0, Google Identity Platform, Microsoft Identity Platform). To do this, you need to register all such applications with an OIDC-compliant authorization server of your choice. You will be asked to provide additional details like Allowed Callback URLs etc. After registration, you will receive a client ID and client secret for your application, which are required to interact with the authorization and token endpoints of an OIDC-compliant authorization server.
The problem with this approach is that you need to register every SPA with an IdP or authorization server. Each SPA must implement a callback URL to invoke the /token endpoint of the authorization server to exchange the authorization code (similar to an OTP) for an access and/or ID token.
The Solution
When you use oauth2-proxy, which runs in the backend, you only need to register this proxy with your authorization server. For your SPAs, this proxy acts as an authorization server, while for the authorization server, it acts as a client application.
There are many advantages for this approach -
Since this proxy runs in the backend, it can securely store the client ID and client secret. Storing these in a SPA poses several security risks, which is why authorization code flow with PKCE is recommended for SPAs. By storing the client ID and client secret in the backend, we eliminate the need to use this flow with one or more SPAs.
You only need to register one application (oauth2-proxy) with your authorization server instead of multiple SPAs, greatly simplifying the integration.
SPAs do not need to implement any callback URL to exchange the authorization code for an access token (and an ID token).
oauth2-proxy wraps your access or ID token into a cookie after encrypting it, ensuring your token is NOT exposed on the client side in its original format.
oauth2-proxy also provides an /auth endpoint, which can be used by a reverse proxy (e.g., nginx) or a Kubernetes ingress controller to check if a web application, acting on behalf of a user, has a valid access or ID token.
Integrating multiple SPAs with an IdP using oauth2-proxy involves a few simple steps:
When a user accesses a SPA, it should call the /auth endpoint of oauth2-proxy to check if the user has a valid session. The /auth endpoint returns 202 if the user has a valid session.
If the user does not have a valid session, the SPA should redirect the user to the /start endpoint of oauth2-proxy, which will begin the authorization code flow.
After a successful login, oauth2-proxy will create a cookie and redirect the user to the SPA with the cookie in the response.
The SPA will repeat step 1. Since there is a valid session this time, it will proceed with normal rendering instead of redirecting the user to the /start endpoint of oauth2-proxy.
You need not to use framework (e.g. react, vue) or platform (e.g. auth0) specific libraries / SDKs in order to integrate oauth 2.0/oidc with your SPAs. Steps described in previous step can be coded in plain javascript in a generic manner and you can use this code across any SPA framework with little or no change.
This setup works well if you need to integrate your applications with a single IdP/authorization server. This is common for internal applications used by a company's employees. However, this approach may not be suitable for a SaaS application that needs to support multiple public authorization servers (Google, Microsoft, Facebook, LinkedIn, etc.) for B2C clients or an internal authorization server (Azure Entra ID, Keycloak etc.) for B2B clients. To handle B2C clients, you can use an intermediate IdP (e.g. Auth0) to allow your users to sign into a SaaS application with an IdP/authorization server of their choice. This is known as federated login.
Federated login has many advantages -
In federated login, an access or ID token is issued by an intermediate IdP (e.g., Auth0). Your SaaS application only needs to verify the token with one IdP, unlike a scenario where you integrate with multiple IdPs and must validate the JWT token against the issuing IdP. This simplifies token validation and the management of IdP metadata in SaaS applications.
The intermediate IdP may offer features like MFA and passwordless authentication, so you don't need to implement these in your SaaS application or your IdP.
The intermediate IdP may also provide multi-layer hard/soft multi-tenancy, which is essential for many SaaS applications to meet strict data residency requirements. This is useful for serving business users from different countries or regions with their own data residency or privacy needs.
For B2B customers, you can either use the above setup by configuring IdPs of such customers in the intermediate IdP or deploy an instance of oauth2-proxy for each of the tenants representing a B2B customer. Each such instance of oauth2-proxy should be configured with the IdP / authorization server of the B2B customer.
Choosing between these two setups will depend on several factors -
Onboarding a B2B customer's IdP into an intermediate IdP can be costly, so as a SaaS owner, you might not want to do it.
If your SaaS application is integrating with the customer's APIs, using direct integration without an intermediate IdP, as shown in the second setup above, will simplify the process. Your SaaS application will receive JWT tokens signed by the customer's IdP, which you can then forward to the customer's APIs. These APIs will accept the tokens without any additional configuration. However, if you use an intermediate IdP, the customer will need to configure its APIs to trust your intermediate IdP, as the JWT tokens will be issued by the intermediate IdP.
Summary
This article addresses the complexities of integrating OAuth 2.0 and OIDC compliant authorization servers into web applications, focusing on the advantages of using oauth2-proxy. By utilizing oauth2-proxy, you can streamline the integration for Single Page Applications (SPAs) by registering only the proxy with the authorization server, thereby securely handling client credentials, eliminating the need for individual SPA callbacks, and simplifying session management and token validation. While this approach is effective for single IdP scenarios, federated login via platforms like Auth0 is recommended for SaaS applications that need to support multiple IdPs, offering benefits such as simplified token validation, advanced security features, and multi-tenancy support.