Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the server-side OAuth 2.0 flow #13

Merged
merged 5 commits into from
Aug 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.bundle/
.byebug_history
log/*.log
pkg/

test/dummy/db/*.sqlite3
test/dummy/db/*.sqlite3-journal
test/dummy/log/*.log
test/dummy/node_modules/
test/dummy/yarn-error.log
test/dummy/storage/
test/dummy/tmp/
124 changes: 121 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,146 @@ PATH
remote: .
specs:
google_sign_in (0.1.4)
activesupport (>= 5.1)
google-id-token (>= 1.4.0)
oauth2 (>= 1.4.0)
rails (>= 5.1.0)

GEM
remote: https://rubygems.org/
specs:
activesupport (5.2.1)
actioncable (5.2.0)
actionpack (= 5.2.0)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.0)
actionpack (= 5.2.0)
actionview (= 5.2.0)
activejob (= 5.2.0)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.0)
actionview (= 5.2.0)
activesupport (= 5.2.0)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.0)
activesupport (= 5.2.0)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.2.0)
activesupport (= 5.2.0)
globalid (>= 0.3.6)
activemodel (5.2.0)
activesupport (= 5.2.0)
activerecord (5.2.0)
activemodel (= 5.2.0)
activesupport (= 5.2.0)
arel (>= 9.0)
activestorage (5.2.0)
actionpack (= 5.2.0)
activerecord (= 5.2.0)
marcel (~> 0.3.1)
activesupport (5.2.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
arel (9.0.0)
builder (3.2.3)
byebug (9.1.0)
concurrent-ruby (1.0.5)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
erubi (1.7.1)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
globalid (0.4.1)
activesupport (>= 4.2.0)
google-id-token (1.4.2)
jwt (>= 1)
hashdiff (0.3.7)
i18n (1.1.0)
concurrent-ruby (~> 1.0)
jwt (2.1.0)
jwt (1.5.6)
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.0)
mini_mime (>= 0.1.1)
marcel (0.3.2)
mimemagic (~> 0.3.2)
method_source (0.9.0)
mimemagic (0.3.2)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.11.3)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
nio4r (2.3.1)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
oauth2 (1.4.0)
faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
public_suffix (3.0.3)
rack (2.0.5)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.0)
actioncable (= 5.2.0)
actionmailer (= 5.2.0)
actionpack (= 5.2.0)
actionview (= 5.2.0)
activejob (= 5.2.0)
activemodel (= 5.2.0)
activerecord (= 5.2.0)
activestorage (= 5.2.0)
activesupport (= 5.2.0)
bundler (>= 1.3.0)
railties (= 5.2.0)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
railties (5.2.0)
actionpack (= 5.2.0)
activesupport (= 5.2.0)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rake (12.0.0)
safe_yaml (1.0.4)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
thor (0.20.0)
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
webmock (3.4.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)

PLATFORMS
ruby
Expand All @@ -34,6 +151,7 @@ DEPENDENCIES
byebug
google_sign_in!
rake
webmock

BUNDLED WITH
1.16.3
159 changes: 112 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,84 +1,149 @@
# Google Sign-In for Rails

Google Sign-In provides an easy and secure way to let users signin into and up for your service,
without adding yet-another per-app email/password combination. Integrating it into your Rails app
should be drop-in easy. This gem makes it so.
This gem allows you to add Google sign-in to your Rails app. You can let users sign up for and sign in to your service
with their Google accounts.

The only configuration needed is setting the Google client id for your application. [Google has a
tutorial on how to setup a client id](https://developers.google.com/identity/sign-in/web/server-side-flow#step_1_create_a_client_id_and_client_secret).

Once you have your client id, create a `config/initializers/google_sign_in_client_id.rb` file with this:
`GoogleSignIn::Identity.client_id = <THAT CLIENT ID YOU GOT FROM GOOGLE>`
## Installation

Now you can use the sign-in integration on your signup or signin screen.
Add `google_sign_in` to your Rails app’s Gemfile and run `bundle install`:

## Example
```ruby
gem 'google_sign_in'
```


## Configuration

First, set up an OAuth 2.0 Client ID in the Google API Console:

1. Go to the [API Console](https://console.developers.google.com/apis/credentials).

2. In the projects menu at the top of the page, ensure the correct project is selected or create a new one.

3. In the left-side navigation menu, choose APIs & Services → Credentials.

4. Click the button labeled “Create credentials.” In the menu that appears, choose to create an **OAuth client ID**.

5. When prompted to select an application type, select **Web application**.

6. Enter your application’s name.

Here's the most basic example:
7. This gem adds a single OAuth callback to your app at `/google_sign_in/callback`. Under **Authorized redirect URIs**,
add that callback for your application’s domain: for example, `https://example.com/google_sign_in/callback`.

To use Google sign-in in development, you’ll need to add another redirect URI for your local environment, like
`http://localhost:3000/google_sign_in/callback`. For security reasons, we recommend using a separate
client ID for local development. Repeat these instructions to set up a new client ID for development.

8. Click the button labeled “Create.” You’ll be presented with a client ID and client secret. Save these.

With your client ID set up, configure your Rails application. In a new initializer, provide the client ID and client secret:

```ruby
# config/initializers/google_sign_in.rb
Rails.application.configure do
config.google_sign_in.client_id = '...'
config.google_sign_in.client_secret = '...'
end
```

**⚠️ Important:** Take care to protect your client secret. Consider storing your Google client ID and client secret in
your Rails application’s [encrypted credentials file](https://guides.rubyonrails.org/security.html#custom-credentials):

```yaml
# rails credentials:edit
google_sign_in_client_id: ...
google_sign_in_client_secret: ...
```

```ruby
# app/views/layouts/application.html.erb
<html>
<head>
<% # Required for google_sign_in to add the Google JS assets and meta tags! %>
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>

# app/views/sessions/new.html.erb
<%= google_sign_in(url: session_path) do %>
# You can replace this with whatever design you please for the button.
# You should follow Google's brand guidelines for Google Sign-In, though:
# https://developers.google.com/identity/branding-guidelines
<%= button_tag("Signin with Google") %>
# config/initializers/google_sign_in.rb
Rails.application.configure do
config.google_sign_in.client_id = Rails.application.credentials.google_sign_in_client_id
config.google_sign_in.client_secret = Rails.application.credentials.google_sign_in_client_secret
end
```

Alternatively, provide the client ID and client secret using ENV variables:

```ruby
# config/initializers/google_sign_in.rb
Rails.application.configure do
config.google_sign_in.client_id = ENV['google_sign_in_client_id']
config.google_sign_in.client_secret = ENV['google_sign_in_client_secret']
end
```

## Usage

This gem provides a `google_sign_in_button` helper. It generates a button which initiates Google sign-in:

```erb
<%= google_sign_in_button 'Sign in with my Google account', proceed_to: create_login_url %>

<%= google_sign_in_button image_tag('google_logo.png', alt: 'Google'), proceed_to: create_login_url %>

<%= google_sign_in_button proceed_to: create_login_url do %>
Sign in with my <%= image_tag('google_logo.png', alt: 'Google') %> account
<% end %>
```

The `url` option is the URL that the hidden form will be submitted against along with the Google ID Token
that's set after the user has picked the account and authenticated in the pop-up window Google provides.
The `proceed_to` argument is required. After authenticating with Google, the gem redirects to `proceed_to`, providing
a Google ID token in `flash[:google_sign_in_token]`. Your application decides what to do with it:

You can then use that in a sessions controller like so:
```ruby
# config/routes.rb
Rails.application.routes.draw do
# ...
get 'login', to: 'logins#new'
get 'login/create', to: 'logins#create', as: :create_login
end
```

```ruby
class SessionsController < ApplicationController
# app/controllers/logins_controller.rb
class LoginsController < ApplicationController
def new
end

def create
if user = authenticate_via_google
if user = authenticate_with_google
cookies.signed[:user_id] = user.id
redirect_to user
else
redirect_to new_session_url, alert: "authentication_failed"
redirect_to new_session_url, alert: 'authentication_failed'
end
end

private
def authenticate_via_google
if params[:google_id_token].present?
User.find_by google_id: GoogleSignIn::Identity.new(params[:google_id_token]).user_id
def authenticate_with_google
if flash[:google_sign_in_token].present?
User.find_by google_id: GoogleSignIn::Identity.new(flash[:google_sign_in_token]).user_id
end
end
end
```

(This example assumes that a user has already signed up for your service using Google Sign-In and that
you're storing the Google user id in the `User#google_id` attribute).
(The above example assumes the user has already signed up for your service and that you’re storing their Google user ID
in the `User#google_id` attribute.)

The `GoogleSignIn::Identity` class decodes and verifies the integrity of a Google ID token. It exposes the profile
information contained in the token via the following instance methods:

* `name`

* `email_address`

* `user_id`: A value that uniquely identifies a single Google user. Use this, not `email_address`, to associate a
Google user with an application user. A Google user’s email address may change, but their `user_id` will remain constant.

* `email_verified?`

That's it! You can checkout the `GoogleSignIn::Identity` class for the thin wrapping it provides around
the decoding of the Google ID Token using the google-id-token library. Interrogating this identity object
for profile details is particularly helpful when you use Google for signup, as you can get the name, email
address, avatar url, and locale through it.
* `avatar_url`

## Compatibility with Turbolinks
* `locale`

Google's JavaScript doesn't play nice with Turbolinks. We've routed around the damage by adding a [Turbolinks
meta tag](https://github.com/turbolinks/turbolinks/blob/master/README.md#ensuring-specific-pages-trigger-a-full-reload)
on whatever page `google_sign_in` is called to always do a full reload for that page. Note that this
auto-compatibility feature requires Turbolinks 5.1+.

## License

Expand Down
3 changes: 2 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ require "rake/testtask"

Rake::TestTask.new do |test|
test.libs << "test"
test.test_files = FileList["test/*_test.rb"]
test.test_files = FileList["test/**/*_test.rb"]
test.warning = false
end

task default: :test
17 changes: 17 additions & 0 deletions app/controllers/google_sign_in/authorizations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'securerandom'

class GoogleSignIn::AuthorizationsController < GoogleSignIn::BaseController
def create
redirect_to login_url(scope: 'openid profile email', state: state),
flash: { proceed_to: params.require(:proceed_to), state: state }
end

private
def login_url(**params)
client.auth_code.authorize_url(prompt: 'login', **params)
end

def state
@state ||= SecureRandom.base64(16)
end
end
Loading