Implementing Login with OAuth2 and OpenID Connect
When I started a new project, one of the first tasks was to add a login system. My goal was to avoid building and maintaining a user database from scratch. The obvious solution was to let users log in with an existing account, like Google. This seemed straightforward, but it led me down a path of learning about OAuth2 and OpenID Connect. This post is a summary of what I figured out along the way.
Provider vs. Client
The first hurdle was understanding the terminology. When I searched for libraries, I kept seeing the terms “OAuth2 Provider” and “OAuth2 Client,” and it wasn’t immediately clear which one I needed.
- An OAuth2 Provider is a service that manages user accounts and issues access tokens. Google, for example, is a provider. A library for building a provider is for when you want your application to be the source of identity for other apps.
- An OAuth2 Client is the application that needs to access a user’s data from a provider. Since my app needed to use Google for login, my app was the client.
This was the first key distinction. I didn’t need to build a provider; I needed to implement a client flow to connect to an existing one.
Breaking Down the Authorization Code Flow
To understand what a client library would do for me, I decided to look into the raw HTTP requests involved. The most common method for a web server is the Authorization Code Flow.
Here are the steps as I understood them:
- Start the Login: When a user clicks “Login,” my server redirects them to the provider’s authorization URL. This URL includes my app’s client_id, a redirect_uri (where the provider sends the user back), the scope of permissions I’m asking for, and a random state value for security.
- Get User Consent: The user lands on the provider’s site (e.g., Google’s login page), signs in, and approves the permissions my app requested.
- Receive the Code: The provider redirects the user back to my redirect_uri with a temporary code in the URL. My server has to check that the state value also sent back matches the one it originally sent.
- Exchange the Code for a Token: My server then makes a direct, server-to-server POST request to the provider’s token endpoint. It sends the code along with its client_id and client_secret. If everything checks out, the provider returns an access token.
Here’s what that POST request might look like using curl:
curl -X POST https://provider.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=TEMPORARY_CODE" \
-d "redirect_uri=https://myapp.com/callback" \
-d "client_id=MY_CLIENT_ID" \
-d "client_secret=MY_CLIENT_SECRET"
Once my app has this access token, it can use it to ask the provider’s API for the user’s information, like their name and email.
Other OAuth2 Flows
The Authorization Code flow isn’t the only one. I learned that other flows exist for different types of applications:
- Authorization Code with PKCE: This is an extension for clients that can’t keep a client_secret safe, like mobile apps or browser-based Single-Page Applications (SPAs).
- Implicit Flow: An older, less secure flow for SPAs that is now largely replaced by the PKCE flow.
- Device Code Flow: This is for devices that don’t have a browser or easy text input, like a smart TV.
Hands-On Learning with a Playground
Reading about these flows is one thing, but seeing them in action is another. I found a great resource at oauth.com/playground that lets you step through the different OAuth2 flows. It was helpful for visualizing how the requests and responses work in practice and solidified my understanding of the moving parts.
Connecting Authorization to Authentication with OpenID Connect
The next big realization was the difference between OAuth2 and OpenID Connect (OIDC). I had been using the terms almost interchangeably, but they serve different purposes.
- OAuth2 is a framework for authorization—it’s about granting permission to access resources. The access_token tells an API what the client is allowed to do.
- OpenID Connect is a layer built on top of OAuth2 for authentication—it’s about confirming who a user is.
The OIDC flow looks almost exactly like the OAuth2 Authorization Code flow. The main difference is in the response from the token endpoint. In addition to the access_token, an OIDC flow also provides an id_token.
This id_token is a JSON Web Token (JWT) that contains claims about the user’s identity, such as their unique ID, email, and name. This token is what makes “login” possible, as it’s a verifiable piece of evidence that the user has been authenticated by the provider.
Verifying the ID Token
Receiving an id_token isn’t the end of the story. My application can’t blindly trust the information inside it. The token’s signature must be verified to ensure it’s authentic and hasn’t been tampered with.
The process involves these steps:
- The identity provider signs the id_token with a private key.
- The provider makes the corresponding public key available at a public URL, known as a JSON Web Key Set (JWKS) endpoint.
- My application fetches this public key from the JWKS endpoint.
A simple curl command can retrieve the key set:
curl https://provider.com/.well-known/jwks.json
- It then uses the public key to verify the signature on the id_token.
Only after this signature check passes can my application trust the identity claims in the token and officially log the user in. This final step ensures the entire login process is secure.