forked from discourse/discourse-oauth2-basic
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathplugin.rb
357 lines (300 loc) · 11.8 KB
/
plugin.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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# frozen_string_literal: true
# name: discourse-oauth2-basic
# about: Allows users to login to your forum using a basic OAuth2 provider.
# meta_topic_id: 33879
# version: 0.3
# authors: Robin Ward
# url: https://github.com/discourse/discourse-oauth2-basic
enabled_site_setting :oauth2_enabled
class ::OmniAuth::Strategies::Oauth2Basic < ::OmniAuth::Strategies::OAuth2
option :name, "oauth2_basic"
uid do
if path = SiteSetting.oauth2_callback_user_id_path.split(".")
recurse(access_token, [*path]) if path.present?
end
end
info do
if paths = SiteSetting.oauth2_callback_user_info_paths.split("|")
result = Hash.new
paths.each do |p|
segments = p.split(":")
if segments.length == 2
key = segments.first
path = [*segments.last.split(".")]
result[key] = recurse(access_token, path)
end
end
result
end
end
def callback_url
Discourse.base_url_no_prefix + script_name + callback_path
end
def recurse(obj, keys)
return nil if !obj
k = keys.shift
result = obj.respond_to?(k) ? obj.send(k) : obj[k]
keys.empty? ? result : recurse(result, keys)
end
end
require "faraday/logging/formatter"
class OAuth2FaradayFormatter < Faraday::Logging::Formatter
def request(env)
warn <<~LOG
OAuth2 Debugging: request #{env.method.upcase} #{env.url}
Headers:
#{env.request_headers.to_yaml}
Body:
#{env[:body].to_yaml}
LOG
end
def response(env)
warn <<~LOG
OAuth2 Debugging: response status #{env.status}
From #{env.method.upcase} #{env.url}
Headers:
#{env.request_headers.to_yaml}
Body:
#{env[:body].to_yaml}
LOG
end
end
# You should use this register if you want to add custom paths to traverse the user details JSON.
# We'll store the value in the user associated account's extra attribute hash using the full path as the key.
DiscoursePluginRegistry.define_filtered_register :oauth2_basic_additional_json_paths
# After authentication, we'll use this to confirm that the registered json paths are fulfilled, or display an error.
# This requires SiteSetting.oauth2_fetch_user_details? to be true, and can be used with
# DiscoursePluginRegistry.oauth2_basic_additional_json_paths.
#
# Example usage:
# DiscoursePluginRegistry.register_oauth2_basic_required_json_path({
# path: "extra:data.is_allowed_user",
# required_value: true,
# error_message: I18n.t("auth.user_not_allowed")
# }, self)
DiscoursePluginRegistry.define_filtered_register :oauth2_basic_required_json_paths
class ::OAuth2BasicAuthenticator < Auth::ManagedAuthenticator
def name
"oauth2_basic"
end
def can_revoke?
SiteSetting.oauth2_allow_association_change
end
def can_connect_existing_user?
SiteSetting.oauth2_allow_association_change
end
def register_middleware(omniauth)
omniauth.provider :oauth2_basic,
name: name,
setup:
lambda { |env|
opts = env["omniauth.strategy"].options
opts[:client_id] = SiteSetting.oauth2_client_id
opts[:client_secret] = SiteSetting.oauth2_client_secret
opts[:provider_ignores_state] = SiteSetting.oauth2_disable_csrf
opts[:client_options] = {
authorize_url: SiteSetting.oauth2_authorize_url,
token_url: SiteSetting.oauth2_token_url,
token_method: SiteSetting.oauth2_token_url_method.downcase.to_sym,
}
opts[:authorize_options] = SiteSetting
.oauth2_authorize_options
.split("|")
.map(&:to_sym)
if SiteSetting.oauth2_authorize_signup_url.present? &&
ActionDispatch::Request.new(env).params["signup"].present?
opts[:client_options][
:authorize_url
] = SiteSetting.oauth2_authorize_signup_url
end
if SiteSetting.oauth2_send_auth_header? &&
SiteSetting.oauth2_send_auth_body?
# For maximum compatibility we include both header and body auth by default
# This is a little unusual, and utilising multiple authentication methods
# is technically disallowed by the spec (RFC2749 Section 5.2)
opts[:client_options][:auth_scheme] = :request_body
opts[:token_params] = {
headers: {
"Authorization" => basic_auth_header,
},
}
elsif SiteSetting.oauth2_send_auth_header?
opts[:client_options][:auth_scheme] = :basic_auth
else
opts[:client_options][:auth_scheme] = :request_body
end
unless SiteSetting.oauth2_scope.blank?
opts[:scope] = SiteSetting.oauth2_scope
end
opts[:client_options][:connection_build] = lambda do |builder|
if SiteSetting.oauth2_debug_auth && defined?(OAuth2FaradayFormatter)
builder.response :logger,
Rails.logger,
{ bodies: true, formatter: OAuth2FaradayFormatter }
end
builder.request :url_encoded # form-encode POST params
builder.adapter FinalDestination::FaradayAdapter # make requests with FinalDestination::HTTP
end
}
end
def basic_auth_header
"Basic " +
Base64.strict_encode64("#{SiteSetting.oauth2_client_id}:#{SiteSetting.oauth2_client_secret}")
end
def walk_path(fragment, segments, seg_index = 0)
first_seg = segments[seg_index]
return if first_seg.blank? || fragment.blank?
return nil unless fragment.is_a?(Hash) || fragment.is_a?(Array)
first_seg = segments[seg_index].scan(/([\d+])/).length > 0 ? first_seg.split("[")[0] : first_seg
if fragment.is_a?(Hash)
deref = fragment[first_seg] || fragment[first_seg.to_sym]
else
array_index = 0
if (seg_index > 0)
last_index = segments[seg_index - 1].scan(/([\d+])/).flatten() || [0]
array_index = last_index.length > 0 ? last_index[0].to_i : 0
end
if fragment.any? && fragment.length >= array_index - 1
deref = fragment[array_index][first_seg]
else
deref = nil
end
end
if (deref.blank? || seg_index == segments.size - 1)
deref
else
seg_index += 1
walk_path(deref, segments, seg_index)
end
end
def json_walk(result, user_json, prop, custom_path: nil)
path = custom_path || SiteSetting.public_send("oauth2_json_#{prop}_path")
if path.present?
#this.[].that is the same as this.that, allows for both this[0].that and this.[0].that path styles
path = path.gsub(".[].", ".").gsub(".[", "[")
segments = parse_segments(path)
val = walk_path(user_json, segments)
result[prop] = val if val.present?
end
end
def parse_segments(path)
segments = [+""]
quoted = false
escaped = false
path
.split("")
.each do |char|
next_char_escaped = false
if !escaped && (char == '"')
quoted = !quoted
elsif !escaped && !quoted && (char == ".")
segments.append +""
elsif !escaped && (char == '\\')
next_char_escaped = true
else
segments.last << char
end
escaped = next_char_escaped
end
segments
end
def log(info)
Rails.logger.warn("OAuth2 Debugging: #{info}") if SiteSetting.oauth2_debug_auth
end
def fetch_user_details(token, id)
user_json_url = SiteSetting.oauth2_user_json_url.sub(":token", token.to_s).sub(":id", id.to_s)
user_json_method = SiteSetting.oauth2_user_json_url_method.downcase.to_sym
bearer_token = "Bearer #{token}"
connection = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
headers = { "Authorization" => bearer_token, "Accept" => "application/json" }
user_json_response = connection.run_request(user_json_method, user_json_url, nil, headers)
log <<-LOG
user_json request: #{user_json_method} #{user_json_url}
request headers: #{headers}
response status: #{user_json_response.status}
response body:
#{user_json_response.body}
LOG
if user_json_response.status == 200
user_json = JSON.parse(user_json_response.body)
log("user_json:\n#{user_json.to_yaml}")
result = {}
if user_json.present?
json_walk(result, user_json, :user_id)
json_walk(result, user_json, :username)
json_walk(result, user_json, :name)
json_walk(result, user_json, :email)
json_walk(result, user_json, :email_verified)
json_walk(result, user_json, :avatar)
DiscoursePluginRegistry.oauth2_basic_additional_json_paths.each do |detail|
prop = "extra:#{detail}"
json_walk(result, user_json, prop, custom_path: detail)
end
end
result
else
nil
end
end
def primary_email_verified?(auth)
return true if SiteSetting.oauth2_email_verified
verified = auth["info"]["email_verified"]
verified = true if verified == "true"
verified = false if verified == "false"
verified
end
def always_update_user_email?
SiteSetting.oauth2_overrides_email
end
def after_authenticate(auth, existing_account: nil)
log <<-LOG
after_authenticate response:
creds:
#{auth["credentials"].to_hash.to_yaml}
uid: #{auth["uid"]}
info:
#{auth["info"].to_hash.to_yaml}
extra:
#{auth["extra"].to_hash.to_yaml}
LOG
if SiteSetting.oauth2_fetch_user_details? && SiteSetting.oauth2_user_json_url.present?
if fetched_user_details = fetch_user_details(auth["credentials"]["token"], auth["uid"])
auth["uid"] = fetched_user_details[:user_id] if fetched_user_details[:user_id]
auth["info"]["nickname"] = fetched_user_details[:username] if fetched_user_details[
:username
]
auth["info"]["image"] = fetched_user_details[:avatar] if fetched_user_details[:avatar]
%w[name email email_verified].each do |property|
auth["info"][property] = fetched_user_details[property.to_sym] if fetched_user_details[
property.to_sym
]
end
DiscoursePluginRegistry.oauth2_basic_additional_json_paths.each do |detail|
auth["extra"][detail] = fetched_user_details["extra:#{detail}"]
end
DiscoursePluginRegistry.oauth2_basic_required_json_paths.each do |x|
if fetched_user_details[x[:path]] != x[:required_value]
result = Auth::Result.new
result.failed = true
result.failed_reason = x[:error_message]
return result
end
end
else
result = Auth::Result.new
result.failed = true
result.failed_reason = I18n.t("login.authenticator_error_fetch_user_details")
return result
end
end
super(auth, existing_account: existing_account)
end
def enabled?
SiteSetting.oauth2_enabled
end
end
auth_provider title_setting: "oauth2_button_title", authenticator: OAuth2BasicAuthenticator.new
load File.expand_path(
"../lib/validators/oauth2_basic/oauth2_fetch_user_details_validator.rb",
__FILE__,
)