Although it may not look so at a glance, authorizing access to a Google service using Ruby is rather simple. In this post I'd like to take a detailed look at what needs to be done to authorize access to a Google service using OAuth2 protocol.

The legend

A friend of mine owns a neat collection of playlists on YouTube. I want to create a tiny web app which, with a permission from him, would access those playlists and, maybe some other data on his YouTube account.

I am not going to walk through the process of making a complete web app here, but instead focus on the authorization part, including making a first API call.


Before proceeding, two things must be done on Google Cloud:

  1. create a project,
  2. setup project's credentials.

Both steps are already well-described on this page, so I'm not going to dive into it much here. Upon completing it, we will receive the following three strings:

  3. REDIRECT_URI (or a list of redirect URIs).

Since I don't have an app running online just yet (after all, all I'm doing is hacking to check if I'm capable of getting the needed data), I set the list of redirection URIs to only one URI – http://localhost. We'll revisit this once again on "Step 2" below.


Google providers a great authorization gem, called signet, which significantly simplifies the process of dealing with authorization logic.

Authorization using OAuth2 consists of 3 steps:

  1. generating AUTHORIZATION_URI,
  2. obtaining AUTHORIZATION_CODE by visiting the AUTHORIZATION_URI,

Let's take a look on how each step is done in Ruby.

1. Generating an AUTHORIZATION_URI:

require 'signet/oauth_2/client'

auth =
  authorization_uri: '',
  scope:             '',
  state:             'useful_dynamic_string',
  redirect_uri:      REDIRECT_URI,
  client_id:         CLIENT_ID,
  client_secret:     CLIENT_SECRET

puts auth.authorization_uri

This should output a URI as such:

Before we proceed, let's take a look at what useful_dynamic_string actually used for.

For the moment, let's pretend that no { state: 'useful_dynamic_string' } parameter is being passed to Signet::OAuth2::Client object constructor. I am as a developer, is the only user of the application, therefore using a simple http://localhost/?code=... URI absolutely satisfies my needs. In a real-world application though, there's a need to distinguish a user who lands on such URL. Here's where the state parameter comes in handy.

Let's say that after pushing the app to a closed beta, I've given the the AUTHORIZATION_URI to the 3 of my friends, a team of early adopters. All of them visited the link and got redirected to my app, each with his own AUTHORIZATION_CODE:


Seeing these three, how do I distinguish which users in my app they belong to? It's not possible, unless I include some kind of URI parameter, that'll travel first to the user user concent screen on Google, and then back to my app on

When it comes to building a real-world app, I guess I'd use either an id of my user to pass along, or an email. Using email as a value for state parameter, the redirection would end up on these three pages, respectively:


Now that I've got a parameter back, I can easily tell which user I should proceed the next step with.


Visiting the above URI and granting the access will result in a redirection to an address, that looks as such:


The value of code URI param is the AUTHORIZATION_CODE we're looking for.

Obtaining the code is a no-brainer when testing on a local machine. When it comes to real-world apps though, a few additional notes must be taken into consideration.

In the example above, the URI is localhost only due to the fact I'm hacking here, i.e. trying to establish whether something will or will not work as I expect it to. For a real world, publicly accessible app, running a staging or production environment, the REDIRECT_URI obviously must be a real URI.

For example, a real URI may look like or for production and staging environments respectively. Both such redirect URLs have to be included in a "Redirect URIs" list when setting up an application on Google Cloud (see "Prerequisites" section above).

3. Obtaining a REFRESH_TOKEN:

require 'signet/oauth_2/client'

auth =
  token_credential_uri: '',
  redirect_uri:         REDIRECT_URI,
  client_id:            CLIENT_ID,
  client_secret:        CLIENT_SECRET,
  code:                 AUTHORIZATION_CODE

puts auth.fetch_access_token!

This will result in an output as such:

  "access_token" => "ya29.1.AADtN_Us0-Q9oRmYIBl-0eYiqThLUxczhLk5lbNskf4nv7baj_2x1v6Oc0ptW3V18RYzZg",
  "token_type"   => "Bearer",
  "expires_in"   => 3600,
  "refresh_token"=> "1/eVluU7xcmEoHxUz4Fmk6yMQGPMaLKBpwJ4eqDxDcKw8"

The value of refresh_token key is the REFRESH_TOKEN we're looking for. Thanks to the way signet is implemented, from now on given REFRESH_TOKEN is the only token required to authorize access to a respective client's data. In other words, refresh_token has to be persisted.

It is worth mentioning that not only #fetch_access_token! returns a hash with refresh_token key, but it also activates the auth object. By activating I mean issuing a new access_token from Google, which will be valid for the next 3600 seconds. Luckily, there's no need to care about them.

As long as we keep the REFRESH_TOKEN, calling #fetch_access_token! will authorize the Signet::OAuth2::Client instance for doing API interaction on behalf of a given client.

Making an API call

Now all that's left is to initialize the Signet::OAuth2::Client instance using the REFRESH_TOKEN, and proceed with making an API call.

This step, being a final, requires google-api-client gem to be installed.

require 'google/api_client'
require 'signet/oauth_2/client'

auth =
  token_credential_uri: '',
  client_id:            CLIENT_ID,
  client_secret:        CLIENT_SECRET,
  refresh_token:        REFRESH_TOKEN


api_client =
  application_name:    'Playlists fetcher',
  application_version: '1.0.0'

api_client.authorization = auth

youtube = api_client.discovered_api('youtube', 'v3')

response_data = api_client.execute(
  api_method: youtube.playlists.list,
  parameters: {
    part: 'id',
    mine: true

puts response_data.inspect