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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42c961227f0c8530cf3597830303b0e4446ade4eb43fbc4ed09cc0c82b38bd0e
4
- data.tar.gz: c5294b87b1b5889c89f03e59f36ea38f8e40c3c33bbe796c31420e5729e470c5
3
+ metadata.gz: 5831caee2b3640fd7ea360cffe55c7941066ace8c22e324ed389215c0cd7c65a
4
+ data.tar.gz: 548b00fa1ad0739b039bed3ee80adfa2ee938a841737b5daef5d8eb5a5e11258
5
5
  SHA512:
6
- metadata.gz: 3a4bf572cb71ef2cb8b3b521ecf22d225dedc61f4da5abf6cc37a4aefb6e96b5d8a5f9879b06c3b7cf84de9b17ed786509ab70a816cbc60454dd3a8478c67161
7
- data.tar.gz: 6d1a42d8b65e52eb51664fe409bd8049ad02dad27ebebeb8c9f22cfa247cbbc52ed12aed4b05e08c9645410098513de152a2f33767f8b45dbe2c789da3e30ffc
6
+ metadata.gz: 5f4d62b9a5b2a3ea7d56b643f53ce4f4ee14cf621c4ca54c68d8da546dd2a38f5844ba8516f63e33fab4d69e716791863b4075adedd2dbfcbd7e638c58faf26c
7
+ data.tar.gz: d9e6e95a67eaf3ac07fc06361ede928f9e046e04f37806e0f63d937b5ff21db2380597aa9bf438dc426053586aa51cef15e2f904b97ee0639753bcb1c818e389
@@ -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:
@@ -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.3.0)
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.5)
90
- mini_portile2 (~> 2.3.0)
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.0.0)
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.16.6
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[:google_sign_in_token]`. Your application decides what to do with it:
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[:google_sign_in_token].present?
112
- User.find_by google_id: GoogleSignIn::Identity.new(flash[:google_sign_in_token]).user_id
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://www.googleapis.com/oauth2/v4/token',
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
- if valid_request?
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.require(:code))['id_token']
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/blorgh/engine', __dir__)
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.
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'google_sign_in'
3
- s.version = '1.1.2'
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'
@@ -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'
@@ -40,6 +40,14 @@ module GoogleSignIn
40
40
  @payload["hd"]
41
41
  end
42
42
 
43
+ def given_name
44
+ @payload["given_name"]
45
+ end
46
+
47
+ def family_name
48
+ @payload["family_name"]
49
+ end
50
+
43
51
  private
44
52
  delegate :client_id, to: GoogleSignIn
45
53
 
@@ -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
- stub_token_request code: '4/SgCpHSVW5-Cy', access_token: 'ya29.GlwIBo', id_token: 'eyJhbGciOiJSUzI'
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[:google_sign_in_token]
12
+ assert_equal 'eyJhbGciOiJSUzI', flash[:google_sign_in][:id_token]
13
+ assert_nil flash[:google_sign_in][:error]
13
14
  end
14
15
 
15
- test "protecting against CSRF" do
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
- assert_response :unprocessable_entity
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 stub_token_request(code:, **params)
30
- stub_request(:post, 'https://www.googleapis.com/oauth2/v4/token').
31
- with(body: { grant_type: 'authorization_code', code: code,
32
- client_id: FAKE_GOOGLE_CLIENT_ID, client_secret: FAKE_GOOGLE_CLIENT_SECRET,
33
- redirect_uri: 'http://www.example.com/google_sign_in/callback' }).
34
- to_return(status: 200, headers: { 'Content-Type' => 'application/json' }, body: JSON.generate(params))
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.1.2
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: 2018-10-22 00:00:00.000000000 Z
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
- rubyforge_project:
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