rodauth-oauth 0.10.4 → 1.0.0.pre.beta1
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/MIGRATION-GUIDE-v1.md +286 -0
- data/README.md +22 -30
- data/doc/release_notes/1_0_0_beta1.md +38 -0
- data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +4 -6
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +18 -29
- data/lib/rodauth/features/oauth_application_management.rb +59 -72
- data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +35 -88
- data/lib/rodauth/features/oauth_authorize_base.rb +103 -20
- data/lib/rodauth/features/oauth_base.rb +365 -302
- data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
- data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +46 -28
- data/lib/rodauth/features/oauth_grant_management.rb +70 -0
- data/lib/rodauth/features/oauth_implicit_grant.rb +25 -24
- data/lib/rodauth/features/oauth_jwt.rb +52 -688
- data/lib/rodauth/features/oauth_jwt_base.rb +435 -0
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +45 -17
- data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +62 -0
- data/lib/rodauth/features/oauth_management_base.rb +2 -0
- data/lib/rodauth/features/oauth_pkce.rb +22 -26
- data/lib/rodauth/features/oauth_resource_indicators.rb +33 -21
- data/lib/rodauth/features/oauth_resource_server.rb +59 -0
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +5 -1
- data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
- data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
- data/lib/rodauth/features/oidc.rb +188 -95
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +89 -53
- data/lib/rodauth/oauth/database_extensions.rb +8 -6
- data/lib/rodauth/oauth/http_extensions.rb +61 -0
- data/lib/rodauth/oauth/railtie.rb +20 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/lib/rodauth/oauth.rb +29 -1
- data/locales/en.yml +32 -22
- data/locales/pt.yml +32 -22
- data/templates/authorize.str +19 -24
- data/templates/device_search.str +1 -1
- data/templates/device_verification.str +2 -2
- data/templates/jwks_field.str +1 -0
- data/templates/new_oauth_application.str +1 -2
- data/templates/oauth_application.str +2 -2
- data/templates/oauth_application_oauth_grants.str +54 -0
- data/templates/oauth_applications.str +2 -2
- data/templates/oauth_grants.str +52 -0
- metadata +20 -16
- data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
- data/lib/rodauth/features/oauth.rb +0 -9
- data/lib/rodauth/features/oauth_http_mac.rb +0 -86
- data/lib/rodauth/features/oauth_token_management.rb +0 -81
- data/lib/rodauth/oauth/refinements.rb +0 -48
- data/templates/jwt_public_key_field.str +0 -4
- data/templates/oauth_application_oauth_tokens.str +0 -52
- data/templates/oauth_tokens.str +0 -50
@@ -3,16 +3,19 @@
|
|
3
3
|
require "time"
|
4
4
|
require "base64"
|
5
5
|
require "securerandom"
|
6
|
-
require "net/http"
|
7
6
|
require "rodauth/version"
|
8
|
-
require "rodauth/oauth
|
9
|
-
require "rodauth/oauth/ttl_store"
|
7
|
+
require "rodauth/oauth"
|
10
8
|
require "rodauth/oauth/database_extensions"
|
11
|
-
require "rodauth/oauth/
|
9
|
+
require "rodauth/oauth/http_extensions"
|
12
10
|
|
13
11
|
module Rodauth
|
14
12
|
Feature.define(:oauth_base, :OauthBase) do
|
15
|
-
|
13
|
+
include OAuth::HTTPExtensions
|
14
|
+
|
15
|
+
EMPTY_HASH = {}.freeze
|
16
|
+
|
17
|
+
auth_value_methods(:http_request)
|
18
|
+
auth_value_methods(:http_request_cache)
|
16
19
|
|
17
20
|
SCOPES = %w[profile.read].freeze
|
18
21
|
|
@@ -26,41 +29,33 @@ module Rodauth
|
|
26
29
|
auth_value_method :json_response_content_type, "application/json"
|
27
30
|
|
28
31
|
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
|
29
|
-
auth_value_method :
|
32
|
+
auth_value_method :oauth_access_token_expires_in, 60 * 60 # 60 minutes
|
30
33
|
auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
|
31
34
|
auth_value_method :oauth_unique_id_generation_retries, 3
|
32
35
|
|
33
|
-
auth_value_method :
|
34
|
-
auth_value_method :
|
36
|
+
auth_value_method :oauth_token_endpoint_auth_methods_supported, %w[client_secret_basic client_secret_post]
|
37
|
+
auth_value_method :oauth_grant_types_supported, %w[refresh_token]
|
38
|
+
auth_value_method :oauth_response_types_supported, []
|
39
|
+
auth_value_method :oauth_response_modes_supported, []
|
35
40
|
|
36
41
|
auth_value_method :oauth_valid_uri_schemes, %w[https]
|
37
42
|
auth_value_method :oauth_scope_separator, " "
|
38
43
|
|
39
|
-
auth_value_method :oauth_tokens_table, :oauth_tokens
|
40
|
-
auth_value_method :oauth_tokens_id_column, :id
|
41
|
-
|
42
|
-
%i[
|
43
|
-
oauth_application_id oauth_token_id oauth_grant_id account_id
|
44
|
-
token refresh_token scopes
|
45
|
-
expires_in revoked_at
|
46
|
-
].each do |column|
|
47
|
-
auth_value_method :"oauth_tokens_#{column}_column", column
|
48
|
-
end
|
49
|
-
|
50
44
|
# OAuth Grants
|
51
45
|
auth_value_method :oauth_grants_table, :oauth_grants
|
52
46
|
auth_value_method :oauth_grants_id_column, :id
|
53
47
|
%i[
|
54
|
-
account_id oauth_application_id
|
55
|
-
redirect_uri code scopes
|
48
|
+
account_id oauth_application_id type
|
49
|
+
redirect_uri code scopes
|
56
50
|
expires_in revoked_at
|
51
|
+
token refresh_token
|
57
52
|
].each do |column|
|
58
53
|
auth_value_method :"oauth_grants_#{column}_column", column
|
59
54
|
end
|
60
55
|
|
61
|
-
#
|
62
|
-
auth_value_method :
|
63
|
-
auth_value_method :
|
56
|
+
# Enables Token Hash
|
57
|
+
auth_value_method :oauth_grants_token_hash_column, :token
|
58
|
+
auth_value_method :oauth_grants_refresh_token_hash_column, :refresh_token
|
64
59
|
|
65
60
|
# Access Token reuse
|
66
61
|
auth_value_method :oauth_reuse_access_token, false
|
@@ -73,36 +68,34 @@ module Rodauth
|
|
73
68
|
name description scopes
|
74
69
|
client_id client_secret
|
75
70
|
homepage_url redirect_uri
|
76
|
-
token_endpoint_auth_method grant_types response_types
|
71
|
+
token_endpoint_auth_method grant_types response_types response_modes
|
77
72
|
logo_uri tos_uri policy_uri jwks jwks_uri
|
78
73
|
contacts software_id software_version
|
79
74
|
].each do |column|
|
80
75
|
auth_value_method :"oauth_applications_#{column}_column", column
|
81
76
|
end
|
77
|
+
# Enables client secret Hash
|
78
|
+
auth_value_method :oauth_applications_client_secret_hash_column, :client_secret
|
82
79
|
|
83
|
-
auth_value_method :
|
84
|
-
auth_value_method :
|
85
|
-
auth_value_method :
|
80
|
+
auth_value_method :oauth_authorization_required_error_status, 401
|
81
|
+
auth_value_method :oauth_invalid_response_status, 400
|
82
|
+
auth_value_method :oauth_already_in_use_response_status, 409
|
86
83
|
|
87
84
|
# Feature options
|
88
|
-
auth_value_method :oauth_application_default_scope, SCOPES.first
|
89
85
|
auth_value_method :oauth_application_scopes, SCOPES
|
90
86
|
auth_value_method :oauth_token_type, "bearer"
|
91
|
-
auth_value_method :oauth_refresh_token_protection_policy, "
|
87
|
+
auth_value_method :oauth_refresh_token_protection_policy, "rotation" # can be: none, sender_constrained, rotation
|
92
88
|
|
93
|
-
translatable_method :
|
94
|
-
translatable_method :
|
95
|
-
translatable_method :
|
96
|
-
translatable_method :
|
97
|
-
translatable_method :
|
89
|
+
translatable_method :oauth_invalid_client_message, "Invalid client"
|
90
|
+
translatable_method :oauth_invalid_grant_type_message, "Invalid grant type"
|
91
|
+
translatable_method :oauth_invalid_grant_message, "Invalid grant"
|
92
|
+
translatable_method :oauth_invalid_scope_message, "Invalid scope"
|
93
|
+
translatable_method :oauth_unsupported_token_type_message, "Invalid token type hint"
|
98
94
|
|
99
|
-
translatable_method :
|
100
|
-
|
101
|
-
auth_value_method :
|
102
|
-
auth_value_method :invalid_grant_type_error_code, "unsupported_grant_type"
|
95
|
+
translatable_method :oauth_already_in_use_message, "error generating unique token"
|
96
|
+
auth_value_method :oauth_already_in_use_error_code, "invalid_request"
|
97
|
+
auth_value_method :oauth_invalid_grant_type_error_code, "unsupported_grant_type"
|
103
98
|
|
104
|
-
# Resource Server params
|
105
|
-
# Only required to use if the plugin is to be used in a resource server
|
106
99
|
auth_value_method :is_authorization_server?, true
|
107
100
|
|
108
101
|
auth_value_methods(:only_json?)
|
@@ -122,41 +115,39 @@ module Rodauth
|
|
122
115
|
:secret_matches?,
|
123
116
|
:authorization_server_url,
|
124
117
|
:oauth_unique_id_generator,
|
125
|
-
:
|
126
|
-
:require_authorizable_account
|
118
|
+
:oauth_grants_unique_columns,
|
119
|
+
:require_authorizable_account,
|
120
|
+
:oauth_account_ds,
|
121
|
+
:oauth_application_ds
|
127
122
|
)
|
128
123
|
|
129
124
|
# /token
|
130
|
-
|
131
|
-
next unless is_authorization_server?
|
132
|
-
|
133
|
-
before_token_route
|
125
|
+
auth_server_route(:token) do |r|
|
134
126
|
require_oauth_application
|
127
|
+
before_token_route
|
135
128
|
|
136
129
|
r.post do
|
137
130
|
catch_error do
|
138
|
-
|
131
|
+
validate_token_params
|
139
132
|
|
140
|
-
|
133
|
+
oauth_grant = nil
|
141
134
|
|
142
135
|
transaction do
|
143
136
|
before_token
|
144
|
-
|
137
|
+
oauth_grant = create_token(param("grant_type"))
|
145
138
|
end
|
146
139
|
|
147
|
-
json_response_success(json_access_token_payload(
|
140
|
+
json_response_success(json_access_token_payload(oauth_grant))
|
148
141
|
end
|
149
142
|
|
150
|
-
throw_json_response_error(
|
143
|
+
throw_json_response_error(oauth_invalid_response_status, "invalid_request")
|
151
144
|
end
|
152
145
|
end
|
153
146
|
|
154
|
-
def
|
147
|
+
def load_oauth_server_metadata_route(issuer = nil)
|
155
148
|
request.on(".well-known") do
|
156
|
-
request.
|
157
|
-
|
158
|
-
json_response_success(oauth_server_metadata_body(issuer), true)
|
159
|
-
end
|
149
|
+
request.get("oauth-authorization-server") do
|
150
|
+
json_response_success(oauth_server_metadata_body(issuer), true)
|
160
151
|
end
|
161
152
|
end
|
162
153
|
end
|
@@ -170,18 +161,25 @@ module Rodauth
|
|
170
161
|
end
|
171
162
|
end
|
172
163
|
|
173
|
-
# Overrides session_value, so that a valid authorization token also authenticates a request
|
174
|
-
# TODO: deprecate
|
175
|
-
def session_value
|
176
|
-
super || oauth_token_subject
|
177
|
-
end
|
178
|
-
|
179
164
|
def oauth_token_subject
|
180
165
|
return unless authorization_token
|
181
166
|
|
182
|
-
|
183
|
-
|
184
|
-
|
167
|
+
authorization_token[oauth_grants_account_id_column] ||
|
168
|
+
db[oauth_applications_table].where(
|
169
|
+
oauth_applications_id_column => authorization_token[oauth_grants_oauth_application_id_column]
|
170
|
+
).select_map(oauth_applications_client_id_column).first
|
171
|
+
end
|
172
|
+
|
173
|
+
def current_oauth_account
|
174
|
+
account_id = authorization_token[oauth_grants_account_id_column]
|
175
|
+
|
176
|
+
return unless account_id
|
177
|
+
|
178
|
+
oauth_account_ds(account_id).first
|
179
|
+
end
|
180
|
+
|
181
|
+
def current_oauth_application
|
182
|
+
oauth_application_ds(authorization_token[oauth_grants_oauth_application_id_column]).first
|
185
183
|
end
|
186
184
|
|
187
185
|
def accepts_json?
|
@@ -190,13 +188,12 @@ module Rodauth
|
|
190
188
|
(accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
|
191
189
|
end
|
192
190
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
191
|
+
# copied from the jwt feature
|
192
|
+
def json_request?
|
193
|
+
return super if features.include?(:jsonn)
|
194
|
+
return @json_request if defined?(@json_request)
|
197
195
|
|
198
|
-
|
199
|
-
end
|
196
|
+
@json_request = request.content_type =~ json_request_regexp
|
200
197
|
end
|
201
198
|
|
202
199
|
def scopes
|
@@ -206,8 +203,6 @@ module Rodauth
|
|
206
203
|
scope
|
207
204
|
when String
|
208
205
|
scope.split(" ")
|
209
|
-
when nil
|
210
|
-
Array(oauth_application_default_scope)
|
211
206
|
end
|
212
207
|
end
|
213
208
|
|
@@ -254,35 +249,13 @@ module Rodauth
|
|
254
249
|
|
255
250
|
return unless bearer_token
|
256
251
|
|
257
|
-
@authorization_token =
|
258
|
-
# check if token has not expired
|
259
|
-
# check if token has been revoked
|
260
|
-
oauth_token_by_token(bearer_token)
|
261
|
-
else
|
262
|
-
# where in resource server, NOT the authorization server.
|
263
|
-
payload = introspection_request("access_token", bearer_token)
|
264
|
-
|
265
|
-
return unless payload["active"]
|
266
|
-
|
267
|
-
payload
|
268
|
-
end
|
252
|
+
@authorization_token = oauth_grant_by_token(bearer_token)
|
269
253
|
end
|
270
254
|
|
271
255
|
def require_oauth_authorization(*scopes)
|
272
256
|
authorization_required unless authorization_token
|
273
257
|
|
274
|
-
|
275
|
-
|
276
|
-
token_scopes = if is_authorization_server?
|
277
|
-
authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
|
278
|
-
else
|
279
|
-
aux_scopes = authorization_token["scope"]
|
280
|
-
if aux_scopes
|
281
|
-
aux_scopes.split(oauth_scope_separator)
|
282
|
-
else
|
283
|
-
[]
|
284
|
-
end
|
285
|
-
end
|
258
|
+
token_scopes = authorization_token[oauth_grants_scopes_column].split(oauth_scope_separator)
|
286
259
|
|
287
260
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
288
261
|
end
|
@@ -291,9 +264,20 @@ module Rodauth
|
|
291
264
|
true
|
292
265
|
end
|
293
266
|
|
267
|
+
# override
|
268
|
+
def translate(key, default, args = EMPTY_HASH)
|
269
|
+
return i18n_translate(key, default, **args) if features.include?(:i18n)
|
270
|
+
# do not attempt to translate by default
|
271
|
+
return default if args.nil?
|
272
|
+
|
273
|
+
default % args
|
274
|
+
end
|
275
|
+
|
294
276
|
def post_configure
|
295
277
|
super
|
296
278
|
|
279
|
+
i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
|
280
|
+
|
297
281
|
# all of the extensions below involve DB changes. Resource server mode doesn't use
|
298
282
|
# database functions for OAuth though.
|
299
283
|
return unless is_authorization_server?
|
@@ -301,18 +285,24 @@ module Rodauth
|
|
301
285
|
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
|
302
286
|
|
303
287
|
# Check whether we can reutilize db entries for the same account / application pair
|
304
|
-
one_oauth_token_per_account = db.indexes(
|
288
|
+
one_oauth_token_per_account = db.indexes(oauth_grants_table).values.any? do |definition|
|
305
289
|
definition[:unique] &&
|
306
|
-
definition[:columns] ==
|
290
|
+
definition[:columns] == oauth_grants_unique_columns
|
307
291
|
end
|
308
292
|
|
309
293
|
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
|
310
|
-
|
311
|
-
i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
|
312
294
|
end
|
313
295
|
|
314
296
|
private
|
315
297
|
|
298
|
+
def oauth_account_ds(account_id)
|
299
|
+
account_ds(account_id)
|
300
|
+
end
|
301
|
+
|
302
|
+
def oauth_application_ds(oauth_application_id)
|
303
|
+
db[oauth_applications_table].where(oauth_applications_id_column => oauth_application_id)
|
304
|
+
end
|
305
|
+
|
316
306
|
def require_authorizable_account
|
317
307
|
require_account
|
318
308
|
end
|
@@ -329,11 +319,11 @@ module Rodauth
|
|
329
319
|
end
|
330
320
|
|
331
321
|
# OAuth Token Unique/Reuse
|
332
|
-
def
|
322
|
+
def oauth_grants_unique_columns
|
333
323
|
[
|
334
|
-
|
335
|
-
|
336
|
-
|
324
|
+
oauth_grants_oauth_application_id_column,
|
325
|
+
oauth_grants_account_id_column,
|
326
|
+
oauth_grants_scopes_column
|
337
327
|
]
|
338
328
|
end
|
339
329
|
|
@@ -353,53 +343,59 @@ module Rodauth
|
|
353
343
|
# parse client id and secret
|
354
344
|
#
|
355
345
|
def require_oauth_application
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
346
|
+
@oauth_application = if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
|
347
|
+
# client_secret_basic
|
348
|
+
require_oauth_application_from_client_secret_basic(token)
|
349
|
+
elsif (client_id = param_or_nil("client_id"))
|
350
|
+
if (client_secret = param_or_nil("client_secret"))
|
351
|
+
# client_secret_post
|
352
|
+
require_oauth_application_from_client_secret_post(client_id, client_secret)
|
353
|
+
else
|
354
|
+
# none
|
355
|
+
require_oauth_application_from_none(client_id)
|
356
|
+
end
|
357
|
+
else
|
358
|
+
authorization_required
|
359
|
+
end
|
360
|
+
end
|
370
361
|
|
362
|
+
def require_oauth_application_from_client_secret_basic(token)
|
363
|
+
client_id, client_secret = Base64.decode64(token).split(/:/, 2)
|
371
364
|
authorization_required unless client_id
|
365
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
366
|
+
authorization_required unless supports_auth_method?(oauth_application,
|
367
|
+
"client_secret_basic") && secret_matches?(oauth_application, client_secret)
|
368
|
+
oauth_application
|
369
|
+
end
|
372
370
|
|
373
|
-
|
374
|
-
|
375
|
-
authorization_required unless
|
371
|
+
def require_oauth_application_from_client_secret_post(client_id, client_secret)
|
372
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
373
|
+
authorization_required unless supports_auth_method?(oauth_application,
|
374
|
+
"client_secret_post") && secret_matches?(oauth_application, client_secret)
|
375
|
+
oauth_application
|
376
|
+
end
|
376
377
|
|
377
|
-
|
378
|
+
def require_oauth_application_from_none(client_id)
|
379
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
380
|
+
authorization_required unless supports_auth_method?(oauth_application, "none")
|
381
|
+
oauth_application
|
378
382
|
end
|
379
383
|
|
380
|
-
def
|
384
|
+
def supports_auth_method?(oauth_application, auth_method)
|
381
385
|
supported_auth_methods = if oauth_application[oauth_applications_token_endpoint_auth_method_column]
|
382
386
|
oauth_application[oauth_applications_token_endpoint_auth_method_column].split(/ +/)
|
383
387
|
else
|
384
|
-
|
388
|
+
oauth_token_endpoint_auth_methods_supported
|
385
389
|
end
|
386
390
|
|
387
|
-
|
388
|
-
supported_auth_methods.include?(auth_method) && secret_matches?(oauth_application, client_secret)
|
389
|
-
else
|
390
|
-
supported_auth_methods.include?("none")
|
391
|
-
end
|
392
|
-
end
|
393
|
-
|
394
|
-
def no_auth_oauth_application?(_oauth_application)
|
395
|
-
supported_auth_methods.include?("none")
|
391
|
+
supported_auth_methods.include?(auth_method)
|
396
392
|
end
|
397
393
|
|
398
394
|
def require_oauth_application_from_account
|
399
395
|
ds = db[oauth_applications_table]
|
400
|
-
.join(
|
396
|
+
.join(oauth_grants_table, Sequel[oauth_grants_table][oauth_grants_oauth_application_id_column] =>
|
401
397
|
Sequel[oauth_applications_table][oauth_applications_id_column])
|
402
|
-
.where(
|
398
|
+
.where(oauth_grant_by_token_ds(param("token")).opts.fetch(:where, true))
|
403
399
|
.where(Sequel[oauth_applications_table][oauth_applications_account_id_column] => account_id)
|
404
400
|
|
405
401
|
@oauth_application = ds.qualify.first
|
@@ -410,7 +406,19 @@ module Rodauth
|
|
410
406
|
end
|
411
407
|
|
412
408
|
def secret_matches?(oauth_application, secret)
|
413
|
-
|
409
|
+
if oauth_applications_client_secret_hash_column
|
410
|
+
BCrypt::Password.new(oauth_application[oauth_applications_client_secret_hash_column]) == secret
|
411
|
+
else
|
412
|
+
oauth_application[oauth_applications_client_secret_column] == secret
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def set_client_secret(params, secret)
|
417
|
+
if oauth_applications_client_secret_hash_column
|
418
|
+
params[oauth_applications_client_secret_hash_column] = secret_hash(secret)
|
419
|
+
else
|
420
|
+
params[oauth_applications_client_secret_column] = secret
|
421
|
+
end
|
414
422
|
end
|
415
423
|
|
416
424
|
def secret_hash(secret)
|
@@ -425,45 +433,53 @@ module Rodauth
|
|
425
433
|
Base64.urlsafe_encode64(Digest::SHA256.digest(token))
|
426
434
|
end
|
427
435
|
|
428
|
-
def
|
429
|
-
|
436
|
+
def grant_from_application?(oauth_grant, oauth_application)
|
437
|
+
oauth_grant[oauth_grants_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
|
430
438
|
end
|
431
439
|
|
432
|
-
|
433
|
-
|
440
|
+
def password_hash(password)
|
441
|
+
return super if features.include?(:login_password_requirements_base)
|
434
442
|
|
435
|
-
|
436
|
-
BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
|
437
|
-
end
|
443
|
+
BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
|
438
444
|
end
|
439
445
|
|
440
|
-
def
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
446
|
+
def generate_token(grant_params = {}, should_generate_refresh_token = true)
|
447
|
+
if grant_params[oauth_grants_id_column] && (oauth_reuse_access_token &&
|
448
|
+
(
|
449
|
+
if oauth_grants_token_hash_column
|
450
|
+
grant_params[oauth_grants_token_hash_column]
|
451
|
+
else
|
452
|
+
grant_params[oauth_grants_token_column]
|
453
|
+
end
|
454
|
+
))
|
455
|
+
return grant_params
|
448
456
|
end
|
449
457
|
|
458
|
+
update_params = {
|
459
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in),
|
460
|
+
oauth_grants_code_column => nil
|
461
|
+
}
|
462
|
+
|
450
463
|
rescue_from_uniqueness_error do
|
451
|
-
access_token = _generate_access_token(
|
452
|
-
refresh_token = _generate_refresh_token(
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
464
|
+
access_token = _generate_access_token(update_params)
|
465
|
+
refresh_token = _generate_refresh_token(update_params) if should_generate_refresh_token
|
466
|
+
oauth_grant = store_token(grant_params, update_params)
|
467
|
+
|
468
|
+
return unless oauth_grant
|
469
|
+
|
470
|
+
oauth_grant[oauth_grants_token_column] = access_token
|
471
|
+
oauth_grant[oauth_grants_refresh_token_column] = refresh_token if refresh_token
|
472
|
+
oauth_grant
|
457
473
|
end
|
458
474
|
end
|
459
475
|
|
460
476
|
def _generate_access_token(params = {})
|
461
477
|
token = oauth_unique_id_generator
|
462
478
|
|
463
|
-
if
|
464
|
-
params[
|
479
|
+
if oauth_grants_token_hash_column
|
480
|
+
params[oauth_grants_token_hash_column] = generate_token_hash(token)
|
465
481
|
else
|
466
|
-
params[
|
482
|
+
params[oauth_grants_token_column] = token
|
467
483
|
end
|
468
484
|
|
469
485
|
token
|
@@ -472,96 +488,154 @@ module Rodauth
|
|
472
488
|
def _generate_refresh_token(params)
|
473
489
|
token = oauth_unique_id_generator
|
474
490
|
|
475
|
-
if
|
476
|
-
params[
|
491
|
+
if oauth_grants_refresh_token_hash_column
|
492
|
+
params[oauth_grants_refresh_token_hash_column] = generate_token_hash(token)
|
477
493
|
else
|
478
|
-
params[
|
494
|
+
params[oauth_grants_refresh_token_column] = token
|
479
495
|
end
|
480
496
|
|
481
497
|
token
|
482
498
|
end
|
483
499
|
|
484
|
-
def
|
485
|
-
|
500
|
+
def _grant_with_access_token?(oauth_grant)
|
501
|
+
if oauth_grants_token_hash_column
|
502
|
+
oauth_grant[oauth_grants_token_hash_column]
|
503
|
+
else
|
504
|
+
oauth_grant[oauth_grants_token_column]
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
def store_token(grant_params, update_params = {})
|
509
|
+
ds = db[oauth_grants_table]
|
486
510
|
|
487
511
|
if __one_oauth_token_per_account
|
488
512
|
|
513
|
+
to_update_if_null = [
|
514
|
+
oauth_grants_token_column,
|
515
|
+
oauth_grants_token_hash_column,
|
516
|
+
oauth_grants_refresh_token_column,
|
517
|
+
oauth_grants_refresh_token_hash_column
|
518
|
+
].compact.map do |attribute|
|
519
|
+
[
|
520
|
+
attribute,
|
521
|
+
(
|
522
|
+
if ds.respond_to?(:supports_insert_conflict?) && ds.supports_insert_conflict?
|
523
|
+
Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], Sequel[:excluded][attribute])
|
524
|
+
else
|
525
|
+
Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], update_params[attribute])
|
526
|
+
end
|
527
|
+
)
|
528
|
+
]
|
529
|
+
end
|
530
|
+
|
489
531
|
token = __insert_or_update_and_return__(
|
490
532
|
ds,
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
Sequel.expr(Sequel[
|
495
|
-
|
533
|
+
oauth_grants_id_column,
|
534
|
+
oauth_grants_unique_columns,
|
535
|
+
grant_params.merge(update_params),
|
536
|
+
Sequel.expr(Sequel[oauth_grants_table][oauth_grants_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
|
537
|
+
Hash[to_update_if_null]
|
496
538
|
)
|
497
539
|
|
498
540
|
# if the previous operation didn't return a row, it means that the conditions
|
499
541
|
# invalidated the update, and the existing token is still valid.
|
500
542
|
token || ds.where(
|
501
|
-
|
502
|
-
|
543
|
+
oauth_grants_account_id_column => update_params[oauth_grants_account_id_column],
|
544
|
+
oauth_grants_oauth_application_id_column => update_params[oauth_grants_oauth_application_id_column]
|
503
545
|
).first
|
504
546
|
else
|
547
|
+
|
505
548
|
if oauth_reuse_access_token
|
506
|
-
unique_conds = Hash[
|
507
|
-
|
508
|
-
|
549
|
+
unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, update_params[column]] }]
|
550
|
+
valid_token_ds = valid_oauth_grant_ds(unique_conds)
|
551
|
+
if oauth_grants_token_hash_column
|
552
|
+
valid_token_ds.exclude(oauth_grants_token_hash_column => nil)
|
553
|
+
else
|
554
|
+
valid_token_ds.exclude(oauth_grants_token_column => nil)
|
555
|
+
end
|
556
|
+
|
557
|
+
valid_token = valid_token_ds.first
|
558
|
+
|
509
559
|
return valid_token if valid_token
|
510
560
|
end
|
511
|
-
|
561
|
+
|
562
|
+
if grant_params[oauth_grants_id_column]
|
563
|
+
__update_and_return__(ds.where(oauth_grants_id_column => grant_params[oauth_grants_id_column]), update_params)
|
564
|
+
else
|
565
|
+
__insert_and_return__(ds, oauth_grants_id_column, grant_params.merge(update_params))
|
566
|
+
end
|
512
567
|
end
|
513
568
|
end
|
514
569
|
|
515
|
-
def
|
516
|
-
|
570
|
+
def valid_locked_oauth_grant(grant_params = nil)
|
571
|
+
oauth_grant = valid_oauth_grant_ds(grant_params).for_update.first
|
517
572
|
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
573
|
+
redirect_response_error("invalid_grant") unless oauth_grant
|
574
|
+
|
575
|
+
oauth_grant
|
576
|
+
end
|
577
|
+
|
578
|
+
def valid_oauth_grant_ds(grant_params = nil)
|
579
|
+
ds = db[oauth_grants_table]
|
580
|
+
.where(Sequel[oauth_grants_table][oauth_grants_revoked_at_column] => nil)
|
581
|
+
.where(Sequel.expr(Sequel[oauth_grants_table][oauth_grants_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP)
|
582
|
+
ds = ds.where(grant_params) if grant_params
|
583
|
+
|
584
|
+
ds
|
585
|
+
end
|
586
|
+
|
587
|
+
def oauth_grant_by_token_ds(token)
|
588
|
+
ds = valid_oauth_grant_ds
|
523
589
|
|
524
|
-
|
525
|
-
.where(Sequel[
|
590
|
+
if oauth_grants_token_hash_column
|
591
|
+
ds.where(Sequel[oauth_grants_table][oauth_grants_token_hash_column] => generate_token_hash(token))
|
592
|
+
else
|
593
|
+
ds.where(Sequel[oauth_grants_table][oauth_grants_token_column] => token)
|
594
|
+
end
|
526
595
|
end
|
527
596
|
|
528
|
-
def
|
529
|
-
|
597
|
+
def oauth_grant_by_token(token)
|
598
|
+
oauth_grant_by_token_ds(token).first
|
530
599
|
end
|
531
600
|
|
532
|
-
def
|
533
|
-
ds = db[
|
601
|
+
def oauth_grant_by_refresh_token_ds(token, revoked: false)
|
602
|
+
ds = db[oauth_grants_table].where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
|
534
603
|
#
|
535
604
|
# filter expired refresh tokens out.
|
536
605
|
# an expired refresh token is a token whose access token expired for a period longer than the
|
537
606
|
# refresh token expiration period.
|
538
607
|
#
|
539
|
-
ds = ds.where(Sequel.date_add(
|
608
|
+
ds = ds.where(Sequel.date_add(oauth_grants_expires_in_column,
|
609
|
+
seconds: (oauth_refresh_token_expires_in - oauth_access_token_expires_in)) >= Sequel::CURRENT_TIMESTAMP)
|
540
610
|
|
541
|
-
ds = if
|
542
|
-
ds.where(
|
611
|
+
ds = if oauth_grants_refresh_token_hash_column
|
612
|
+
ds.where(oauth_grants_refresh_token_hash_column => generate_token_hash(token))
|
543
613
|
else
|
544
|
-
ds.where(
|
614
|
+
ds.where(oauth_grants_refresh_token_column => token)
|
545
615
|
end
|
546
616
|
|
547
|
-
ds = ds.where(
|
617
|
+
ds = ds.where(oauth_grants_revoked_at_column => nil) unless revoked
|
618
|
+
|
619
|
+
ds
|
620
|
+
end
|
548
621
|
|
549
|
-
|
622
|
+
def oauth_grant_by_refresh_token(token, **kwargs)
|
623
|
+
oauth_grant_by_refresh_token_ds(token, **kwargs).first
|
550
624
|
end
|
551
625
|
|
552
|
-
def json_access_token_payload(
|
626
|
+
def json_access_token_payload(oauth_grant)
|
553
627
|
payload = {
|
554
|
-
"access_token" =>
|
628
|
+
"access_token" => oauth_grant[oauth_grants_token_column],
|
555
629
|
"token_type" => oauth_token_type,
|
556
|
-
"expires_in" =>
|
630
|
+
"expires_in" => oauth_access_token_expires_in
|
557
631
|
}
|
558
|
-
payload["refresh_token"] =
|
632
|
+
payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column] if oauth_grant[oauth_grants_refresh_token_column]
|
559
633
|
payload
|
560
634
|
end
|
561
635
|
|
562
636
|
# Access Tokens
|
563
637
|
|
564
|
-
def
|
638
|
+
def validate_token_params
|
565
639
|
unless (grant_type = param_or_nil("grant_type"))
|
566
640
|
redirect_response_error("invalid_request")
|
567
641
|
end
|
@@ -569,76 +643,88 @@ module Rodauth
|
|
569
643
|
redirect_response_error("invalid_request") if grant_type == "refresh_token" && !param_or_nil("refresh_token")
|
570
644
|
end
|
571
645
|
|
572
|
-
def
|
573
|
-
|
574
|
-
# fetch potentially revoked oauth token
|
575
|
-
oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
|
576
|
-
|
577
|
-
if !oauth_token
|
578
|
-
redirect_response_error("invalid_grant")
|
579
|
-
elsif oauth_token[oauth_tokens_revoked_at_column]
|
580
|
-
if oauth_refresh_token_protection_policy == "rotation"
|
581
|
-
# https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
|
582
|
-
#
|
583
|
-
# If a refresh token is compromised and subsequently used by both the attacker and the legitimate
|
584
|
-
# client, one of them will present an invalidated refresh token, which will inform the authorization
|
585
|
-
# server of the breach. The authorization server cannot determine which party submitted the invalid
|
586
|
-
# refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
|
587
|
-
# forcing the legitimate client to obtain a fresh authorization grant.
|
588
|
-
|
589
|
-
db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
|
590
|
-
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
591
|
-
end
|
592
|
-
redirect_response_error("invalid_grant")
|
593
|
-
end
|
646
|
+
def create_token(grant_type)
|
647
|
+
redirect_response_error("invalid_request") unless supported_grant_type?(grant_type, "refresh_token")
|
594
648
|
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
649
|
+
refresh_token = param("refresh_token")
|
650
|
+
# fetch potentially revoked oauth token
|
651
|
+
oauth_grant = oauth_grant_by_refresh_token_ds(refresh_token, revoked: true).for_update.first
|
652
|
+
|
653
|
+
update_params = { oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
|
654
|
+
seconds: oauth_access_token_expires_in) }
|
655
|
+
|
656
|
+
if !oauth_grant || oauth_grant[oauth_grants_revoked_at_column]
|
657
|
+
redirect_response_error("invalid_grant")
|
658
|
+
elsif oauth_refresh_token_protection_policy == "rotation"
|
659
|
+
# https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
|
660
|
+
#
|
661
|
+
# If a refresh token is compromised and subsequently used by both the attacker and the legitimate
|
662
|
+
# client, one of them will present an invalidated refresh token, which will inform the authorization
|
663
|
+
# server of the breach. The authorization server cannot determine which party submitted the invalid
|
664
|
+
# refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
|
665
|
+
# forcing the legitimate client to obtain a fresh authorization grant.
|
666
|
+
|
667
|
+
refresh_token = _generate_refresh_token(update_params)
|
602
668
|
end
|
669
|
+
|
670
|
+
update_params[oauth_grants_oauth_application_id_column] = oauth_grant[oauth_grants_oauth_application_id_column]
|
671
|
+
|
672
|
+
oauth_grant = create_token_from_token(oauth_grant, update_params)
|
673
|
+
oauth_grant[oauth_grants_refresh_token_column] = refresh_token
|
674
|
+
oauth_grant
|
603
675
|
end
|
604
676
|
|
605
|
-
def
|
606
|
-
redirect_response_error("invalid_grant") unless
|
677
|
+
def create_token_from_token(oauth_grant, update_params)
|
678
|
+
redirect_response_error("invalid_grant") unless grant_from_application?(oauth_grant, oauth_application)
|
607
679
|
|
608
680
|
rescue_from_uniqueness_error do
|
609
|
-
|
681
|
+
oauth_grants_ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
610
682
|
access_token = _generate_access_token(update_params)
|
683
|
+
oauth_grant = __update_and_return__(oauth_grants_ds, update_params)
|
611
684
|
|
612
|
-
|
613
|
-
|
614
|
-
**update_params,
|
615
|
-
oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
|
616
|
-
oauth_tokens_account_id_column => oauth_token[oauth_tokens_account_id_column],
|
617
|
-
oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
|
618
|
-
}
|
619
|
-
|
620
|
-
refresh_token = _generate_refresh_token(update_params)
|
621
|
-
else
|
622
|
-
refresh_token = param("refresh_token")
|
623
|
-
end
|
624
|
-
oauth_token = __update_and_return__(oauth_tokens_ds, update_params)
|
625
|
-
|
626
|
-
oauth_token[oauth_tokens_token_column] = access_token
|
627
|
-
oauth_token[oauth_tokens_refresh_token_column] = refresh_token
|
628
|
-
oauth_token
|
685
|
+
oauth_grant[oauth_grants_token_column] = access_token
|
686
|
+
oauth_grant
|
629
687
|
end
|
630
688
|
end
|
631
689
|
|
632
690
|
def supported_grant_type?(grant_type, expected_grant_type = grant_type)
|
633
691
|
return false unless grant_type == expected_grant_type
|
634
692
|
|
635
|
-
|
636
|
-
|
637
|
-
|
693
|
+
grant_types_supported = if oauth_application[oauth_applications_grant_types_column]
|
694
|
+
oauth_application[oauth_applications_grant_types_column].split(/ +/)
|
695
|
+
else
|
696
|
+
oauth_grant_types_supported
|
697
|
+
end
|
638
698
|
|
639
699
|
grant_types_supported.include?(grant_type)
|
640
700
|
end
|
641
701
|
|
702
|
+
def supported_response_type?(response_type, expected_response_type = response_type)
|
703
|
+
return false unless response_type == expected_response_type
|
704
|
+
|
705
|
+
response_types_supported = if oauth_application[oauth_applications_grant_types_column]
|
706
|
+
oauth_application[oauth_applications_response_types_column].split(/ +/)
|
707
|
+
else
|
708
|
+
oauth_response_types_supported
|
709
|
+
end
|
710
|
+
|
711
|
+
response_types = response_type.split(/ +/)
|
712
|
+
|
713
|
+
(response_types - response_types_supported).empty?
|
714
|
+
end
|
715
|
+
|
716
|
+
def supported_response_mode?(response_mode, expected_response_mode = response_mode)
|
717
|
+
return false unless response_mode == expected_response_mode
|
718
|
+
|
719
|
+
response_modes_supported = if oauth_application[oauth_applications_response_modes_column]
|
720
|
+
oauth_application[oauth_applications_response_modes_column].split(/ +/)
|
721
|
+
else
|
722
|
+
oauth_response_modes_supported
|
723
|
+
end
|
724
|
+
|
725
|
+
response_modes_supported.include?(response_mode)
|
726
|
+
end
|
727
|
+
|
642
728
|
def oauth_server_metadata_body(path = nil)
|
643
729
|
issuer = base_url
|
644
730
|
issuer += "/#{path}" if path
|
@@ -647,10 +733,10 @@ module Rodauth
|
|
647
733
|
issuer: issuer,
|
648
734
|
token_endpoint: token_url,
|
649
735
|
scopes_supported: oauth_application_scopes,
|
650
|
-
response_types_supported:
|
651
|
-
response_modes_supported:
|
652
|
-
grant_types_supported:
|
653
|
-
token_endpoint_auth_methods_supported:
|
736
|
+
response_types_supported: oauth_response_types_supported,
|
737
|
+
response_modes_supported: oauth_response_modes_supported,
|
738
|
+
grant_types_supported: oauth_grant_types_supported,
|
739
|
+
token_endpoint_auth_methods_supported: oauth_token_endpoint_auth_methods_supported,
|
654
740
|
service_documentation: oauth_metadata_service_documentation,
|
655
741
|
ui_locales_supported: oauth_metadata_ui_locales_supported,
|
656
742
|
op_policy_uri: oauth_metadata_op_policy_uri,
|
@@ -660,10 +746,10 @@ module Rodauth
|
|
660
746
|
|
661
747
|
def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
|
662
748
|
if accepts_json?
|
663
|
-
status_code = if respond_to?(:"#{error_code}_response_status")
|
664
|
-
send(:"#{error_code}_response_status")
|
749
|
+
status_code = if respond_to?(:"oauth_#{error_code}_response_status")
|
750
|
+
send(:"oauth_#{error_code}_response_status")
|
665
751
|
else
|
666
|
-
|
752
|
+
oauth_invalid_response_status
|
667
753
|
end
|
668
754
|
|
669
755
|
throw_json_response_error(status_code, error_code)
|
@@ -671,14 +757,14 @@ module Rodauth
|
|
671
757
|
redirect_url = URI.parse(redirect_url)
|
672
758
|
query_params = []
|
673
759
|
|
674
|
-
query_params << if respond_to?(:"#{error_code}_error_code")
|
675
|
-
"error=#{send(:"#{error_code}_error_code")}"
|
760
|
+
query_params << if respond_to?(:"oauth_#{error_code}_error_code")
|
761
|
+
"error=#{send(:"oauth_#{error_code}_error_code")}"
|
676
762
|
else
|
677
763
|
"error=#{error_code}"
|
678
764
|
end
|
679
765
|
|
680
|
-
if respond_to?(:"#{error_code}_message")
|
681
|
-
message = send(:"#{error_code}_message")
|
766
|
+
if respond_to?(:"oauth_#{error_code}_message")
|
767
|
+
message = send(:"oauth_#{error_code}_message")
|
682
768
|
query_params << ["error_description=#{CGI.escape(message)}"]
|
683
769
|
end
|
684
770
|
|
@@ -705,26 +791,26 @@ module Rodauth
|
|
705
791
|
|
706
792
|
def throw_json_response_error(status, error_code, message = nil)
|
707
793
|
set_response_error_status(status)
|
708
|
-
code = if respond_to?(:"#{error_code}_error_code")
|
709
|
-
send(:"#{error_code}_error_code")
|
794
|
+
code = if respond_to?(:"oauth_#{error_code}_error_code")
|
795
|
+
send(:"oauth_#{error_code}_error_code")
|
710
796
|
else
|
711
797
|
error_code
|
712
798
|
end
|
713
799
|
payload = { "error" => code }
|
714
|
-
payload["error_description"] = message || (send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message"))
|
800
|
+
payload["error_description"] = message || (send(:"oauth_#{error_code}_message") if respond_to?(:"oauth_#{error_code}_message"))
|
715
801
|
json_payload = _json_response_body(payload)
|
716
802
|
response["Content-Type"] ||= json_response_content_type
|
717
803
|
response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
|
718
804
|
return_response(json_payload)
|
719
805
|
end
|
720
806
|
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
807
|
+
def _json_response_body(hash)
|
808
|
+
return super if features.include?(:json)
|
809
|
+
|
810
|
+
if request.respond_to?(:convert_to_json)
|
811
|
+
request.send(:convert_to_json, hash)
|
812
|
+
else
|
813
|
+
JSON.dump(hash)
|
728
814
|
end
|
729
815
|
end
|
730
816
|
|
@@ -736,7 +822,7 @@ module Rodauth
|
|
736
822
|
end
|
737
823
|
|
738
824
|
def authorization_required
|
739
|
-
throw_json_response_error(
|
825
|
+
throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
|
740
826
|
end
|
741
827
|
|
742
828
|
def check_valid_scopes?
|
@@ -751,34 +837,11 @@ module Rodauth
|
|
751
837
|
|
752
838
|
# Resource server mode
|
753
839
|
|
754
|
-
SERVER_METADATA = OAuth::TtlStore.new
|
755
|
-
|
756
840
|
def authorization_server_metadata
|
757
|
-
auth_url = URI(authorization_server_url)
|
841
|
+
auth_url = URI(authorization_server_url).dup
|
842
|
+
auth_url.path = "/.well-known/oauth-authorization-server"
|
758
843
|
|
759
|
-
|
760
|
-
|
761
|
-
return server_metadata if server_metadata
|
762
|
-
|
763
|
-
SERVER_METADATA.set(auth_url) do
|
764
|
-
http = Net::HTTP.new(auth_url.host, auth_url.port)
|
765
|
-
http.use_ssl = auth_url.scheme == "https"
|
766
|
-
|
767
|
-
request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
|
768
|
-
request["accept"] = json_response_content_type
|
769
|
-
response = http.request(request)
|
770
|
-
authorization_required unless response.code.to_i == 200
|
771
|
-
|
772
|
-
# time-to-live
|
773
|
-
ttl = if response.key?("cache-control")
|
774
|
-
cache_control = response["cache-control"]
|
775
|
-
cache_control[/max-age=(\d+)/, 1].to_i
|
776
|
-
elsif response.key?("expires")
|
777
|
-
Time.parse(response["expires"]).to_i - Time.now.to_i
|
778
|
-
end
|
779
|
-
|
780
|
-
[JSON.parse(response.body, symbolize_names: true), ttl]
|
781
|
-
end
|
844
|
+
http_request_with_cache(auth_url)
|
782
845
|
end
|
783
846
|
end
|
784
847
|
end
|