Beginner’s guide to OAuth: Understanding access tokens and authorization codes using Google API
Risa Fujii
Posted on August 16, 2019
As a user, it’s easy and convenient to use your Google account (or Facebook, Twitter, etc.) to sign into other services.
- You click the "Sign in with Google" button
- You get redirected to a consent screen
- You click "Allow"
- The page redirects to the actual application
But there's actually a lot going on under the hood, and it can be useful as a developer to understand it. When I implemented OAuth in Rails for the first time, I was using an external library (the sorcery
gem’s external module), which made it easy for me to gloss over the process. Yet as soon as I wanted to customize something, I realized it was necessary to have a better understanding of it. I decided to re-implement the flow without the help of any authentication helper gems.
Based on what I learned, I wrote this basic explanation of the OAuth flow and what authorization codes and access/refresh tokens are. I’m using Google as the OAuth provider.
This post is meant for people who are not very familiar with OAuth. In other words, if you already know how it works and can understand Google's OAuth guide, this post may be too elementary for you.
What are authorization codes and access tokens?
In the OAuth flow, your app needs to send two requests to Google. The first request is to get an authorization code, the second is to get an access token. They both take the form of long strings, but have different purposes.
This kind of similar terminology can be tricky at first, so let's first briefly cover what they are.
I bet this screen looks familiar. The authorization code is a code that Google sends back to your app once the user consents on this screen.
This code can be used to get an access token. Once you've received the authorization code, you put it in the params and send a second request to Google, essentially saying "Give me an access token so I can send requests on behalf of this user?"
Google's response to that should include an access token. By putting this token in your request headers, you can do things like create a new event in the user's Google Calendar or access a user’s Gmail.
Key things to note:
Authorization Code
- Only valid for one-time use, since its only usage is to exchange it for an access token
- Expires very quickly (according to this article, the OAuth protocol's recommended maximum is 10 minutes, and many services' authorization codes expire even earlier)
Access Token
- Can be obtained using the authorization code
- Put in the headers of any API requests to Google on behalf of the user
- Expires after one hour (the expiration time may vary if you're using something besides Google)
What do you do after an hour when the access token expires? Do you have to make the user re-login to get a new one?
No. You can get something called a refresh token, which allows you to get new access tokens. More on this in the last section of this article.
OAuth step-by-step
Let's go over the basic OAuth flow again, except this time, we're looking at it from the developer's point of view, not the user. I've included Ruby code snippets as well.
Note: I am skipping over the very first configuration you have to do, which is to create your client_id
and client_secret
in the Google API Console. This guide walks you through the steps.
Step 1. User clicks the "Sign In With Google" button in your app
Step 2. Redirect to the Google consent screen
In my case, clicking the button calls the oauths_controller
's oauth
method, which then redirects the page.
# oauths_controller.rb
def oauth
args = {
client_id: ENV['GOOGLE_CLIENT_ID'],
response_type: 'code',
scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/calendar',
redirect_uri: 'http://my-app.com/oauth/callback?provider=google',
access_type: 'offline'
}
redirect_to 'https://accounts.google.com/o/oauth2/v2/auth?' + args.to_query
end
A quick explanation of the query parameters:
-
client_id
is the one you created in the Google API Console. I’ve just stored it in an environment variable. -
response_type: 'code'
signals that you'd like an authorization code for obtaining an access token. -
scope
defines what kinds of permissions you need. I needed access to the user's Google Calendar in addition to the user's name and email address, which is why I have the three scopes above. The full list of scopes can be found in the docs. -
redirect_uri
is the URI that Google redirects to once the user hits "Allow". You can't put any random URI here; it needs to match one of the URIs you added in the Google API Console. -
access_type: 'offline'
has to do with the refresh tokens I mentioned above, and we will cover this later.
Step 3. The user clicks "Allow" on the consent screen
Step 4. The page redirects to your callback_uri
Google handles this part. In my case, the oauths_controller
's callback
method gets called.
# routes.rb
get 'oauth/callback', to: 'oauths#callback'
The parameters of this incoming request from Google includes an authorization code (in params[:code]
).
Step 5. Exchange the authorization code for an access token
Next, you need to make an HTTP POST request to Google's token endpoint (/oauth2/v4/token
) to get an access token in exchange for the authorization code you just received.
Note: I’m using the HTTParty
gem to make HTTP requests, but of course this isn’t mandatory.
# oauths_controller.rb
def callback
# Exchange the authorization code for an access token (step 5)
query = {
code: params[:code],
client_id: ENV['GOOGLE_CLIENT_ID'],
client_secret: ENV['GOOGLE_CLIENT_SECRET'],
redirect_uri: 'http://my-app.com/oauth/callback?provider=google',
grant_type: 'authorization_code'
}
response = HTTParty.post('https://www.googleapis.com/oauth2/v4/token', query: query)
# Save the access token (step 6)
session[:access_token] = response['access_token']
end
As for the params in this POST request, this article provides a good explanation if you're interested.
Step 6. Save the access token
As seen in the final part of the snippet above, I'm saving the returned access_token
in the session. Now, whenever I want to make a request to the Google API on behalf of the user, I can do so using this token.
For example, to get a list of the user's Google Calendar events:
headers = {
'Content-Type': 'application/json',
'Authorization': "Bearer #{session[:access_token]}"
}
HTTParty.get(
'https://www.googleapis.com/calendar/v3/calendars/primary/events',
headers: headers
)
There we go! Now we've successfully implemented the OAuth flow using authorization tokens.
Use refresh tokens to get new access tokens
As mentioned above, access tokens expire after a certain amount of time (e.g. 1 hour). If your app's login also expires at the same time or earlier, you have nothing to worry about - the user would have to re-login anyway.
But what if your app allowed users to be logged in for longer (after all, in many cases, being kicked out of an app after an hour could be obnoxious)? Google's access token would still expire, so any requests to the Google API would be rejected.
That’s where refresh tokens come in. You can get one in the same response as the one that returns an access token (Step 5), as long as you specified access_type: 'offline'
in the initial redirect (Step 2).
Unlike access tokens, refresh tokens have no set expiration time. If your access token has expired, you can get a new one using a refresh token with an HTTP POST request like below:
query = {
'client_id': ENV['GOOGLE_CLIENT_ID'],
'client_secret': ENV['GOOGLE_CLIENT_SECRET'],
# Assuming we've saved the refresh_token in the DB along with the user info
'refresh_token': current_user.refresh_token,
'grant_type': 'refresh_token',
}
response = HTTParty.post(
'https://www.googleapis.com/oauth2/v4/token',
query: query
)
session[:access_token] = response['access_token']
Refresh tokens gotchas
1. You should reuse them
You’re generally expected to keep using the same refresh token each time you request a new access token. For this reason, Google only gives you a refresh token the first time the user consents and logs in to your app (docs on offline access):
This value instructs the Google authorization server to return a refresh token and an access token the first time that your application exchanges an authorization code for tokens.
This means that it’s important to store refresh tokens in long-term storage, like the DB.
Note: If you really need to get a new refresh token every time the user logs in, you can add prompt=consent
to the parameters in the authorization request in Step 2. This will require the user to consent every time they log in, but the response will always include a new refresh token.
2. They can become invalid too
While refresh tokens don’t expire after a set amount of time, they can become invalid in some cases (docs):
You must write your code to anticipate the possibility that a granted refresh token might no longer work. A refresh token might stop working for one of these reasons:
- The user has revoked your app's access.
- The refresh token has not been used for six months.
- The user changed passwords and the refresh token contains Gmail scopes.
- The user account has exceeded a maximum number of granted (live) refresh tokens. There is currently a limit of 50 refresh tokens per user account per client. If the limit is reached, creating a new refresh token automatically invalidates the oldest refresh token without warning.
For example, if the user revoked your app’s access, any requests to obtain a new access token using the existing refresh token would cease to work. In such cases, you would need to get the user to log out and re-login in order to get a new refresh token.
In this post, I handled everything manually without relying on gems like devise
, sorcery
or omniauth-google-oauth2
. I'm not saying that's the best approach here; realistically, it might be easier to use those gems. The goal of this post was to walk through what is actually happening, in order not to blindly let the gems handle everything. Thanks for reading!
Posted on August 16, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.