Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save rikikonikoff/cf7b7dccaaee044bacb6c0153f44aed8 to your computer and use it in GitHub Desktop.
Save rikikonikoff/cf7b7dccaaee044bacb6c0153f44aed8 to your computer and use it in GitHub Desktop.
Google Auth: Implementation on the Back End

Set Up

Get your environment variables; these uniquely identify your app to google.

  1. Visit https://console.developers.google.com/
  2. In the top left corner of the page, next to where it says “Google APIs”, click the dropdown menu.
  • This will open a popup screen with more options. Select intrepid.io, then click the plus sign to create a new project.
  • Pick a name for the project and click “Create”
  1. Click “Enable and manage APIs”
  • choose the APIs you want to use; Google has many, but at the very least you should enable the Google + API.
  • You can always add more APIs later by visiting your google developer console and clicking “Library” on the sidebar.
  1. On the sidebar, click “Credentials”
  • Click “Create Credentials” > “OAuth client ID"
  • Choose “Web application” and click “Create"
  • Provide an email address and product name
  1. Click “Create client ID”
  2. You should now have access to your client credentials
  • Save your Client ID and Client Secret to your .env file:
GOOGLE_CLIENT_ID="some-random-string.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="another-random-string"
  1. Click "Create Credentials > "API Key"
  • save this key to your .env file as well:
GOOGLE_API_KEY="a string"

Create migrations to set up GoogleIdentites and Users. A User should have_one GoogleIdentity, and a GoogleIdentity should belong_to a User.


Creating Service Objects for Google Auth

First, create a service object called GoogleTokenValidator.

This class should:

  1. take in a google access token (in string format - which the client will pass in)
  2. validate that it
    • came from google,
    • is current, and
    • matches our app's identity;
  3. retrieve information from google, and
  4. return true. If it gets an invalid token, it should return false without trying to retrieve any information.

Luckily, google will test whether a token was issued by them and/or is current for you. You just have to make sure your response doesn't contain the following:

{
  "error_description": "Invalid Value"
}

...that takes care of the first 2 validations.

For the 3rd validation, check the response["aud"] against a list of your app's client ids (you might have 1 for web, 1 for android, and 1 for iOS). If it matches any of them, it's valid! Yay!

If you've gotten this far, it's time to set what I call the token_hash. This is the information we want to extract from google's response and save for prosperity. It looks like this:

{ 
  google_uid: response["sub"], 
  email: response["email"], 
  name: response["name"], 
  image: response["picture"] 
}

If you were able to set that, you can go ahead and return true. If you want other information from the token's response, add or edit the hash based on what is in the response and what you want to know about the User.

Now, time for the GoogleAuthenticator, the all-important class that will actually take that information and turn it into a User and GoogleIdentity for you.

GoogleAuthenticator takes in that same token string it passes into the GoogleTokenValidator. The first thing it does is run that validator and make sure it returns true. If not, the User is unauthorized and it raises an error.

Next, it looks for an existing GoogleIdentity with the google_uid it just got from the token_hash. If one exists, it will update the attributes to match what google knows about this person. It will also update the name of the User that this GoogleIdentity belongs to.

If no GoogleIdentity exists yet with that google_uid, the GoogleAuthenticator will create a new one and a corresponding User based on this information from the token_hash.

GoogleAuthenticator returns an array containing the relevant User and GoogleIdentity objects (provided it was passed a valid token and the GoogleTokenValidator thus returned true and set the token_hash appropriately).

Run it by calling GoogleAuthenticator.perform("valid_google_token").

In order to test the GoogleAuthenticator, you will have to stub the GoogleTokenValidator. You can do this as follows:

google_profile_info = JSON.parse some_fixture_of_fake_token_hash.json
user_info = {
	email: google_profile_info["email"],
	name: google_profile_info["name"],
	google_uid: google_profile_info["sub"],
	image: google_profile_info["picture"]
}

GoogleAuthenticator.any_instance.stub(:token_valid?).and_return(true)
GoogleAuthenticator.any_instance.stub(:token_hash).and_return(user_info)

Creating Server-Specific Access Tokens

In order to make google auth useful, you'll need to give the user an access token for your app once they've signed in; otherwise, authenticating with google auth didn't authorize anything.

You'll need 2 more service objects to get this done: an Encoder and a Decoder.

First, add gem "warden" to your Gemfile and run bundle install. You now have access to JWT methods and session information. Congratulations!

Now, create your Encoder: it will take in a User and an optional expiration datetime, and return an encoded token. The key logic looks like this:

  JWT.encode { "sub": user.id, "exp": expiration_datetime.to_i }, Rails.application.secrets.secret_key_base, "HS256"

Your decoder should do the opposite:

JWT.decode(your_access_token, Rails.application.secrets.secret_key_base, "HS256").first

Returning a Server-Specific Access Token

Inside app/controllers/api/v1/, make an auths_controller.rb:

class Api::V1::AuthsController < ApiController (or whatever your controllers inherit from)
  def create
      user, google_identity = GoogleAuthenticator.perform(auth_params[:token])
      render json: user, status: :created, serializer: AuthSerializer
  end

  private

  def auth_params
      params.require(:auth).permit(:token)
  end
end

This basically means that when a client sends appropriate authentication information to the backend, the server will sign in the user and create or update the User and GoogleIdentity belonging to that user.

Now, you obviously need an AuthSerializer. Run rails g serializer Auth. In app/serializers/auth_serializer.rb, add the following:

attributes :access_token, :user

has_one :user

def user
  object
end

def access_token
  EncodeJwt.perform(user: user)
end

def root
  "auth" //or whatever you want to call the param you send back and forth with the client side
end

This ensures that the server will understand a request and the response will display properly.


Putting it all to Use

Now that you've got all your service objects and such in place, you want to make sure users send you access_tokens when they try to view various parts of your app.

This can be more or less complicated, depending on whether you care about scope. For this example, all authentication is just an issue of "yes" or "no".

Create a TokenAuthenticationStrategy that inherits from ::Warden::Strategies::Base. Inside it, define a method called authenticate! that will user Warden methods to verify that the decoded token is valid.

Next, make a unique constraint in app/constraints/authenticated_constraint.rb:

class AuthenticatedConstraint
  def matches?(request)
    warden = request.env["warden"]
    warden && warden.authenticate!(:token_authentication_strategy)
  end

  Warden::Strategies.add(:token_authentication_strategy, TokenAuthenticationStrategy)
end

Now, update your config/routes.rb file to reflect the need for authorization:

Rails.application.routes.draw do
  resources :auths, only: [:create]

  constraints AuthenticatedConstraint.new do
    resources :whatever-your-resources-are
  end
end

...And you're good to go! You can now assume that in order to view any part of your application besides the sign-in page, a user must be authenticated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment