Set Up
Get your environment variables; these uniquely identify your app to google.
- Visit https://console.developers.google.com/
- 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”
- 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.
- On the sidebar, click “Credentials”
- Click “Create Credentials” > “OAuth client ID"
- Choose “Web application” and click “Create"
- Provide an email address and product name
- Click “Create client ID”
- 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"
- 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:
- take in a google access token (in string format - which the client will pass in)
- validate that it
- came from google,
- is current, and
- matches our app's identity;
- retrieve information from google, and
- return
true
. If it gets an invalid token, it should returnfalse
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.