google_sign_in 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|