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