-
-
Notifications
You must be signed in to change notification settings - Fork 300
/
oauth2.rb
166 lines (138 loc) · 5.66 KB
/
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
require "oauth2"
require "omniauth"
require "securerandom"
require "socket" # for SocketError
require "timeout" # for Timeout::Error
module OmniAuth
module Strategies
# Authentication strategy for connecting with APIs constructed using
# the [OAuth 2.0 Specification](http://tools.ietf.org/html/draft-ietf-oauth-v2-10).
# You must generally register your application with the provider and
# utilize an application id and secret in order to authenticate using
# OAuth 2.0.
class OAuth2
include OmniAuth::Strategy
def self.inherited(subclass)
OmniAuth::Strategy.included(subclass)
end
args %i[client_id client_secret]
option :client_id, nil
option :client_secret, nil
option :client_options, {}
option :authorize_params, {}
option :authorize_options, %i[scope state]
option :token_params, {}
option :token_options, []
option :auth_token_params, {}
option :provider_ignores_state, false
option :pkce, false
option :pkce_verifier, nil
option :pkce_options, {
:code_challenge => proc { |verifier|
Base64.urlsafe_encode64(
Digest::SHA2.digest(verifier),
:padding => false,
)
},
:code_challenge_method => "S256",
}
attr_accessor :access_token
def client
::OAuth2::Client.new(options.client_id, options.client_secret, deep_symbolize(options.client_options))
end
credentials do
hash = {"token" => access_token.token}
hash["refresh_token"] = access_token.refresh_token if access_token.expires? && access_token.refresh_token
hash["expires_at"] = access_token.expires_at if access_token.expires?
hash["expires"] = access_token.expires?
hash
end
def request_phase
redirect client.auth_code.authorize_url({:redirect_uri => callback_url}.merge(authorize_params))
end
def authorize_params # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
options.authorize_params[:state] = SecureRandom.hex(24)
if OmniAuth.config.test_mode
@env ||= {}
@env["rack.session"] ||= {}
end
params = options.authorize_params
.merge(options_for("authorize"))
.merge(pkce_authorize_params)
session["omniauth.pkce.verifier"] = options.pkce_verifier if options.pkce
session["omniauth.state"] = params[:state]
params
end
def token_params
options.token_params.merge(options_for("token")).merge(pkce_token_params)
end
def callback_phase # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
error = request.params["error_reason"] || request.params["error"]
if !options.provider_ignores_state && (request.params["state"].to_s.empty? || request.params["state"] != session.delete("omniauth.state"))
fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected"))
elsif error
fail!(error, CallbackError.new(request.params["error"], request.params["error_description"] || request.params["error_reason"], request.params["error_uri"]))
else
self.access_token = build_access_token
self.access_token = access_token.refresh! if access_token.expired?
super
end
rescue ::OAuth2::Error, CallbackError => e
fail!(:invalid_credentials, e)
rescue ::Timeout::Error, ::Errno::ETIMEDOUT, ::OAuth2::TimeoutError, ::OAuth2::ConnectionError => e
fail!(:timeout, e)
rescue ::SocketError => e
fail!(:failed_to_connect, e)
end
protected
def pkce_authorize_params
return {} unless options.pkce
options.pkce_verifier = SecureRandom.hex(64)
# NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A
{
:code_challenge => options.pkce_options[:code_challenge]
.call(options.pkce_verifier),
:code_challenge_method => options.pkce_options[:code_challenge_method],
}
end
def pkce_token_params
return {} unless options.pkce
{:code_verifier => session.delete("omniauth.pkce.verifier")}
end
def build_access_token
verifier = request.params["code"]
client.auth_code.get_token(verifier, {:redirect_uri => callback_url}.merge(token_params.to_hash(:symbolize_keys => true)), deep_symbolize(options.auth_token_params))
end
def deep_symbolize(options)
options.each_with_object({}) do |(key, value), hash|
hash[key.to_sym] = value.is_a?(Hash) ? deep_symbolize(value) : value
end
end
def options_for(option)
hash = {}
options.send(:"#{option}_options").select { |key| options[key] }.each do |key|
hash[key.to_sym] = if options[key].respond_to?(:call)
options[key].call(env)
else
options[key]
end
end
hash
end
# An error that is indicated in the OAuth 2.0 callback.
# This could be a `redirect_uri_mismatch` or other
class CallbackError < StandardError
attr_accessor :error, :error_reason, :error_uri
def initialize(error, error_reason = nil, error_uri = nil)
self.error = error
self.error_reason = error_reason
self.error_uri = error_uri
end
def message
[error, error_reason, error_uri].compact.join(" | ")
end
end
end
end
end
OmniAuth.config.add_camelization "oauth2", "OAuth2"