OAuth2 in Simple Terms
propelauthblog
Posted on September 9, 2024
If you’ve ever searched for OAuth (or OAuth2 or OpenID connect/OIDC), you’ve probably been greeted with a picture that looks like this:
along with some descriptions of resource servers, the difference between authentication and authorization, consent screens, token endpoints, and more.
And while that can be helpful, it’s usually more helpful if you already know what OAuth is for and a bit of how it works.
In this post, I want to start simpler, explain more of the why behind OAuth, and then afterwards we’ll dig into the weeds.
Walking through a simplified OAuth2 example
What is the problem we are trying to solve?
Succinctly describing OAuth can be challenging because it comes in a lot of different flavors that are for different use cases. Let’s start with one of the simplest use cases:
We are building a web application. We want users to be able to log in using their existing Google accounts. We also need to get the user’s email address as part of that login process.
Importantly, there are three parties here:
Us - we are building a web application
joe@example.com - the user, they are trying to log in to our web application
Google - the source of truth about Joe’s account
A wrong approach on the right track: Ask for the user’s Google password
First idea that comes to mind: let’s just ask the user for their Google email address and password. We can try and log in as them, and if it’s successful, we’ll know they are who they say they are!
Ignoring the obvious issue that no one should ever give out their Google password, this approach doesn’t work for a bunch of other reasons like:
Does the user give you their 2FA secret as well?
What happens when Google flags the login as coming from a new device?
We can’t use their password, but the underlying idea here is reasonable:
We need something unique to the user, that we can send to Google and have them respond with “Yes, this is joe@example.com”
A better approach: One time codes
The password was obviously a little sensitive, we shouldn’t ask the user for that. What if they instead could go to Google and ask for a short-lived code (e.g. aGV5IHRoZXJl
)?
The user then comes to us and says “Hey, my code is aGV5IHRoZXJl
, ask Google who I am”
We ask Google “Is aGV5IHRoZXJl
a valid code, and if so, who’s it for?”
Google responds with: “Yes, that’s joe@example.com”
We did it! The user was able to prove to us that they own the joe@example.com account that Google manages. We can create an account for Joe using that email address, log them in, and move on.
Google does need to be a little careful - they wouldn’t want aGV5IHRoZXJl
to be usable multiple times and ideally it has a very narrow window to be used in the first place.
This approach seems reasonable, the next questions are… how does Joe get a code? And how does Joe share the code with us?
Getting a code from Google
If you’ve ever been on a screen like this:
this is actually the first step in getting the code.
The way that it works is Google sets up a URL like https://identity.google.com/get-me-a-code
(not the real URL).
When we want to log a user in, we’ll redirect the user from our application to that URL.
The user then logs in to Google (if they weren’t already) and Google will create a code for that account.
Since we want this to be simple, ideally, the user doesn’t need to know about the code at all. Google can just send the user back to us, by redirecting them to: https://ourapp.com/handle-the-code?code=aGV5IHRoZXJl
We read the query parameter code
and send it to Google to determine if it’s valid and who it’s for (more on this later).
Putting it all together
The whole flow, end-to-end, looks like this:
Joe comes to our site and clicks “Log in with Google” which kicks off the OAuth2 flow
We redirect Joe to https://identity.google.com/get-me-a-code (still not the real URL)
Joe logs in to Google
Google generates a code
aGV5IHRoZXJl
Google redirects Joe back to our application with the code:
https://ourapp.com/handle-the-code?code=aGV5IHRoZXJl
We read the code from the URL and ask Google who it’s for
Google responds:
joe@example.com
And that’s all - as long as we trust Google, we know this user is joe@example.com
Understanding a more complete OAuth2 example
To make this more understandable, I skipped a few important steps in the OAuth 2.0 process. Let’s go back now and fill in some extra details.
Some of the things I skipped
How does Google know where to redirect the user back to?
Good question 🙂 The answer is we provide a query parameter that tells Google where to redirect the user back to:
https://identity.google.com/get-me-a-code?
redirect_uri=https://ourapp.com/handle-the-code
Why can’t someone put any redirect_uri in?
Since Google is just using query parameters, someone can come along and do this:
https://identity.google.com/get-me-a-code?
redirect_uri=https://uhoh.com
This can lead to a vulnerability called an open-redirect vulnerability, where a user thinks they are going to https://ourapp.com but actually end up on https://uhoh.com.
To prevent this, Google (and any OAuth provider) must require that you register your redirect URLs with them ahead of time.
Anyone attempting to redirect to https://uhoh.com would error as it’s an unknown URL and therefore invalid.
How does Google know who we are?
You might be wondering - how can Google validate our redirect URI? When we redirected the user to https://identity.google.com/get-me-a-code we didn’t include any information on who we are.
To solve this, when we first set up Sign in with Google, we have to register with them. We’re given a Client ID & Client Secret, which you can think of like a username and password. When we redirect our user to Google, we include the client ID in it:
https://identity.google.com/get-me-a-code?
redirect_uri=https://ourapp.com/handle-the-code
&client_id={CLIENT_ID}
This is how Google identifies us. Later on, when we verify the code with Google, we include both the client ID & client secret.
Can’t a malicious user make me log in to their account using one of their codes?
This is a really important point. Imagine we’re making a web application to help businesses manage and pay their bills in one central location. A malicious user comes along and sends us this link:
https://ourapp.com/handle-the-code?
code={A_CODE_THE_ATTACKER_GENERATED_FOR_THEIR_ACCOUNT}
We click this link and are immediately logged in to the attackers account. Hopefully you notice before you pay any of the bills - but it’s understandable to miss it, especially for big companies.
To prevent this issue, we add one more variable to the initial request, called state
. The state
should be a random value that you generate. It should both be unique for each request and unguessable:
https://identity.google.com/get-me-a-code?
redirect_uri=https://ourapp.com/handle-the-code
&client_id={CLIENT_ID}
&state=SOMETHING_MORE_RANDOM_THAN_THIS
Before redirecting the user, we save that state
value somewhere in our application (e.g. a cookie, localStorage, etc.). When the user comes back to our redirect URL, Google will include the state:
https://ourapp.com/handle-the-code?
code=aRandomCode
&state=SOMETHING_MORE_RANDOM_THAN_THIS
If the state matches the value we saved, we can be confident that we initiated the flow. Otherwise, we should reject the request.
Note: Sometimes people will persist information in the state, like this:
const state = base64encode({ returnToPath: "/app", random: genRandom() })
This is totally fine as long as it’s still has a random/unguessable component to it.
The biggest thing I skipped: Scopes
So far, we’ve looked at OAuth2 for one very specific reason: to get access to the email address of their Google account. However, OAuth2 was designed with more than just that in mind.
What if, in addition to getting the user’s email address, we also wanted to read health data from the user’s Google Fit account?
To do this, we can pass in a “scope” to Google, signifying the access we are requesting:
https://identity.google.com/get-me-a-code?
redirect_uri=https://ourapp.com/handle-the-code
&client_id={CLIENT_ID}
&state=SOMETHING_MORE_RANDOM_THAN_THIS
&scope=letMeReadFitData letMeReadEmail
Because many scopes are sensitive, the user will be asked if they consent to the scopes that you requested:
That page, unsurprisingly, is called a consent screen. Those scopes above were obviously fake, but here’s a list of scopes that Google supports.
How can we access the Google Fit data? Doesn’t Google respond to our “code” with an email address?
This was an area where I intentionally oversimplified things.
Google doesn’t respond to the our “code” with an email address. It actually responds with an access token. We can use this access token to make requests to Google on behalf of our user, based on what they consented to.
If the user consented to us having access to their email address, we can use the access token to fetch their email address.
If the user consented to us having their Google Fit data, we can use the access token to fetch their Google Fit data.
Putting it all together (again)
Ok, let’s bring it all together with one final walkthrough. As a reminder, our use case is:
We are building a web application. We want users to be able to log in using their existing Google accounts. We also need to get the user’s email address as part of that login process.
Before we start, we register with Google and tell Google to expect that we will redirect the user back to https://ourapp.com/handle-the-code
Google provides us with a Client ID and Client Secret.
When our user is ready to log in, we generate a random variable called state
and save it as a cookie:
state=XfDv59k5JZz00Tz0zGtb8TOzUqDTbFqZ
Then, we kick off the OAuth flow by redirecting the user to the authorization URL:
https://identity.google.com/get-me-a-code?
redirect_uri=https://ourapp.com/handle-the-code
&client_id={CLIENT_ID}
&state=XfDv59k5JZz00Tz0zGtb8TOzUqDTbFqZ
&scope=https://www.googleapis.com/auth/userinfo.email
The user is sent to Google and asked to log in / select an account.
The user is then asked to consent to the scopes we selected - in this case, they’ll be asked if they are comfortable sharing their email address with us.
Assuming the user says yes, Google will redirect the user back to our application with a code and the state variable we passed in earlier:
https://ourapp.com/handle-the-code?
code=aRandomCode
&state=XfDv59k5JZz00Tz0zGtb8TOzUqDTbFqZ
We load our state cookie and verify that the query parameter and cookie match.
We then make a request to Google’s token endpoint to exchange the code for an access token:
POST https://identity.google.com/token
// This is standard "basic" authentication with
// client ID / client secret as username /password
Headers:
- Authorization: Basic {base64encoded(client_id:client_secret)}
URL Encoded Body:
- grant_type=authorization_code
- code=aRandomCode
- redirect_uri=https://ourapp.com/handle-the-code
Google returns an access token and that access token will have the ability to do whatever the user consented to.
In our case, this is as simple as making a request to
GET https://www.googleapis.com/oauth2/v3/userinfo
Headers:
- Authorization: Bearer {access token}
and getting the user’s email address and if it’s been confirmed.
That seemed like a lot?
OAuth can feel pretty heavy, especially for the login case, where we did all that just to get the user’s email.
This is made a bit worse by the fact that we’re only covering one grant type. There are other OAuth flows for the unique requirements of smart tvs and extensions like PKCE for mobile applications or cases where you can’t securely store the client secret. There are also extensions for OIDC that providers may or may not support.
Luckily, there are a lot of great OAuth client libraries that can hide most of the details from you.
Adding back the jargon
To me, technical jargon usually makes the barrier to understanding a new concept higher, so I try and avoid it.
That being said, for better or worse, you might need to know the jargon at some point, so let’s connect the concepts we just learned to the jargon.
What we just looked at was a version of OAuth2 with the Authorization Code grant. This is a really common grant type for web applications.
When we redirected the user to https://identity.google.com/get-me-a-code, that was the authorization request (and that URL can be referred to as the authorization URL).
When Joe was redirected back to our application, https://ourapp.com/handle-the-code
is called the redirect URL but it can also be called a callback URL.
When we made the request to exchange the code for a token, the URL we hit is the token URL (e.g. https://identity.google.com/token
)
Summary
OAuth2 might seem complex at first glance, but at its core, it's about securely accessing user data without handling passwords.
We've walked through a simplified flow: redirecting users to log in with a provider like Google, getting a temporary code, and exchanging that for an access token to fetch user data.
While there's certainly more to OAuth2, including different grant types for various scenarios, understanding these basics gives you a solid foundation.
And remember, many libraries can handle the nitty-gritty details, making OAuth2 integration much more manageable in practice.
Posted on September 9, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.