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!
Top comments (18)
Thanks a lot. That was very informative. Could you perhaps speak of other Oauth flows that exist in an advanced post? The flow you described is suitable for frontend requests, but what about backend requests? I saw that the specification allows that, too, with a different type of flow.
Sounds like a great article idea. I only cover topics I feel comfortable writing about, but I’ll look into it if I have a chance :)
Awesome! Thanks!
I was going to suggest the same thing!
Ah yes, this is what I need ! Can't wait for it lol
Ha - what a coincidence! I'm currently working through understanding oauth right now, through javascript. This has been helpful, especially since I couldn't figure out the refresh tokens bit and how that component work. Thank you!
Did you figure out with Javascript yet? I am trying to do the same, but there are things I certainly don't quite understand yet. If you have time, would you see if you can answer my question?
I am trying to use Passport JS Google OAuth 2 for authentication. I understand that when using Passport and upon successful login in Google (or Facebook/Twitter), it sends you back a token, user profile, etc. which contains the email address of the user among other things. Now, my Database is set up so that each user has a unique email address. What if the user decides to register for an account in my database with an email address, but decides to use Google to log in (this person has multiple email addresses)? Won’t the email address retrieved by logging into Google not match the other email address which is in my database? How do you handle that?
I haven't got into that use case. I wish I could go technical for you. Sorry.
From a consumer side: I know Patreon and some saas products i use... offer account creation and then Google Oauth. So yes, you can create a account with email, and login with oauth. If you register with oauth, they make me create an account anyways with email.
The unique key will always be the email, and a separate data field is used to store the Google oauth secret. So email/oauth data are completely separate.
Within the login form, it just needs a successful handshake from email or oauth to provide access.
It's not something I've done, and I'm only speaking from how I see it working in other situations. Best of luck!
You were able to simplify the example so much and it is very easy to follow. This can help not only for Google OAuth but as a whole because this is the flow with other libraries as well - or at least LinkedIn, Microsoft Active Directory.
simple and clear post,thanks
Very nice introduction article Risa!
Most developers are not comfortable with Oauth, it's ticky, complex and yet very powerful, I'm sure this will help.
Very helpful and just in right time for me.
Well written, thanks a lot!
Happy to hear it helped! :)
Yes indeed a really good article about OAuth. I have read some articles the past few days about the topic and this one is really easy to understand and well written.
Really good post, cheers 😁
You broke that down really well -- love the flow of the overview and the code snippets are great. Thank you!
that was more than useful, thanks!
I really appreciate comments like this - thanks!