rodauth-oauth 0.0.1 → 0.0.6
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/CHANGELOG.md +148 -1
- data/LICENSE.txt +191 -0
- data/README.md +309 -21
- data/lib/generators/roda/oauth/install_generator.rb +1 -1
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +12 -0
- data/lib/generators/roda/oauth/views_generator.rb +1 -6
- data/lib/rodauth/features/oauth.rb +740 -301
- data/lib/rodauth/features/oauth_http_mac.rb +108 -0
- data/lib/rodauth/features/oauth_jwt.rb +421 -0
- data/lib/rodauth/oauth/ttl_store.rb +59 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +7 -2
@@ -10,7 +10,7 @@ module Rodauth::OAuth::Rails
|
|
10
10
|
include ::Rails::Generators::Migration
|
11
11
|
|
12
12
|
source_root "#{__dir__}/templates"
|
13
|
-
namespace "
|
13
|
+
namespace "rodauth:oauth:install"
|
14
14
|
|
15
15
|
def create_rodauth_migration
|
16
16
|
return unless defined?(ActiveRecord::Base)
|
@@ -24,6 +24,12 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
|
|
24
24
|
t.datetime :revoked_at
|
25
25
|
t.string :scopes, null: false
|
26
26
|
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
27
|
+
# for using access_types
|
28
|
+
t.string :access_type, null: false, default: "offline"
|
29
|
+
# uncomment to enable PKCE
|
30
|
+
# t.string :code_challenge
|
31
|
+
# t.string :code_challenge_method
|
32
|
+
|
27
33
|
t.index(%i[oauth_application_id code], unique: true)
|
28
34
|
end
|
29
35
|
|
@@ -37,7 +43,13 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
|
|
37
43
|
t.integer :oauth_application_id
|
38
44
|
t.foreign_key :oauth_applications, column: :oauth_application_id
|
39
45
|
t.string :token, null: false, token: true
|
46
|
+
# uncomment if setting oauth_tokens_token_hash_column
|
47
|
+
# and delete the token column
|
48
|
+
# t.string :token_hash, token: true
|
40
49
|
t.string :refresh_token
|
50
|
+
# uncomment if setting oauth_tokens_refresh_token_hash_column
|
51
|
+
# and delete the refresh_token column
|
52
|
+
# t.string :refresh_token_hash, token: true
|
41
53
|
t.datetime :expires_in, null: false
|
42
54
|
t.datetime :revoked_at
|
43
55
|
t.string :scopes, null: false
|
@@ -7,7 +7,7 @@ module Rodauth::OAuth
|
|
7
7
|
module Generators
|
8
8
|
class ViewsGenerator < ::Rails::Generators::Base
|
9
9
|
source_root "#{__dir__}/templates"
|
10
|
-
namespace "
|
10
|
+
namespace "rodauth:oauth:views"
|
11
11
|
|
12
12
|
DEFAULT = %w[oauth_authorize].freeze
|
13
13
|
VIEWS = {
|
@@ -16,11 +16,6 @@ module Rodauth::OAuth
|
|
16
16
|
}.freeze
|
17
17
|
|
18
18
|
DEPENDENCIES = {
|
19
|
-
active_sessions: :logout,
|
20
|
-
otp: :two_factor_base,
|
21
|
-
sms_codes: :two_factor_base,
|
22
|
-
recovery_codes: :two_factor_base,
|
23
|
-
webauthn: :two_factor_base
|
24
19
|
}.freeze
|
25
20
|
|
26
21
|
class_option :features, type: :array,
|
@@ -1,8 +1,15 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
+
require "base64"
|
4
|
+
require "securerandom"
|
5
|
+
require "net/http"
|
6
|
+
|
7
|
+
require "rodauth/oauth/ttl_store"
|
8
|
+
|
3
9
|
module Rodauth
|
4
10
|
Feature.define(:oauth) do
|
5
11
|
# RUBY EXTENSIONS
|
12
|
+
# :nocov:
|
6
13
|
unless Regexp.method_defined?(:match?)
|
7
14
|
module RegexpExtensions
|
8
15
|
refine(Regexp) do
|
@@ -14,20 +21,37 @@ module Rodauth
|
|
14
21
|
using(RegexpExtensions)
|
15
22
|
end
|
16
23
|
|
17
|
-
|
24
|
+
unless String.method_defined?(:delete_suffix!)
|
25
|
+
module SuffixExtensions
|
26
|
+
refine(String) do
|
27
|
+
def delete_suffix!(suffix)
|
28
|
+
suffix = suffix.to_s
|
29
|
+
chomp! if frozen?
|
30
|
+
len = suffix.length
|
31
|
+
return unless len.positive? && index(suffix, -len)
|
32
|
+
|
33
|
+
self[-len..-1] = ""
|
34
|
+
self
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
using(SuffixExtensions)
|
39
|
+
end
|
40
|
+
# :nocov:
|
18
41
|
|
19
|
-
|
42
|
+
SCOPES = %w[profile.read].freeze
|
20
43
|
|
21
44
|
before "authorize"
|
22
45
|
after "authorize"
|
23
46
|
after "authorize_failure"
|
24
47
|
|
25
48
|
before "token"
|
26
|
-
after "token"
|
27
49
|
|
28
50
|
before "revoke"
|
29
51
|
after "revoke"
|
30
52
|
|
53
|
+
before "introspect"
|
54
|
+
|
31
55
|
before "create_oauth_application"
|
32
56
|
after "create_oauth_application"
|
33
57
|
|
@@ -49,26 +73,28 @@ module Rodauth
|
|
49
73
|
|
50
74
|
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
|
51
75
|
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
|
52
|
-
auth_value_method :use_oauth_implicit_grant_type
|
76
|
+
auth_value_method :use_oauth_implicit_grant_type?, false
|
77
|
+
auth_value_method :use_oauth_pkce?, true
|
78
|
+
auth_value_method :use_oauth_access_type?, true
|
53
79
|
|
54
|
-
|
80
|
+
auth_value_method :oauth_require_pkce, false
|
81
|
+
auth_value_method :oauth_pkce_challenge_method, "S256"
|
55
82
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
state redirect_uri scopes token_type_hint token
|
60
|
-
access_type response_type
|
61
|
-
].each do |param|
|
62
|
-
auth_value_method :"#{param}_param", param
|
63
|
-
end
|
83
|
+
auth_value_method :oauth_valid_uri_schemes, %w[http https]
|
84
|
+
|
85
|
+
auth_value_method :oauth_scope_separator, " "
|
64
86
|
|
65
87
|
# Application
|
66
|
-
APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri].freeze
|
88
|
+
APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri client_secret].freeze
|
67
89
|
auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
|
68
90
|
|
69
|
-
(APPLICATION_REQUIRED_PARAMS + %w[client_id
|
91
|
+
(APPLICATION_REQUIRED_PARAMS + %w[client_id]).each do |param|
|
70
92
|
auth_value_method :"oauth_application_#{param}_param", param
|
93
|
+
translatable_method :"#{param}_label", param.gsub("_", " ").capitalize
|
71
94
|
end
|
95
|
+
button "Register", "oauth_application"
|
96
|
+
button "Authorize", "oauth_authorize"
|
97
|
+
button "Revoke", "oauth_token_revoke"
|
72
98
|
|
73
99
|
# OAuth Token
|
74
100
|
auth_value_method :oauth_tokens_path, "oauth-tokens"
|
@@ -83,6 +109,10 @@ module Rodauth
|
|
83
109
|
auth_value_method :"oauth_tokens_#{column}_column", column
|
84
110
|
end
|
85
111
|
|
112
|
+
# Oauth Token Hash
|
113
|
+
auth_value_method :oauth_tokens_token_hash_column, nil
|
114
|
+
auth_value_method :oauth_tokens_refresh_token_hash_column, nil
|
115
|
+
|
86
116
|
# OAuth Grants
|
87
117
|
auth_value_method :oauth_grants_table, :oauth_grants
|
88
118
|
auth_value_method :oauth_grants_id_column, :id
|
@@ -90,6 +120,7 @@ module Rodauth
|
|
90
120
|
account_id oauth_application_id
|
91
121
|
redirect_uri code scopes access_type
|
92
122
|
expires_in revoked_at
|
123
|
+
code_challenge code_challenge_method
|
93
124
|
].each do |column|
|
94
125
|
auth_value_method :"oauth_grants_#{column}_column", column
|
95
126
|
end
|
@@ -115,7 +146,7 @@ module Rodauth
|
|
115
146
|
|
116
147
|
auth_value_method :oauth_application_default_scope, SCOPES.first
|
117
148
|
auth_value_method :oauth_application_scopes, SCOPES
|
118
|
-
auth_value_method :oauth_token_type, "
|
149
|
+
auth_value_method :oauth_token_type, "bearer"
|
119
150
|
|
120
151
|
auth_value_method :invalid_request, "Request is missing a required parameter"
|
121
152
|
auth_value_method :invalid_client, "Invalid client"
|
@@ -130,31 +161,46 @@ module Rodauth
|
|
130
161
|
auth_value_method :unique_error_message, "is already in use"
|
131
162
|
auth_value_method :null_error_message, "is not filled"
|
132
163
|
|
164
|
+
# PKCE
|
165
|
+
auth_value_method :code_challenge_required_error_code, "invalid_request"
|
166
|
+
auth_value_method :code_challenge_required_message, "code challenge required"
|
167
|
+
auth_value_method :unsupported_transform_algorithm_error_code, "invalid_request"
|
168
|
+
auth_value_method :unsupported_transform_algorithm_message, "transform algorithm not supported"
|
169
|
+
|
170
|
+
# METADATA
|
171
|
+
auth_value_method :oauth_metadata_service_documentation, nil
|
172
|
+
auth_value_method :oauth_metadata_ui_locales_supported, nil
|
173
|
+
auth_value_method :oauth_metadata_op_policy_uri, nil
|
174
|
+
auth_value_method :oauth_metadata_op_tos_uri, nil
|
175
|
+
|
176
|
+
# Resource Server params
|
177
|
+
# Only required to use if the plugin is to be used in a resource server
|
178
|
+
auth_value_method :is_authorization_server?, true
|
179
|
+
|
133
180
|
auth_value_methods(
|
134
|
-
:
|
181
|
+
:fetch_access_token,
|
182
|
+
:oauth_unique_id_generator,
|
183
|
+
:secret_matches?,
|
184
|
+
:secret_hash,
|
185
|
+
:generate_token_hash,
|
186
|
+
:authorization_server_url,
|
187
|
+
:before_introspection_request
|
135
188
|
)
|
136
189
|
|
137
|
-
|
138
|
-
"/#{oauth_applications_path}/#{id}"
|
139
|
-
end
|
190
|
+
auth_value_methods(:only_json?)
|
140
191
|
|
141
|
-
|
142
|
-
if logged_in?
|
143
|
-
oauth_authorize_path
|
144
|
-
else
|
145
|
-
login_redirect
|
146
|
-
end
|
147
|
-
end
|
192
|
+
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
148
193
|
|
149
|
-
|
150
|
-
auth_methods(:json_request?)
|
194
|
+
SERVER_METADATA = OAuth::TtlStore.new
|
151
195
|
|
152
196
|
def check_csrf?
|
153
197
|
case request.path
|
154
|
-
when oauth_token_path
|
198
|
+
when oauth_token_path, oauth_introspect_path
|
155
199
|
false
|
156
200
|
when oauth_revoke_path
|
157
201
|
!json_request?
|
202
|
+
when oauth_authorize_path, %r{/#{oauth_applications_path}}
|
203
|
+
only_json? ? false : super
|
158
204
|
else
|
159
205
|
super
|
160
206
|
end
|
@@ -165,104 +211,98 @@ module Rodauth
|
|
165
211
|
super || authorization_token
|
166
212
|
end
|
167
213
|
|
168
|
-
def
|
169
|
-
return
|
214
|
+
def accepts_json?
|
215
|
+
return true if only_json?
|
170
216
|
|
171
|
-
|
217
|
+
(accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
|
172
218
|
end
|
173
219
|
|
174
|
-
|
220
|
+
unless method_defined?(:json_request?)
|
221
|
+
# :nocov:
|
222
|
+
# copied from the jwt feature
|
223
|
+
def json_request?
|
224
|
+
return @json_request if defined?(@json_request)
|
175
225
|
|
176
|
-
|
177
|
-
|
226
|
+
@json_request = request.content_type =~ json_request_regexp
|
227
|
+
end
|
228
|
+
# :nocov:
|
178
229
|
end
|
179
230
|
|
180
|
-
def
|
181
|
-
|
182
|
-
|
183
|
-
return unless state && !state.empty?
|
184
|
-
|
185
|
-
state
|
231
|
+
def initialize(scope)
|
232
|
+
@scope = scope
|
186
233
|
end
|
187
234
|
|
188
235
|
def scopes
|
189
|
-
|
190
|
-
|
191
|
-
return [oauth_application_default_scope] unless scopes && !scopes.empty?
|
192
|
-
|
193
|
-
scopes.split(" ")
|
236
|
+
(param_or_nil("scope") || oauth_application_default_scope).split(" ")
|
194
237
|
end
|
195
238
|
|
196
|
-
def
|
197
|
-
|
198
|
-
|
199
|
-
return unless client_id && !client_id.empty?
|
239
|
+
def redirect_uri
|
240
|
+
param_or_nil("redirect_uri") || begin
|
241
|
+
return unless oauth_application
|
200
242
|
|
201
|
-
|
243
|
+
redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
|
244
|
+
redirect_uris.size == 1 ? redirect_uris.first : nil
|
245
|
+
end
|
202
246
|
end
|
203
247
|
|
204
|
-
def
|
205
|
-
|
248
|
+
def oauth_application
|
249
|
+
return @oauth_application if defined?(@oauth_application)
|
206
250
|
|
207
|
-
|
251
|
+
@oauth_application = begin
|
252
|
+
client_id = param_or_nil("client_id")
|
208
253
|
|
209
|
-
|
210
|
-
end
|
254
|
+
return unless client_id
|
211
255
|
|
212
|
-
|
213
|
-
|
256
|
+
db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
|
257
|
+
end
|
258
|
+
end
|
214
259
|
|
215
|
-
|
260
|
+
def fetch_access_token
|
261
|
+
value = request.env["HTTP_AUTHORIZATION"]
|
216
262
|
|
217
|
-
|
218
|
-
end
|
263
|
+
return unless value
|
219
264
|
|
220
|
-
|
221
|
-
token = param(token_param)
|
265
|
+
scheme, token = value.split(" ", 2)
|
222
266
|
|
223
|
-
return unless
|
267
|
+
return unless scheme.downcase == oauth_token_type
|
224
268
|
|
225
269
|
token
|
226
270
|
end
|
227
271
|
|
228
|
-
def
|
229
|
-
return @
|
272
|
+
def authorization_token
|
273
|
+
return @authorization_token if defined?(@authorization_token)
|
230
274
|
|
231
|
-
|
232
|
-
|
275
|
+
# check if there is a token
|
276
|
+
bearer_token = fetch_access_token
|
233
277
|
|
234
|
-
|
278
|
+
return unless bearer_token
|
235
279
|
|
236
|
-
|
237
|
-
|
280
|
+
# check if token has not expired
|
281
|
+
# check if token has been revoked
|
282
|
+
@authorization_token = oauth_token_by_token(bearer_token)
|
238
283
|
end
|
239
284
|
|
240
|
-
def
|
241
|
-
|
285
|
+
def require_oauth_authorization(*scopes)
|
286
|
+
token_scopes = if is_authorization_server?
|
287
|
+
authorization_required unless authorization_token
|
242
288
|
|
243
|
-
|
244
|
-
value = request.get_header("HTTP_AUTHORIZATION").to_s
|
289
|
+
scopes << oauth_application_default_scope if scopes.empty?
|
245
290
|
|
246
|
-
|
291
|
+
authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
|
292
|
+
else
|
293
|
+
bearer_token = fetch_access_token
|
247
294
|
|
248
|
-
|
295
|
+
authorization_required unless bearer_token
|
249
296
|
|
250
|
-
|
251
|
-
# check if token has not expired
|
252
|
-
# check if token has been revoked
|
253
|
-
db[oauth_tokens_table].where(oauth_tokens_token_column => token)
|
254
|
-
.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
255
|
-
.where(oauth_tokens_revoked_at_column => nil)
|
256
|
-
.first
|
257
|
-
end
|
258
|
-
end
|
297
|
+
scopes << oauth_application_default_scope if scopes.empty?
|
259
298
|
|
260
|
-
|
261
|
-
|
299
|
+
# where in resource server, NOT the authorization server.
|
300
|
+
payload = introspection_request("access_token", bearer_token)
|
262
301
|
|
263
|
-
|
302
|
+
authorization_required unless payload["active"]
|
264
303
|
|
265
|
-
|
304
|
+
payload["scope"].split(oauth_scope_separator)
|
305
|
+
end
|
266
306
|
|
267
307
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
268
308
|
end
|
@@ -275,8 +315,11 @@ module Rodauth
|
|
275
315
|
request.get "new" do
|
276
316
|
new_oauth_application_view
|
277
317
|
end
|
318
|
+
|
278
319
|
request.on(oauth_applications_id_pattern) do |id|
|
279
320
|
oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
|
321
|
+
next unless oauth_application
|
322
|
+
|
280
323
|
scope.instance_variable_set(:@oauth_application, oauth_application)
|
281
324
|
|
282
325
|
request.is do
|
@@ -288,7 +331,9 @@ module Rodauth
|
|
288
331
|
request.on(oauth_tokens_path) do
|
289
332
|
oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
|
290
333
|
scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
|
291
|
-
|
334
|
+
request.get do
|
335
|
+
oauth_tokens_view
|
336
|
+
end
|
292
337
|
end
|
293
338
|
end
|
294
339
|
|
@@ -306,7 +351,7 @@ module Rodauth
|
|
306
351
|
id = create_oauth_application
|
307
352
|
after_create_oauth_application
|
308
353
|
set_notice_flash create_oauth_application_notice_flash
|
309
|
-
redirect
|
354
|
+
redirect "#{request.path}/#{id}"
|
310
355
|
end
|
311
356
|
end
|
312
357
|
set_error_flash create_oauth_application_error_flash
|
@@ -315,10 +360,216 @@ module Rodauth
|
|
315
360
|
end
|
316
361
|
end
|
317
362
|
|
363
|
+
def oauth_server_metadata(issuer = nil)
|
364
|
+
request.on(".well-known") do
|
365
|
+
request.on("oauth-authorization-server") do
|
366
|
+
request.get do
|
367
|
+
json_response_success(oauth_server_metadata_body(issuer))
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
318
373
|
private
|
319
374
|
|
375
|
+
def authorization_server_url
|
376
|
+
base_url
|
377
|
+
end
|
378
|
+
|
379
|
+
def authorization_server_metadata
|
380
|
+
auth_url = URI(authorization_server_url)
|
381
|
+
|
382
|
+
server_metadata = SERVER_METADATA[auth_url]
|
383
|
+
|
384
|
+
return server_metadata if server_metadata
|
385
|
+
|
386
|
+
SERVER_METADATA.set(auth_url) do
|
387
|
+
http = Net::HTTP.new(auth_url.host, auth_url.port)
|
388
|
+
http.use_ssl = auth_url.scheme == "https"
|
389
|
+
|
390
|
+
request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
|
391
|
+
request["accept"] = json_response_content_type
|
392
|
+
response = http.request(request)
|
393
|
+
authorization_required unless response.code.to_i == 200
|
394
|
+
|
395
|
+
# time-to-live
|
396
|
+
ttl = if response.key?("cache-control")
|
397
|
+
cache_control = response["cache_control"]
|
398
|
+
cache_control[/max-age=(\d+)/, 1]
|
399
|
+
elsif response.key?("expires")
|
400
|
+
Time.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
|
401
|
+
end
|
402
|
+
|
403
|
+
[JSON.parse(response.body, symbolize_names: true), ttl]
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
def introspection_request(token_type_hint, token)
|
408
|
+
auth_url = URI(authorization_server_url)
|
409
|
+
http = Net::HTTP.new(auth_url.host, auth_url.port)
|
410
|
+
http.use_ssl = auth_url.scheme == "https"
|
411
|
+
|
412
|
+
request = Net::HTTP::Post.new(oauth_introspect_path)
|
413
|
+
request["content-type"] = json_response_content_type
|
414
|
+
request["accept"] = json_response_content_type
|
415
|
+
request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
|
416
|
+
|
417
|
+
before_introspection_request(request)
|
418
|
+
response = http.request(request)
|
419
|
+
authorization_required unless response.code.to_i == 200
|
420
|
+
|
421
|
+
JSON.parse(response.body)
|
422
|
+
end
|
423
|
+
|
424
|
+
def before_introspection_request(request); end
|
425
|
+
|
426
|
+
def template_path(page)
|
427
|
+
path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
|
428
|
+
return super unless File.exist?(path)
|
429
|
+
|
430
|
+
path
|
431
|
+
end
|
432
|
+
|
433
|
+
# to be used internally. Same semantics as require account, must:
|
434
|
+
# fetch an authorization basic header
|
435
|
+
# parse client id and secret
|
436
|
+
#
|
437
|
+
def require_oauth_application
|
438
|
+
# get client credenntials
|
439
|
+
client_id = client_secret = nil
|
440
|
+
|
441
|
+
# client_secret_basic
|
442
|
+
if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
|
443
|
+
client_id, client_secret = Base64.decode64(token).split(/:/, 2)
|
444
|
+
else
|
445
|
+
client_id = param_or_nil("client_id")
|
446
|
+
client_secret = param_or_nil("client_secret")
|
447
|
+
end
|
448
|
+
|
449
|
+
authorization_required unless client_id
|
450
|
+
|
451
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
452
|
+
|
453
|
+
# skip if using pkce
|
454
|
+
return if @oauth_application && use_oauth_pkce? && param_or_nil("code_verifier")
|
455
|
+
|
456
|
+
authorization_required unless @oauth_application && secret_matches?(@oauth_application, client_secret)
|
457
|
+
end
|
458
|
+
|
459
|
+
def secret_matches?(oauth_application, secret)
|
460
|
+
BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret
|
461
|
+
end
|
462
|
+
|
463
|
+
def secret_hash(secret)
|
464
|
+
password_hash(secret)
|
465
|
+
end
|
466
|
+
|
320
467
|
def oauth_unique_id_generator
|
321
|
-
SecureRandom.
|
468
|
+
SecureRandom.hex(32)
|
469
|
+
end
|
470
|
+
|
471
|
+
def generate_token_hash(token)
|
472
|
+
Base64.urlsafe_encode64(Digest::SHA256.digest(token))
|
473
|
+
end
|
474
|
+
|
475
|
+
def token_from_application?(oauth_token, oauth_application)
|
476
|
+
oauth_token[oauth_tokens_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
|
477
|
+
end
|
478
|
+
|
479
|
+
unless method_defined?(:password_hash)
|
480
|
+
# :nocov:
|
481
|
+
# From login_requirements_base feature
|
482
|
+
if ENV["RACK_ENV"] == "test"
|
483
|
+
def password_hash_cost
|
484
|
+
BCrypt::Engine::MIN_COST
|
485
|
+
end
|
486
|
+
else
|
487
|
+
def password_hash_cost
|
488
|
+
BCrypt::Engine::DEFAULT_COST
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
def password_hash(password)
|
493
|
+
BCrypt::Password.create(password, cost: password_hash_cost)
|
494
|
+
end
|
495
|
+
# :nocov:
|
496
|
+
end
|
497
|
+
|
498
|
+
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
499
|
+
create_params = {
|
500
|
+
oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
|
501
|
+
}.merge(params)
|
502
|
+
|
503
|
+
token = oauth_unique_id_generator
|
504
|
+
refresh_token = nil
|
505
|
+
|
506
|
+
if oauth_tokens_token_hash_column
|
507
|
+
create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
508
|
+
else
|
509
|
+
create_params[oauth_tokens_token_column] = token
|
510
|
+
end
|
511
|
+
|
512
|
+
if should_generate_refresh_token
|
513
|
+
refresh_token = oauth_unique_id_generator
|
514
|
+
|
515
|
+
if oauth_tokens_refresh_token_hash_column
|
516
|
+
create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
517
|
+
else
|
518
|
+
create_params[oauth_tokens_refresh_token_column] = refresh_token
|
519
|
+
end
|
520
|
+
end
|
521
|
+
oauth_token = _generate_oauth_token(create_params)
|
522
|
+
|
523
|
+
oauth_token[oauth_tokens_token_column] = token
|
524
|
+
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
525
|
+
oauth_token
|
526
|
+
end
|
527
|
+
|
528
|
+
def _generate_oauth_token(params = {})
|
529
|
+
ds = db[oauth_tokens_table]
|
530
|
+
|
531
|
+
begin
|
532
|
+
if ds.supports_returning?(:insert)
|
533
|
+
ds.returning.insert(params).first
|
534
|
+
else
|
535
|
+
id = ds.insert(params)
|
536
|
+
ds.where(oauth_tokens_id_column => id).first
|
537
|
+
end
|
538
|
+
rescue Sequel::UniqueConstraintViolation
|
539
|
+
retry
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
def oauth_token_by_token(token, dataset = db[oauth_tokens_table])
|
544
|
+
ds = if oauth_tokens_token_hash_column
|
545
|
+
dataset.where(oauth_tokens_token_hash_column => generate_token_hash(token))
|
546
|
+
else
|
547
|
+
dataset.where(oauth_tokens_token_column => token)
|
548
|
+
end
|
549
|
+
|
550
|
+
ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
551
|
+
.where(oauth_tokens_revoked_at_column => nil).first
|
552
|
+
end
|
553
|
+
|
554
|
+
def oauth_token_by_refresh_token(token, dataset = db[oauth_tokens_table])
|
555
|
+
ds = if oauth_tokens_refresh_token_hash_column
|
556
|
+
dataset.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
|
557
|
+
else
|
558
|
+
dataset.where(oauth_tokens_refresh_token_column => token)
|
559
|
+
end
|
560
|
+
|
561
|
+
ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
562
|
+
.where(oauth_tokens_revoked_at_column => nil).first
|
563
|
+
end
|
564
|
+
|
565
|
+
def json_access_token_payload(oauth_token)
|
566
|
+
payload = {
|
567
|
+
"access_token" => oauth_token[oauth_tokens_token_column],
|
568
|
+
"token_type" => oauth_token_type,
|
569
|
+
"expires_in" => oauth_token_expires_in
|
570
|
+
}
|
571
|
+
payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[oauth_tokens_refresh_token_column]
|
572
|
+
payload
|
322
573
|
end
|
323
574
|
|
324
575
|
# Oauth Application
|
@@ -336,11 +587,21 @@ module Rodauth
|
|
336
587
|
|
337
588
|
def validate_oauth_application_params
|
338
589
|
oauth_application_params.each do |key, value|
|
339
|
-
if key == oauth_application_homepage_url_param
|
340
|
-
key == oauth_application_redirect_uri_param
|
590
|
+
if key == oauth_application_homepage_url_param
|
341
591
|
|
342
|
-
set_field_error(key, invalid_url_message) unless
|
592
|
+
set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
|
343
593
|
|
594
|
+
elsif key == oauth_application_redirect_uri_param
|
595
|
+
|
596
|
+
if value.respond_to?(:each)
|
597
|
+
value.each do |uri|
|
598
|
+
next if uri.empty?
|
599
|
+
|
600
|
+
set_field_error(key, invalid_url_message) unless check_valid_uri?(uri)
|
601
|
+
end
|
602
|
+
else
|
603
|
+
set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
|
604
|
+
end
|
344
605
|
elsif key == oauth_application_scopes_param
|
345
606
|
|
346
607
|
value.each do |scope|
|
@@ -358,35 +619,32 @@ module Rodauth
|
|
358
619
|
oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
|
359
620
|
oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
|
360
621
|
oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
|
361
|
-
oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
|
362
|
-
oauth_applications_redirect_uri_column => oauth_application_params[oauth_application_redirect_uri_param]
|
622
|
+
oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
|
363
623
|
}
|
364
624
|
|
625
|
+
redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
|
626
|
+
redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
|
627
|
+
create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
|
365
628
|
# set client ID/secret pairs
|
629
|
+
|
366
630
|
create_params.merge! \
|
367
631
|
oauth_applications_client_id_column => oauth_unique_id_generator,
|
368
|
-
oauth_applications_client_secret_column =>
|
632
|
+
oauth_applications_client_secret_column => \
|
633
|
+
secret_hash(oauth_application_params[oauth_application_client_secret_param])
|
369
634
|
|
370
635
|
create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
|
371
|
-
create_params[oauth_applications_scopes_column].join(
|
636
|
+
create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
|
372
637
|
else
|
373
638
|
oauth_application_default_scope
|
374
639
|
end
|
375
640
|
|
376
|
-
ds = db[oauth_applications_table]
|
377
|
-
|
378
641
|
id = nil
|
379
642
|
raised = begin
|
380
|
-
|
381
|
-
|
382
|
-
else
|
383
|
-
id = db[oauth_applications_table].insert(create_params)
|
384
|
-
db[oauth_applications_table].where(oauth_applications_id_column => id).get(oauth_applications_id_column)
|
385
|
-
end
|
386
|
-
false
|
643
|
+
id = db[oauth_applications_table].insert(create_params)
|
644
|
+
false
|
387
645
|
rescue Sequel::ConstraintViolation => e
|
388
646
|
e
|
389
|
-
|
647
|
+
end
|
390
648
|
|
391
649
|
if raised
|
392
650
|
field = raised.message[/\.(.*)$/, 1]
|
@@ -402,12 +660,38 @@ module Rodauth
|
|
402
660
|
end
|
403
661
|
|
404
662
|
# Authorize
|
663
|
+
def before_authorize
|
664
|
+
require_account
|
665
|
+
end
|
405
666
|
|
406
667
|
def validate_oauth_grant_params
|
407
|
-
unless oauth_application && check_valid_redirect_uri?
|
668
|
+
redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri?
|
669
|
+
|
670
|
+
unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
|
671
|
+
check_valid_approval_prompt? && check_valid_response_type?
|
408
672
|
redirect_response_error("invalid_request")
|
409
673
|
end
|
410
674
|
redirect_response_error("invalid_scope") unless check_valid_scopes?
|
675
|
+
|
676
|
+
validate_pkce_challenge_params if use_oauth_pkce?
|
677
|
+
end
|
678
|
+
|
679
|
+
def try_approval_prompt
|
680
|
+
approval_prompt = param_or_nil("approval_prompt")
|
681
|
+
|
682
|
+
return unless approval_prompt && approval_prompt == "auto"
|
683
|
+
|
684
|
+
return if db[oauth_grants_table].where(
|
685
|
+
oauth_grants_account_id_column => account_id,
|
686
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
687
|
+
oauth_grants_redirect_uri_column => redirect_uri,
|
688
|
+
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
689
|
+
oauth_grants_access_type_column => "online"
|
690
|
+
).count.zero?
|
691
|
+
|
692
|
+
# if there's a previous oauth grant for the params combo, it means that this user has approved before.
|
693
|
+
|
694
|
+
request.env["REQUEST_METHOD"] = "POST"
|
411
695
|
end
|
412
696
|
|
413
697
|
def create_oauth_grant
|
@@ -415,24 +699,37 @@ module Rodauth
|
|
415
699
|
oauth_grants_account_id_column => account_id,
|
416
700
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
417
701
|
oauth_grants_redirect_uri_column => redirect_uri,
|
418
|
-
oauth_grants_code_column => oauth_unique_id_generator,
|
419
702
|
oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
|
420
|
-
oauth_grants_scopes_column => scopes.join(
|
703
|
+
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
421
704
|
}
|
422
705
|
|
423
|
-
|
424
|
-
|
706
|
+
# Access Type flow
|
707
|
+
if use_oauth_access_type?
|
708
|
+
if (access_type = param_or_nil("access_type"))
|
709
|
+
create_params[oauth_grants_access_type_column] = access_type
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
# PKCE flow
|
714
|
+
if use_oauth_pkce?
|
715
|
+
|
716
|
+
if (code_challenge = param_or_nil("code_challenge"))
|
717
|
+
code_challenge_method = param_or_nil("code_challenge_method")
|
718
|
+
|
719
|
+
create_params[oauth_grants_code_challenge_column] = code_challenge
|
720
|
+
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
721
|
+
elsif oauth_require_pkce
|
722
|
+
redirect_response_error("code_challenge_required")
|
723
|
+
end
|
425
724
|
end
|
426
725
|
|
427
726
|
ds = db[oauth_grants_table]
|
428
727
|
|
429
728
|
begin
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
ds.where(oauth_grants_id_column => id).get(oauth_grants_code_column)
|
435
|
-
end
|
729
|
+
authorization_code = oauth_unique_id_generator
|
730
|
+
create_params[oauth_grants_code_column] = authorization_code
|
731
|
+
ds.insert(create_params)
|
732
|
+
authorization_code
|
436
733
|
rescue Sequel::UniqueConstraintViolation
|
437
734
|
retry
|
438
735
|
end
|
@@ -440,155 +737,187 @@ module Rodauth
|
|
440
737
|
|
441
738
|
# Access Tokens
|
442
739
|
|
443
|
-
def
|
444
|
-
|
740
|
+
def before_token
|
741
|
+
require_oauth_application
|
742
|
+
end
|
445
743
|
|
446
|
-
|
744
|
+
def validate_oauth_token_params
|
745
|
+
unless (grant_type = param_or_nil("grant_type"))
|
447
746
|
redirect_response_error("invalid_request")
|
448
747
|
end
|
449
748
|
|
450
749
|
case grant_type
|
451
750
|
when "authorization_code"
|
452
|
-
redirect_response_error("invalid_request") unless
|
751
|
+
redirect_response_error("invalid_request") unless param_or_nil("code")
|
453
752
|
|
454
753
|
when "refresh_token"
|
455
|
-
redirect_response_error("invalid_request") unless
|
754
|
+
redirect_response_error("invalid_request") unless param_or_nil("refresh_token")
|
456
755
|
else
|
457
756
|
redirect_response_error("invalid_request")
|
458
757
|
end
|
459
758
|
end
|
460
759
|
|
461
|
-
def
|
760
|
+
def create_oauth_token
|
761
|
+
case param("grant_type")
|
762
|
+
when "authorization_code"
|
763
|
+
create_oauth_token_from_authorization_code(oauth_application)
|
764
|
+
when "refresh_token"
|
765
|
+
create_oauth_token_from_token(oauth_application)
|
766
|
+
else
|
767
|
+
redirect_response_error("invalid_grant")
|
768
|
+
end
|
769
|
+
end
|
770
|
+
|
771
|
+
def create_oauth_token_from_authorization_code(oauth_application)
|
772
|
+
# fetch oauth grant
|
773
|
+
oauth_grant = db[oauth_grants_table].where(
|
774
|
+
oauth_grants_code_column => param("code"),
|
775
|
+
oauth_grants_redirect_uri_column => param("redirect_uri"),
|
776
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
777
|
+
oauth_grants_revoked_at_column => nil
|
778
|
+
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
779
|
+
.for_update
|
780
|
+
.first
|
781
|
+
|
782
|
+
redirect_response_error("invalid_grant") unless oauth_grant
|
783
|
+
|
784
|
+
# PKCE
|
785
|
+
if use_oauth_pkce?
|
786
|
+
if oauth_grant[oauth_grants_code_challenge_column]
|
787
|
+
code_verifier = param_or_nil("code_verifier")
|
788
|
+
|
789
|
+
redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
|
790
|
+
elsif oauth_require_pkce
|
791
|
+
redirect_response_error("code_challenge_required")
|
792
|
+
end
|
793
|
+
end
|
794
|
+
|
462
795
|
create_params = {
|
463
|
-
|
464
|
-
|
465
|
-
|
796
|
+
oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
|
797
|
+
oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
|
798
|
+
oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
|
799
|
+
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
800
|
+
}
|
466
801
|
|
467
|
-
|
802
|
+
# revoke oauth grant
|
803
|
+
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
804
|
+
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
468
805
|
|
469
|
-
|
470
|
-
|
471
|
-
|
806
|
+
should_generate_refresh_token = !use_oauth_access_type? ||
|
807
|
+
oauth_grant[oauth_grants_access_type_column] == "offline"
|
808
|
+
|
809
|
+
generate_oauth_token(create_params, should_generate_refresh_token)
|
810
|
+
end
|
811
|
+
|
812
|
+
def create_oauth_token_from_token(oauth_application)
|
813
|
+
# fetch oauth token
|
814
|
+
oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
|
815
|
+
|
816
|
+
redirect_response_error("invalid_grant") unless oauth_token && token_from_application?(oauth_token, oauth_application)
|
817
|
+
|
818
|
+
token = oauth_unique_id_generator
|
819
|
+
|
820
|
+
update_params = {
|
821
|
+
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
822
|
+
oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
|
823
|
+
}
|
824
|
+
|
825
|
+
if oauth_tokens_token_hash_column
|
826
|
+
update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
827
|
+
else
|
828
|
+
update_params[oauth_tokens_token_column] = token
|
829
|
+
end
|
830
|
+
|
831
|
+
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
832
|
+
|
833
|
+
oauth_token = begin
|
834
|
+
if ds.supports_returning?(:update)
|
835
|
+
ds.returning.update(update_params).first
|
472
836
|
else
|
473
|
-
|
474
|
-
ds.
|
837
|
+
ds.update(update_params)
|
838
|
+
ds.first
|
475
839
|
end
|
476
|
-
|
477
|
-
|
840
|
+
rescue Sequel::UniqueConstraintViolation
|
841
|
+
retry
|
478
842
|
end
|
843
|
+
|
844
|
+
oauth_token[oauth_tokens_token_column] = token
|
845
|
+
oauth_token
|
479
846
|
end
|
480
847
|
|
481
|
-
|
482
|
-
case param(grant_type_param)
|
483
|
-
when "authorization_code"
|
484
|
-
# fetch oauth grant
|
485
|
-
oauth_grant = db[oauth_grants_table].where(
|
486
|
-
oauth_grants_code_column => param(code_param),
|
487
|
-
oauth_grants_redirect_uri_column => param(redirect_uri_param),
|
488
|
-
oauth_grants_oauth_application_id_column => db[oauth_applications_table].where(
|
489
|
-
oauth_applications_client_id_column => param(client_id_param),
|
490
|
-
oauth_applications_account_id_column => oauth_applications_account_id_column
|
491
|
-
).select(oauth_applications_id_column)
|
492
|
-
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
493
|
-
.where(oauth_grants_revoked_at_column => nil)
|
494
|
-
.first
|
495
|
-
|
496
|
-
redirect_response_error("invalid_grant") unless oauth_grant
|
497
|
-
|
498
|
-
create_params = {
|
499
|
-
oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
|
500
|
-
oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
|
501
|
-
oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
|
502
|
-
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
503
|
-
}
|
504
|
-
|
505
|
-
if oauth_grant[oauth_grants_access_type_column] == "offline"
|
506
|
-
create_params[oauth_tokens_refresh_token_column] = oauth_unique_id_generator
|
507
|
-
end
|
508
|
-
# revoke oauth grant
|
509
|
-
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
510
|
-
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
848
|
+
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
|
511
849
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
oauth_applications_client_id_column => param(client_id_param),
|
519
|
-
oauth_applications_account_id_column => account_id
|
520
|
-
).select(oauth_applications_id_column)
|
521
|
-
).where(oauth_grants_revoked_at_column => nil).first
|
522
|
-
|
523
|
-
redirect_response_error("invalid_grant") unless oauth_token
|
524
|
-
|
525
|
-
update_params = {
|
526
|
-
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
527
|
-
oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in,
|
528
|
-
oauth_tokens_token_column => oauth_unique_id_generator
|
529
|
-
}
|
530
|
-
|
531
|
-
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
532
|
-
begin
|
533
|
-
if ds.supports_returning?(:update)
|
534
|
-
ds.returning.update(update_params)
|
535
|
-
else
|
536
|
-
ds.update(update_params)
|
537
|
-
ds.first
|
538
|
-
end
|
539
|
-
rescue Sequel::UniqueConstraintViolation
|
540
|
-
retry
|
541
|
-
end
|
542
|
-
else
|
543
|
-
redirect_response_error("invalid_grant")
|
850
|
+
# Token introspect
|
851
|
+
|
852
|
+
def validate_oauth_introspect_params
|
853
|
+
# check if valid token hint type
|
854
|
+
if param_or_nil("token_type_hint")
|
855
|
+
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
544
856
|
end
|
857
|
+
|
858
|
+
redirect_response_error("invalid_request") unless param_or_nil("token")
|
545
859
|
end
|
546
860
|
|
861
|
+
def json_token_introspect_payload(token)
|
862
|
+
return { active: false } unless token
|
863
|
+
|
864
|
+
{
|
865
|
+
active: true,
|
866
|
+
scope: token[oauth_tokens_scopes_column],
|
867
|
+
client_id: oauth_application[oauth_applications_client_id_column],
|
868
|
+
# username
|
869
|
+
token_type: oauth_token_type
|
870
|
+
}
|
871
|
+
end
|
872
|
+
|
873
|
+
def before_introspect; end
|
874
|
+
|
547
875
|
# Token revocation
|
548
876
|
|
549
|
-
|
877
|
+
def before_revoke
|
878
|
+
require_oauth_application
|
879
|
+
end
|
550
880
|
|
551
881
|
def validate_oauth_revoke_params
|
552
882
|
# check if valid token hint type
|
553
|
-
|
883
|
+
if param_or_nil("token_type_hint")
|
884
|
+
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
885
|
+
end
|
554
886
|
|
555
|
-
redirect_response_error("invalid_request") unless
|
887
|
+
redirect_response_error("invalid_request") unless param_or_nil("token")
|
556
888
|
end
|
557
889
|
|
558
890
|
def revoke_oauth_token
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
|
567
|
-
oauth_applications_client_id_column => param(client_id_param),
|
568
|
-
oauth_applications_account_id_column => account_id
|
569
|
-
).select(oauth_applications_id_column)
|
570
|
-
)
|
571
|
-
)
|
572
|
-
ds = case token_type_hint
|
573
|
-
when "access_token"
|
574
|
-
ds.where(oauth_tokens_token_column => token)
|
575
|
-
when "refresh_token"
|
576
|
-
ds.where(oauth_tokens_refresh_token_column => token)
|
577
|
-
end
|
891
|
+
token = param("token")
|
892
|
+
|
893
|
+
oauth_token = if param("token_type_hint") == "refresh_token"
|
894
|
+
oauth_token_by_refresh_token(token)
|
895
|
+
else
|
896
|
+
oauth_token_by_token(token)
|
897
|
+
end
|
578
898
|
|
579
|
-
oauth_token = ds.first
|
580
899
|
redirect_response_error("invalid_request") unless oauth_token
|
581
900
|
|
901
|
+
if oauth_application
|
902
|
+
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
903
|
+
else
|
904
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
905
|
+
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
906
|
+
end
|
907
|
+
|
582
908
|
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
583
909
|
|
584
910
|
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
585
911
|
|
586
|
-
if ds.supports_returning?(:update)
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
912
|
+
oauth_token = if ds.supports_returning?(:update)
|
913
|
+
ds.returning.update(update_params).first
|
914
|
+
else
|
915
|
+
ds.update(update_params)
|
916
|
+
ds.first
|
917
|
+
end
|
918
|
+
|
919
|
+
oauth_token[oauth_tokens_token_column] = token
|
920
|
+
oauth_token
|
592
921
|
|
593
922
|
# If the particular
|
594
923
|
# token is a refresh token and the authorization server supports the
|
@@ -601,106 +930,229 @@ module Rodauth
|
|
601
930
|
|
602
931
|
# Response helpers
|
603
932
|
|
604
|
-
def redirect_response_error(error_code, redirect_url = request.referer || default_redirect)
|
605
|
-
if
|
933
|
+
def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
|
934
|
+
if accepts_json?
|
606
935
|
throw_json_response_error(invalid_oauth_response_status, error_code)
|
607
936
|
else
|
608
937
|
redirect_url = URI.parse(redirect_url)
|
609
|
-
query_params = [
|
938
|
+
query_params = []
|
939
|
+
|
940
|
+
query_params << if respond_to?(:"#{error_code}_error_code")
|
941
|
+
"error=#{send(:"#{error_code}_error_code")}"
|
942
|
+
else
|
943
|
+
"error=#{error_code}"
|
944
|
+
end
|
945
|
+
|
610
946
|
if respond_to?(:"#{error_code}_message")
|
611
947
|
message = send(:"#{error_code}_message")
|
612
948
|
query_params << ["error_description=#{CGI.escape(message)}"]
|
613
949
|
end
|
950
|
+
|
614
951
|
query_params << redirect_url.query if redirect_url.query
|
615
952
|
redirect_url.query = query_params.join("&")
|
616
953
|
redirect(redirect_url.to_s)
|
617
954
|
end
|
618
955
|
end
|
619
956
|
|
957
|
+
def json_response_success(body)
|
958
|
+
response.status = 200
|
959
|
+
response["Content-Type"] ||= json_response_content_type
|
960
|
+
json_payload = _json_response_body(body)
|
961
|
+
response.write(json_payload)
|
962
|
+
request.halt
|
963
|
+
end
|
964
|
+
|
620
965
|
def throw_json_response_error(status, error_code)
|
621
966
|
set_response_error_status(status)
|
622
|
-
|
967
|
+
code = if respond_to?(:"#{error_code}_error_code")
|
968
|
+
send(:"#{error_code}_error_code")
|
969
|
+
else
|
970
|
+
error_code
|
971
|
+
end
|
972
|
+
payload = { "error" => code }
|
623
973
|
payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
|
624
|
-
json_payload =
|
625
|
-
request.send(:convert_to_json, payload)
|
626
|
-
else
|
627
|
-
JSON.dump(payload)
|
628
|
-
end
|
974
|
+
json_payload = _json_response_body(payload)
|
629
975
|
response["Content-Type"] ||= json_response_content_type
|
630
|
-
response["WWW-Authenticate"] =
|
976
|
+
response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
|
631
977
|
response.write(json_payload)
|
632
978
|
request.halt
|
633
979
|
end
|
634
980
|
|
981
|
+
unless method_defined?(:_json_response_body)
|
982
|
+
# :nocov:
|
983
|
+
def _json_response_body(hash)
|
984
|
+
if request.respond_to?(:convert_to_json)
|
985
|
+
request.send(:convert_to_json, hash)
|
986
|
+
else
|
987
|
+
JSON.dump(hash)
|
988
|
+
end
|
989
|
+
end
|
990
|
+
# :nocov:
|
991
|
+
end
|
992
|
+
|
635
993
|
def authorization_required
|
636
|
-
if
|
994
|
+
if accepts_json?
|
637
995
|
throw_json_response_error(authorization_required_error_status, "invalid_client")
|
638
996
|
else
|
639
997
|
set_redirect_error_flash(require_authorization_error_flash)
|
640
|
-
redirect(
|
998
|
+
redirect(oauth_authorize_path)
|
641
999
|
end
|
642
1000
|
end
|
643
1001
|
|
1002
|
+
def check_valid_uri?(uri)
|
1003
|
+
URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
|
1004
|
+
end
|
1005
|
+
|
644
1006
|
def check_valid_scopes?
|
645
1007
|
return false unless scopes
|
646
1008
|
|
647
|
-
(scopes - oauth_application[oauth_applications_scopes_column].split(
|
1009
|
+
(scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
|
648
1010
|
end
|
649
1011
|
|
650
1012
|
def check_valid_redirect_uri?
|
651
|
-
|
1013
|
+
oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri)
|
652
1014
|
end
|
653
1015
|
|
654
1016
|
ACCESS_TYPES = %w[offline online].freeze
|
655
1017
|
|
656
1018
|
def check_valid_access_type?
|
657
|
-
|
658
|
-
|
1019
|
+
return true unless use_oauth_access_type?
|
1020
|
+
|
1021
|
+
access_type = param_or_nil("access_type")
|
1022
|
+
!access_type || ACCESS_TYPES.include?(access_type)
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
APPROVAL_PROMPTS = %w[force auto].freeze
|
1026
|
+
|
1027
|
+
def check_valid_approval_prompt?
|
1028
|
+
return true unless use_oauth_access_type?
|
1029
|
+
|
1030
|
+
approval_prompt = param_or_nil("approval_prompt")
|
1031
|
+
!approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
|
659
1032
|
end
|
660
1033
|
|
661
1034
|
def check_valid_response_type?
|
662
|
-
response_type =
|
1035
|
+
response_type = param_or_nil("response_type")
|
663
1036
|
|
664
|
-
return true if response_type.
|
1037
|
+
return true if response_type.nil? || response_type == "code"
|
665
1038
|
|
666
|
-
return use_oauth_implicit_grant_type if response_type == "token"
|
1039
|
+
return use_oauth_implicit_grant_type? if response_type == "token"
|
667
1040
|
|
668
1041
|
false
|
669
1042
|
end
|
670
1043
|
|
1044
|
+
# PKCE
|
1045
|
+
|
1046
|
+
def validate_pkce_challenge_params
|
1047
|
+
if param_or_nil("code_challenge")
|
1048
|
+
|
1049
|
+
challenge_method = param_or_nil("code_challenge_method")
|
1050
|
+
redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
|
1051
|
+
else
|
1052
|
+
return unless oauth_require_pkce
|
1053
|
+
|
1054
|
+
redirect_response_error("code_challenge_required")
|
1055
|
+
end
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
def check_valid_grant_challenge?(grant, verifier)
|
1059
|
+
challenge = grant[oauth_grants_code_challenge_column]
|
1060
|
+
|
1061
|
+
case grant[oauth_grants_code_challenge_method_column]
|
1062
|
+
when "plain"
|
1063
|
+
challenge == verifier
|
1064
|
+
when "S256"
|
1065
|
+
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
|
1066
|
+
generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
|
1067
|
+
|
1068
|
+
challenge == generated_challenge
|
1069
|
+
else
|
1070
|
+
redirect_response_error("unsupported_transform_algorithm")
|
1071
|
+
end
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
# Server metadata
|
1075
|
+
|
1076
|
+
def oauth_server_metadata_body(path)
|
1077
|
+
issuer = base_url
|
1078
|
+
issuer += "/#{path}" if issuer
|
1079
|
+
|
1080
|
+
responses_supported = %w[code]
|
1081
|
+
response_modes_supported = %w[query]
|
1082
|
+
grant_types_supported = %w[authorization_code]
|
1083
|
+
|
1084
|
+
if use_oauth_implicit_grant_type?
|
1085
|
+
responses_supported << "token"
|
1086
|
+
response_modes_supported << "fragment"
|
1087
|
+
grant_types_supported << "implicit"
|
1088
|
+
end
|
1089
|
+
{
|
1090
|
+
issuer: issuer,
|
1091
|
+
authorization_endpoint: oauth_authorize_url,
|
1092
|
+
token_endpoint: oauth_token_url,
|
1093
|
+
registration_endpoint: "#{base_url}/#{oauth_applications_path}",
|
1094
|
+
scopes_supported: oauth_application_scopes,
|
1095
|
+
response_types_supported: responses_supported,
|
1096
|
+
response_modes_supported: response_modes_supported,
|
1097
|
+
grant_types_supported: grant_types_supported,
|
1098
|
+
token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
|
1099
|
+
service_documentation: oauth_metadata_service_documentation,
|
1100
|
+
ui_locales_supported: oauth_metadata_ui_locales_supported,
|
1101
|
+
op_policy_uri: oauth_metadata_op_policy_uri,
|
1102
|
+
op_tos_uri: oauth_metadata_op_tos_uri,
|
1103
|
+
revocation_endpoint: oauth_revoke_url,
|
1104
|
+
revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
|
1105
|
+
introspection_endpoint: oauth_introspect_url,
|
1106
|
+
introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
|
1107
|
+
code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
|
1108
|
+
}
|
1109
|
+
end
|
1110
|
+
|
671
1111
|
# /oauth-token
|
672
1112
|
route(:oauth_token) do |r|
|
673
|
-
|
1113
|
+
before_token
|
674
1114
|
|
675
|
-
# access-token
|
676
1115
|
r.post do
|
677
1116
|
catch_error do
|
678
1117
|
validate_oauth_token_params
|
679
1118
|
|
680
1119
|
oauth_token = nil
|
681
1120
|
transaction do
|
682
|
-
before_token
|
683
1121
|
oauth_token = create_oauth_token
|
684
|
-
after_token
|
685
1122
|
end
|
686
1123
|
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
1124
|
+
json_response_success(json_access_token_payload(oauth_token))
|
1125
|
+
end
|
1126
|
+
|
1127
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
1128
|
+
end
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
# /oauth-introspect
|
1132
|
+
route(:oauth_introspect) do |r|
|
1133
|
+
before_introspect
|
1134
|
+
|
1135
|
+
r.post do
|
1136
|
+
catch_error do
|
1137
|
+
validate_oauth_introspect_params
|
1138
|
+
|
1139
|
+
oauth_token = case param("token_type_hint")
|
1140
|
+
when "access_token"
|
1141
|
+
oauth_token_by_token(param("token"))
|
1142
|
+
when "refresh_token"
|
1143
|
+
oauth_token_by_refresh_token(param("token"))
|
1144
|
+
else
|
1145
|
+
oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
if oauth_application
|
1149
|
+
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
1150
|
+
elsif oauth_token
|
1151
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
1152
|
+
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
json_response_success(json_token_introspect_payload(oauth_token))
|
704
1156
|
end
|
705
1157
|
|
706
1158
|
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
@@ -709,80 +1161,67 @@ module Rodauth
|
|
709
1161
|
|
710
1162
|
# /oauth-revoke
|
711
1163
|
route(:oauth_revoke) do |r|
|
712
|
-
|
1164
|
+
before_revoke
|
713
1165
|
|
714
|
-
# access-token
|
715
1166
|
r.post do
|
716
1167
|
catch_error do
|
717
1168
|
validate_oauth_revoke_params
|
718
1169
|
|
719
1170
|
oauth_token = nil
|
720
1171
|
transaction do
|
721
|
-
before_revoke
|
722
1172
|
oauth_token = revoke_oauth_token
|
723
1173
|
after_revoke
|
724
1174
|
end
|
725
1175
|
|
726
|
-
if
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
"
|
731
|
-
"refresh_token" => oauth_token[:refresh_token],
|
732
|
-
"revoked_at" => oauth_token[:revoked_at]
|
733
|
-
}
|
734
|
-
json_payload = if request.respond_to?(:convert_to_json)
|
735
|
-
request.send(:convert_to_json, json_response)
|
736
|
-
else
|
737
|
-
JSON.dump(json_response)
|
738
|
-
end
|
739
|
-
response.write(json_payload)
|
740
|
-
request.halt
|
1176
|
+
if accepts_json?
|
1177
|
+
json_response_success \
|
1178
|
+
"token" => oauth_token[oauth_tokens_token_column],
|
1179
|
+
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
1180
|
+
"revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
|
741
1181
|
else
|
742
1182
|
set_notice_flash revoke_oauth_token_notice_flash
|
743
1183
|
redirect request.referer || "/"
|
744
1184
|
end
|
745
1185
|
end
|
746
1186
|
|
747
|
-
|
1187
|
+
redirect_response_error("invalid_request", request.referer || "/")
|
748
1188
|
end
|
749
1189
|
end
|
750
1190
|
|
751
1191
|
# /oauth-authorize
|
752
1192
|
route(:oauth_authorize) do |r|
|
753
1193
|
require_account
|
1194
|
+
validate_oauth_grant_params
|
1195
|
+
try_approval_prompt if use_oauth_access_type? && request.get?
|
1196
|
+
|
1197
|
+
before_authorize
|
754
1198
|
|
755
1199
|
r.get do
|
756
|
-
validate_oauth_grant_params
|
757
1200
|
authorize_view
|
758
1201
|
end
|
759
1202
|
|
760
1203
|
r.post do
|
761
|
-
validate_oauth_grant_params
|
762
|
-
|
763
1204
|
code = nil
|
764
1205
|
query_params = []
|
765
1206
|
fragment_params = []
|
766
1207
|
|
767
1208
|
transaction do
|
768
|
-
|
769
|
-
case param(response_type_param)
|
1209
|
+
case param("response_type")
|
770
1210
|
when "token"
|
771
|
-
redirect_response_error("invalid_request"
|
1211
|
+
redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
|
772
1212
|
|
773
1213
|
create_params = {
|
774
1214
|
oauth_tokens_account_id_column => account_id,
|
775
1215
|
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
776
1216
|
oauth_tokens_scopes_column => scopes
|
777
1217
|
}
|
778
|
-
oauth_token = generate_oauth_token(create_params)
|
1218
|
+
oauth_token = generate_oauth_token(create_params, false)
|
779
1219
|
|
780
|
-
|
781
|
-
fragment_params
|
782
|
-
fragment_params << ["expires_in=#{oauth_token_expires_in}"]
|
1220
|
+
token_payload = json_access_token_payload(oauth_token)
|
1221
|
+
fragment_params.replace(token_payload.map { |k, v| "#{k}=#{v}" })
|
783
1222
|
when "code", "", nil
|
784
1223
|
code = create_oauth_grant
|
785
|
-
query_params <<
|
1224
|
+
query_params << "code=#{code}"
|
786
1225
|
else
|
787
1226
|
redirect_response_error("invalid_request")
|
788
1227
|
end
|
@@ -790,7 +1229,7 @@ module Rodauth
|
|
790
1229
|
end
|
791
1230
|
|
792
1231
|
redirect_url = URI.parse(redirect_uri)
|
793
|
-
query_params << "state=#{state}" if state
|
1232
|
+
query_params << "state=#{param('state')}" if param_or_nil("state")
|
794
1233
|
query_params << redirect_url.query if redirect_url.query
|
795
1234
|
redirect_url.query = query_params.join("&") unless query_params.empty?
|
796
1235
|
redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
|