-
-
Notifications
You must be signed in to change notification settings - Fork 416
/
google_oauth2.rb
226 lines (184 loc) · 8.13 KB
/
google_oauth2.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# frozen_string_literal: true
require 'jwt'
require 'omniauth/strategies/oauth2'
require 'uri'
module OmniAuth
module Strategies
# Main class for Google OAuth2 strategy.
class GoogleOauth2 < OmniAuth::Strategies::OAuth2
ALLOWED_ISSUERS = ['accounts.google.com', 'https://accounts.google.com'].freeze
BASE_SCOPE_URL = 'https://www.googleapis.com/auth/'
BASE_SCOPES = %w[profile email openid].freeze
DEFAULT_SCOPE = 'email,profile'
USER_INFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
option :name, 'google_oauth2'
option :skip_friends, true
option :skip_image_info, true
option :skip_jwt, false
option :jwt_leeway, 60
option :authorize_options, %i[access_type hd login_hint prompt request_visible_actions scope state redirect_uri include_granted_scopes openid_realm device_id device_name]
option :authorized_client_ids, []
option :client_options,
site: 'https://oauth2.googleapis.com',
authorize_url: 'https://accounts.google.com/o/oauth2/auth',
token_url: '/token'
def authorize_params
super.tap do |params|
options[:authorize_options].each do |k|
params[k] = request.params[k.to_s] unless [nil, ''].include?(request.params[k.to_s])
end
params[:scope] = get_scope(params)
params[:access_type] = 'offline' if params[:access_type].nil?
params['openid.realm'] = params.delete(:openid_realm) unless params[:openid_realm].nil?
session['omniauth.state'] = params[:state] if params[:state]
end
end
uid { raw_info['sub'] }
info do
prune!(
name: raw_info['name'],
email: verified_email,
unverified_email: raw_info['email'],
email_verified: raw_info['email_verified'],
first_name: raw_info['given_name'],
last_name: raw_info['family_name'],
image: image_url,
urls: {
google: raw_info['profile']
}
)
end
extra do
hash = {}
hash[:id_token] = access_token['id_token']
if !options[:skip_jwt] && !access_token['id_token'].nil?
decoded = ::JWT.decode(access_token['id_token'], nil, false).first
# We have to manually verify the claims because the third parameter to
# JWT.decode is false since no verification key is provided.
::JWT::Verify.verify_claims(decoded,
verify_iss: true,
iss: ALLOWED_ISSUERS,
verify_aud: true,
aud: options.client_id,
verify_sub: false,
verify_expiration: true,
verify_not_before: true,
verify_iat: true,
verify_jti: false,
leeway: options[:jwt_leeway])
hash[:id_info] = decoded
end
hash[:raw_info] = raw_info unless skip_info?
prune! hash
end
def raw_info
@raw_info ||= access_token.get(USER_INFO_URL).parsed
end
def custom_build_access_token
access_token = get_access_token(request)
verify_hd(access_token)
access_token
end
alias build_access_token custom_build_access_token
private
def callback_url
options[:redirect_uri] || (full_host + script_name + callback_path)
end
def get_access_token(request)
if request.xhr? && request.params['code']
verifier = request.params['code']
redirect_uri = request.params['redirect_uri'] || 'postmessage'
client.auth_code.get_token(verifier, get_token_options(redirect_uri), deep_symbolize(options.auth_token_params || {}))
elsif request.params['code'] && request.params['redirect_uri']
verifier = request.params['code']
redirect_uri = request.params['redirect_uri']
client.auth_code.get_token(verifier, get_token_options(redirect_uri), deep_symbolize(options.auth_token_params || {}))
elsif verify_token(request.params['access_token'])
::OAuth2::AccessToken.from_hash(client, request.params.dup)
elsif request.content_type =~ /json/i
begin
body = JSON.parse(request.body.read)
request.body.rewind # rewind request body for downstream middlewares
verifier = body && body['code']
if verifier
redirect_uri = 'postmessage'
client.auth_code.get_token(verifier, get_token_options(redirect_uri), deep_symbolize(options.auth_token_params || {}))
end
rescue JSON::ParserError => e
warn "[omniauth google-oauth2] JSON parse error=#{e}"
end
else
verifier = request.params['code']
client.auth_code.get_token(verifier, get_token_options(callback_url), deep_symbolize(options.auth_token_params))
end
end
def get_scope(params)
raw_scope = params[:scope] || DEFAULT_SCOPE
scope_list = raw_scope.split(' ').map { |item| item.split(',') }.flatten
scope_list.map! { |s| s =~ %r{^https?://} || BASE_SCOPES.include?(s) ? s : "#{BASE_SCOPE_URL}#{s}" }
scope_list.join(' ')
end
def verified_email
raw_info['email_verified'] ? raw_info['email'] : nil
end
def get_token_options(redirect_uri)
{ redirect_uri: redirect_uri }.merge(token_params.to_hash(symbolize_keys: true))
end
def prune!(hash)
hash.delete_if do |_, v|
prune!(v) if v.is_a?(Hash)
v.nil? || (v.respond_to?(:empty?) && v.empty?)
end
end
def image_url
return nil unless raw_info['picture']
u = URI.parse(raw_info['picture'].gsub('https:https', 'https'))
path_index = u.path.to_s.index('/photo.jpg')
if path_index && image_size_opts_passed?
u.path.insert(path_index, image_params)
u.path = u.path.gsub('//', '/')
end
u.query = strip_unnecessary_query_parameters(u.query)
u.to_s
end
def image_size_opts_passed?
options[:image_size] || options[:image_aspect_ratio]
end
def image_params
image_params = []
if options[:image_size].is_a?(Integer)
image_params << "s#{options[:image_size]}"
elsif options[:image_size].is_a?(Hash)
image_params << "w#{options[:image_size][:width]}" if options[:image_size][:width]
image_params << "h#{options[:image_size][:height]}" if options[:image_size][:height]
end
image_params << 'c' if options[:image_aspect_ratio] == 'square'
'/' + image_params.join('-')
end
def strip_unnecessary_query_parameters(query_parameters)
# strip `sz` parameter (defaults to sz=50) which overrides `image_size` options
return nil if query_parameters.nil?
params = CGI.parse(query_parameters)
stripped_params = params.delete_if { |key| key == 'sz' }
# don't return an empty Hash since that would result
# in URLs with a trailing ? character: http://image.url?
return nil if stripped_params.empty?
URI.encode_www_form(stripped_params)
end
def verify_token(access_token)
return false unless access_token
raw_response = client.request(:get, 'https://www.googleapis.com/oauth2/v3/tokeninfo',
params: { access_token: access_token }).parsed
raw_response['aud'] == options.client_id || options.authorized_client_ids.include?(raw_response['aud'])
end
def verify_hd(access_token)
return true unless options.hd
@raw_info ||= access_token.get(USER_INFO_URL).parsed
options.hd = options.hd.call if options.hd.is_a? Proc
allowed_hosted_domains = Array(options.hd)
raise CallbackError.new(:invalid_hd, 'Invalid Hosted Domain') unless allowed_hosted_domains.include?(@raw_info['hd']) || options.hd == '*'
true
end
end
end
end