google_sign_in 1.1.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -3
- data/Gemfile.lock +5 -5
- data/README.md +21 -3
- data/app/controllers/google_sign_in/authorizations_controller.rb +2 -0
- data/app/controllers/google_sign_in/base_controller.rb +1 -1
- data/app/controllers/google_sign_in/callbacks_controller.rb +20 -10
- data/bin/rails +1 -1
- data/google_sign_in.gemspec +1 -1
- data/lib/google_sign_in.rb +25 -0
- data/lib/google_sign_in/identity.rb +8 -0
- data/lib/google_sign_in/redirect_protector.rb +2 -2
- data/test/controllers/callbacks_controller_test.rb +107 -10
- data/test/models/identity_test.rb +8 -0
- data/test/models/redirect_protector_test.rb +6 -0
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5831caee2b3640fd7ea360cffe55c7941066ace8c22e324ed389215c0cd7c65a
|
4
|
+
data.tar.gz: 548b00fa1ad0739b039bed3ee80adfa2ee938a841737b5daef5d8eb5a5e11258
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f4d62b9a5b2a3ea7d56b643f53ce4f4ee14cf621c4ca54c68d8da546dd2a38f5844ba8516f63e33fab4d69e716791863b4075adedd2dbfcbd7e638c58faf26c
|
7
|
+
data.tar.gz: d9e6e95a67eaf3ac07fc06361ede928f9e046e04f37806e0f63d937b5ff21db2380597aa9bf438dc426053586aa51cef15e2f904b97ee0639753bcb1c818e389
|
data/.travis.yml
CHANGED
@@ -2,14 +2,14 @@ language: ruby
|
|
2
2
|
sudo: false
|
3
3
|
cache: bundler
|
4
4
|
|
5
|
-
# Bundler/RubyGems incompat on Ruby 2.5.0
|
6
|
-
before_install: gem install bundler
|
5
|
+
# Bundler/RubyGems incompat on Ruby 2.5.0 and 2.6.1
|
6
|
+
before_install: gem update --system && gem install bundler -v 1.17.3
|
7
7
|
|
8
8
|
rvm:
|
9
|
-
- 2.2
|
10
9
|
- 2.3
|
11
10
|
- 2.4
|
12
11
|
- 2.5
|
12
|
+
- 2.6
|
13
13
|
- ruby-head
|
14
14
|
|
15
15
|
matrix:
|
data/Gemfile.lock
CHANGED
@@ -80,14 +80,14 @@ GEM
|
|
80
80
|
method_source (0.9.0)
|
81
81
|
mimemagic (0.3.2)
|
82
82
|
mini_mime (1.0.1)
|
83
|
-
mini_portile2 (2.
|
83
|
+
mini_portile2 (2.4.0)
|
84
84
|
minitest (5.11.3)
|
85
85
|
multi_json (1.13.1)
|
86
86
|
multi_xml (0.6.0)
|
87
87
|
multipart-post (2.0.0)
|
88
88
|
nio4r (2.3.1)
|
89
|
-
nokogiri (1.8
|
90
|
-
mini_portile2 (~> 2.
|
89
|
+
nokogiri (1.10.8)
|
90
|
+
mini_portile2 (~> 2.4.0)
|
91
91
|
oauth2 (1.4.1)
|
92
92
|
faraday (>= 0.8, < 0.16.0)
|
93
93
|
jwt (>= 1.0, < 3.0)
|
@@ -122,7 +122,7 @@ GEM
|
|
122
122
|
method_source
|
123
123
|
rake (>= 0.8.7)
|
124
124
|
thor (>= 0.19.0, < 2.0)
|
125
|
-
rake (12.
|
125
|
+
rake (12.3.3)
|
126
126
|
safe_yaml (1.0.4)
|
127
127
|
sprockets (3.7.2)
|
128
128
|
concurrent-ruby (~> 1.0)
|
@@ -155,4 +155,4 @@ DEPENDENCIES
|
|
155
155
|
webmock (>= 3.4.2)
|
156
156
|
|
157
157
|
BUNDLED WITH
|
158
|
-
1.
|
158
|
+
1.17.2
|
data/README.md
CHANGED
@@ -64,6 +64,16 @@ end
|
|
64
64
|
|
65
65
|
**⚠️ Important:** Take care to protect your client secret from disclosure to third parties.
|
66
66
|
|
67
|
+
9. (Optional) The callback route can be configured using:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
# config/initializers/google_sign_in.rb
|
71
|
+
Rails.application.configure do
|
72
|
+
config.google_sign_in.root = "my_own/google_sign_in_route"
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
Which would make the callback `/my_own/google_sign_in_route/callback`.
|
67
77
|
|
68
78
|
## Usage
|
69
79
|
|
@@ -80,7 +90,8 @@ This gem provides a `google_sign_in_button` helper. It generates a button which
|
|
80
90
|
```
|
81
91
|
|
82
92
|
The `proceed_to` argument is required. After authenticating with Google, the gem redirects to `proceed_to`, providing
|
83
|
-
a Google ID token in `flash[:
|
93
|
+
a Google ID token in `flash[:google_sign_in][:id_token]` or an [OAuth authorizaton code grant error](https://tools.ietf.org/html/rfc6749#section-4.1.2.1)
|
94
|
+
in `flash[:google_sign_in][:error]`. Your application decides what to do with it:
|
84
95
|
|
85
96
|
```ruby
|
86
97
|
# config/routes.rb
|
@@ -108,8 +119,11 @@ class LoginsController < ApplicationController
|
|
108
119
|
|
109
120
|
private
|
110
121
|
def authenticate_with_google
|
111
|
-
if flash[:
|
112
|
-
User.find_by google_id: GoogleSignIn::Identity.new(
|
122
|
+
if id_token = flash[:google_sign_in][:id_token]
|
123
|
+
User.find_by google_id: GoogleSignIn::Identity.new(id_token).user_id
|
124
|
+
elsif error = flash[:google_sign_in][:error]
|
125
|
+
logger.error "Google authentication error: #{error}"
|
126
|
+
nil
|
113
127
|
end
|
114
128
|
end
|
115
129
|
end
|
@@ -143,6 +157,10 @@ information contained in the token via the following instance methods:
|
|
143
157
|
|
144
158
|
* `hosted_domain`: The user’s hosted G Suite domain, provided only if they belong to a G Suite.
|
145
159
|
|
160
|
+
* `given_name`: The user's given name.
|
161
|
+
|
162
|
+
* `last_name`: The user's last name.
|
163
|
+
|
146
164
|
|
147
165
|
## Security
|
148
166
|
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
|
3
3
|
class GoogleSignIn::AuthorizationsController < GoogleSignIn::BaseController
|
4
|
+
skip_forgery_protection only: :create
|
5
|
+
|
4
6
|
def create
|
5
7
|
redirect_to login_url(scope: 'openid profile email', state: state),
|
6
8
|
flash: { proceed_to: params.require(:proceed_to), state: state }
|
@@ -9,7 +9,7 @@ class GoogleSignIn::BaseController < ActionController::Base
|
|
9
9
|
GoogleSignIn.client_id,
|
10
10
|
GoogleSignIn.client_secret,
|
11
11
|
authorize_url: 'https://accounts.google.com/o/oauth2/auth',
|
12
|
-
token_url: 'https://
|
12
|
+
token_url: 'https://oauth2.googleapis.com/token',
|
13
13
|
redirect_uri: callback_url
|
14
14
|
end
|
15
15
|
end
|
@@ -2,26 +2,36 @@ require_dependency 'google_sign_in/redirect_protector'
|
|
2
2
|
|
3
3
|
class GoogleSignIn::CallbacksController < GoogleSignIn::BaseController
|
4
4
|
def show
|
5
|
-
|
6
|
-
redirect_to proceed_to_url, flash: { google_sign_in_token: id_token }
|
7
|
-
else
|
8
|
-
head :unprocessable_entity
|
9
|
-
end
|
5
|
+
redirect_to proceed_to_url, flash: { google_sign_in: google_sign_in_response }
|
10
6
|
rescue GoogleSignIn::RedirectProtector::Violation => error
|
11
7
|
logger.error error.message
|
12
8
|
head :bad_request
|
13
9
|
end
|
14
10
|
|
15
11
|
private
|
16
|
-
def valid_request?
|
17
|
-
flash[:state].present? && params.require(:state) == flash[:state]
|
18
|
-
end
|
19
|
-
|
20
12
|
def proceed_to_url
|
21
13
|
flash[:proceed_to].tap { |url| GoogleSignIn::RedirectProtector.ensure_same_origin(url, request.url) }
|
22
14
|
end
|
23
15
|
|
16
|
+
def google_sign_in_response
|
17
|
+
if valid_request? && params[:code].present?
|
18
|
+
{ id_token: id_token }
|
19
|
+
else
|
20
|
+
{ error: error_message_for(params[:error]) }
|
21
|
+
end
|
22
|
+
rescue OAuth2::Error => error
|
23
|
+
{ error: error_message_for(error.code) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def valid_request?
|
27
|
+
flash[:state].present? && params[:state] == flash[:state]
|
28
|
+
end
|
29
|
+
|
24
30
|
def id_token
|
25
|
-
client.auth_code.get_token(params
|
31
|
+
client.auth_code.get_token(params[:code])['id_token']
|
32
|
+
end
|
33
|
+
|
34
|
+
def error_message_for(error_code)
|
35
|
+
error_code.presence_in(GoogleSignIn::OAUTH2_ERRORS) || "invalid_request"
|
26
36
|
end
|
27
37
|
end
|
data/bin/rails
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
# installed from the root of your application.
|
4
4
|
|
5
5
|
ENGINE_ROOT = File.expand_path('..', __dir__)
|
6
|
-
ENGINE_PATH = File.expand_path('../lib/
|
6
|
+
ENGINE_PATH = File.expand_path('../lib/google_sign_in/engine', __dir__)
|
7
7
|
APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
|
8
8
|
|
9
9
|
# Set up gems listed in the Gemfile.
|
data/google_sign_in.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'google_sign_in'
|
3
|
-
s.version = '1.
|
3
|
+
s.version = '1.2.0'
|
4
4
|
s.authors = ['David Heinemeier Hansson', 'George Claghorn']
|
5
5
|
s.email = ['david@basecamp.com', 'george@basecamp.com']
|
6
6
|
s.summary = 'Sign in (or up) with Google for Rails applications'
|
data/lib/google_sign_in.rb
CHANGED
@@ -4,6 +4,31 @@ require 'active_support/rails'
|
|
4
4
|
module GoogleSignIn
|
5
5
|
mattr_accessor :client_id
|
6
6
|
mattr_accessor :client_secret
|
7
|
+
|
8
|
+
# https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
9
|
+
authorization_request_errors = %w[
|
10
|
+
invalid_request
|
11
|
+
unauthorized_client
|
12
|
+
access_denied
|
13
|
+
unsupported_response_type
|
14
|
+
invalid_scope
|
15
|
+
server_error
|
16
|
+
temporarily_unavailable
|
17
|
+
]
|
18
|
+
|
19
|
+
# https://tools.ietf.org/html/rfc6749#section-5.2
|
20
|
+
access_token_request_errors = %w[
|
21
|
+
invalid_request
|
22
|
+
invalid_client
|
23
|
+
invalid_grant
|
24
|
+
unauthorized_client
|
25
|
+
unsupported_grant_type
|
26
|
+
invalid_scope
|
27
|
+
]
|
28
|
+
|
29
|
+
# Authorization Code Grant errors from both authorization requests
|
30
|
+
# and access token requests.
|
31
|
+
OAUTH2_ERRORS = authorization_request_errors | access_token_request_errors
|
7
32
|
end
|
8
33
|
|
9
34
|
require 'google_sign_in/identity'
|
@@ -9,8 +9,8 @@ module GoogleSignIn
|
|
9
9
|
QUALIFIED_URL_PATTERN = /\A#{URI::DEFAULT_PARSER.make_regexp}\z/
|
10
10
|
|
11
11
|
def ensure_same_origin(target, source)
|
12
|
-
if target =~ QUALIFIED_URL_PATTERN && origin_of(target) != origin_of(source)
|
13
|
-
raise Violation, "Redirect target #{target} does not have same origin as request (expected #{origin_of(source)})"
|
12
|
+
if target.blank? || (target =~ QUALIFIED_URL_PATTERN && origin_of(target) != origin_of(source))
|
13
|
+
raise Violation, "Redirect target #{target.inspect} does not have same origin as request (expected #{origin_of(source)})"
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
@@ -5,16 +5,92 @@ class GoogleSignIn::CallbacksControllerTest < ActionDispatch::IntegrationTest
|
|
5
5
|
post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
6
6
|
assert_response :redirect
|
7
7
|
|
8
|
-
|
8
|
+
stub_token_for '4/SgCpHSVW5-Cy', access_token: 'ya29.GlwIBo', id_token: 'eyJhbGciOiJSUzI'
|
9
9
|
|
10
10
|
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
|
11
11
|
assert_redirected_to 'http://www.example.com/login'
|
12
|
-
assert_equal 'eyJhbGciOiJSUzI', flash[:
|
12
|
+
assert_equal 'eyJhbGciOiJSUzI', flash[:google_sign_in][:id_token]
|
13
|
+
assert_nil flash[:google_sign_in][:error]
|
13
14
|
end
|
14
15
|
|
15
|
-
|
16
|
+
# Authorization request errors: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
17
|
+
%w[ invalid_request unauthorized_client access_denied unsupported_response_type invalid_scope server_error temporarily_unavailable ].each do |error|
|
18
|
+
test "receiving an authorization code grant error: #{error}" do
|
19
|
+
post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
20
|
+
assert_response :redirect
|
21
|
+
|
22
|
+
get google_sign_in.callback_url(error: error, state: flash[:state])
|
23
|
+
assert_redirected_to 'http://www.example.com/login'
|
24
|
+
assert_nil flash[:google_sign_in][:id_token]
|
25
|
+
assert_equal error, flash[:google_sign_in][:error]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
test "receiving an invalid authorization error" do
|
30
|
+
post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
31
|
+
assert_response :redirect
|
32
|
+
|
33
|
+
get google_sign_in.callback_url(error: 'unknown error code', state: flash[:state])
|
34
|
+
assert_redirected_to 'http://www.example.com/login'
|
35
|
+
assert_nil flash[:google_sign_in][:id_token]
|
36
|
+
assert_equal "invalid_request", flash[:google_sign_in][:error]
|
37
|
+
end
|
38
|
+
|
39
|
+
test "receiving neither code nor error" do
|
40
|
+
post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
41
|
+
assert_response :redirect
|
42
|
+
|
43
|
+
get google_sign_in.callback_url(state: flash[:state])
|
44
|
+
assert_redirected_to 'http://www.example.com/login'
|
45
|
+
assert_nil flash[:google_sign_in][:id_token]
|
46
|
+
assert_equal 'invalid_request', flash[:google_sign_in][:error]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Access token request errors: https://tools.ietf.org/html/rfc6749#section-5.2
|
50
|
+
%w[ invalid_request invalid_client invalid_grant unauthorized_client unsupported_grant_type invalid_scope ].each do |error|
|
51
|
+
test "receiving an access token request error: #{error}" do
|
52
|
+
post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
53
|
+
assert_response :redirect
|
54
|
+
|
55
|
+
stub_token_error_for '4/SgCpHSVW5-Cy', error: error
|
56
|
+
|
57
|
+
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
|
58
|
+
assert_redirected_to 'http://www.example.com/login'
|
59
|
+
assert_nil flash[:google_sign_in][:id_token]
|
60
|
+
assert_equal error, flash[:google_sign_in][:error]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
test "protecting against CSRF without flash state" do
|
65
|
+
post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
66
|
+
assert_response :redirect
|
67
|
+
|
68
|
+
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: 'invalid')
|
69
|
+
assert_redirected_to 'http://www.example.com/login'
|
70
|
+
assert_nil flash[:google_sign_in][:id_token]
|
71
|
+
assert_equal 'invalid_request', flash[:google_sign_in][:error]
|
72
|
+
end
|
73
|
+
|
74
|
+
test "protecting against CSRF with invalid state" do
|
75
|
+
post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
76
|
+
assert_response :redirect
|
77
|
+
assert_not_nil flash[:state]
|
78
|
+
|
16
79
|
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: 'invalid')
|
17
|
-
|
80
|
+
assert_redirected_to 'http://www.example.com/login'
|
81
|
+
assert_nil flash[:google_sign_in][:id_token]
|
82
|
+
assert_equal 'invalid_request', flash[:google_sign_in][:error]
|
83
|
+
end
|
84
|
+
|
85
|
+
test "protecting against CSRF with missing state" do
|
86
|
+
post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
87
|
+
assert_response :redirect
|
88
|
+
assert_not_nil flash[:state]
|
89
|
+
|
90
|
+
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy')
|
91
|
+
assert_redirected_to 'http://www.example.com/login'
|
92
|
+
assert_nil flash[:google_sign_in][:id_token]
|
93
|
+
assert_equal 'invalid_request', flash[:google_sign_in][:error]
|
18
94
|
end
|
19
95
|
|
20
96
|
test "protecting against open redirects" do
|
@@ -25,12 +101,33 @@ class GoogleSignIn::CallbacksControllerTest < ActionDispatch::IntegrationTest
|
|
25
101
|
assert_response :bad_request
|
26
102
|
end
|
27
103
|
|
104
|
+
test "receiving no proceed_to URL" do
|
105
|
+
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: 'invalid')
|
106
|
+
assert_response :bad_request
|
107
|
+
end
|
108
|
+
|
28
109
|
private
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
110
|
+
def stub_token_for(code, **response_body)
|
111
|
+
stub_token_request(code, status: 200, response: response_body)
|
112
|
+
end
|
113
|
+
|
114
|
+
def stub_token_error_for(code, error:)
|
115
|
+
stub_token_request(code, status: 418, response: { error: error })
|
116
|
+
end
|
117
|
+
|
118
|
+
def stub_token_request(code, status:, response:)
|
119
|
+
stub_request(:post, 'https://oauth2.googleapis.com/token').with(
|
120
|
+
body: {
|
121
|
+
grant_type: 'authorization_code',
|
122
|
+
code: code,
|
123
|
+
client_id: FAKE_GOOGLE_CLIENT_ID,
|
124
|
+
client_secret: FAKE_GOOGLE_CLIENT_SECRET,
|
125
|
+
redirect_uri: 'http://www.example.com/google_sign_in/callback'
|
126
|
+
}
|
127
|
+
).to_return(
|
128
|
+
status: status,
|
129
|
+
headers: { 'Content-Type' => 'application/json' },
|
130
|
+
body: JSON.generate(response)
|
131
|
+
)
|
35
132
|
end
|
36
133
|
end
|
@@ -65,6 +65,14 @@ class GoogleSignIn::IdentityTest < ActiveSupport::TestCase
|
|
65
65
|
assert_equal "basecamp.com", GoogleSignIn::Identity.new(token_with(hd: "basecamp.com")).hosted_domain
|
66
66
|
end
|
67
67
|
|
68
|
+
test "extracting given name" do
|
69
|
+
assert_equal "George", GoogleSignIn::Identity.new(token_with(given_name: "George")).given_name
|
70
|
+
end
|
71
|
+
|
72
|
+
test "extracting family name" do
|
73
|
+
assert_equal "Claghorn", GoogleSignIn::Identity.new(token_with(family_name: "Claghorn")).family_name
|
74
|
+
end
|
75
|
+
|
68
76
|
private
|
69
77
|
def switch_client_id_to(value)
|
70
78
|
previous_value = GoogleSignIn.client_id
|
@@ -20,6 +20,12 @@ class GoogleSignIn::RedirectProtectorTest < ActiveSupport::TestCase
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
test "disallows empty URL target" do
|
24
|
+
assert_raises GoogleSignIn::RedirectProtector::Violation do
|
25
|
+
GoogleSignIn::RedirectProtector.ensure_same_origin nil, 'https://basecamp.com'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
23
29
|
test "allows URL target with same origin as source" do
|
24
30
|
assert_nothing_raised do
|
25
31
|
GoogleSignIn::RedirectProtector.ensure_same_origin 'https://basecamp.com', 'https://basecamp.com'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: google_sign_in
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-05-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -212,8 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
212
|
- !ruby/object:Gem::Version
|
213
213
|
version: '0'
|
214
214
|
requirements: []
|
215
|
-
|
216
|
-
rubygems_version: 2.7.6
|
215
|
+
rubygems_version: 3.1.2
|
217
216
|
signing_key:
|
218
217
|
specification_version: 4
|
219
218
|
summary: Sign in (or up) with Google for Rails applications
|