rodauth-oauth 0.7.4 → 0.8.0

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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -424
  3. data/README.md +26 -389
  4. data/doc/release_notes/0_0_1.md +3 -0
  5. data/doc/release_notes/0_0_2.md +15 -0
  6. data/doc/release_notes/0_0_3.md +31 -0
  7. data/doc/release_notes/0_0_4.md +36 -0
  8. data/doc/release_notes/0_0_5.md +36 -0
  9. data/doc/release_notes/0_0_6.md +21 -0
  10. data/doc/release_notes/0_1_0.md +44 -0
  11. data/doc/release_notes/0_2_0.md +43 -0
  12. data/doc/release_notes/0_3_0.md +28 -0
  13. data/doc/release_notes/0_4_0.md +18 -0
  14. data/doc/release_notes/0_4_1.md +9 -0
  15. data/doc/release_notes/0_4_2.md +5 -0
  16. data/doc/release_notes/0_4_3.md +3 -0
  17. data/doc/release_notes/0_5_0.md +11 -0
  18. data/doc/release_notes/0_5_1.md +13 -0
  19. data/doc/release_notes/0_6_0.md +9 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_7_0.md +20 -0
  22. data/doc/release_notes/0_7_1.md +10 -0
  23. data/doc/release_notes/0_7_2.md +21 -0
  24. data/doc/release_notes/0_7_3.md +10 -0
  25. data/doc/release_notes/0_7_4.md +5 -0
  26. data/doc/release_notes/0_8_0.md +37 -0
  27. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +3 -3
  28. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
  29. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
  30. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +22 -10
  31. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +11 -5
  32. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +38 -0
  33. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +5 -5
  34. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +11 -15
  35. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +9 -1
  36. data/lib/rodauth/features/oauth.rb +3 -1418
  37. data/lib/rodauth/features/oauth_application_management.rb +209 -0
  38. data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
  39. data/lib/rodauth/features/oauth_authorization_code_grant.rb +249 -0
  40. data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
  41. data/lib/rodauth/features/oauth_base.rb +735 -0
  42. data/lib/rodauth/features/oauth_device_grant.rb +221 -0
  43. data/lib/rodauth/features/oauth_http_mac.rb +3 -21
  44. data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
  45. data/lib/rodauth/features/oauth_jwt.rb +37 -60
  46. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
  47. data/lib/rodauth/features/oauth_pkce.rb +98 -0
  48. data/lib/rodauth/features/oauth_resource_server.rb +21 -0
  49. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
  50. data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
  51. data/lib/rodauth/features/oauth_token_management.rb +77 -0
  52. data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
  53. data/lib/rodauth/features/oidc.rb +4 -3
  54. data/lib/rodauth/oauth/database_extensions.rb +15 -2
  55. data/lib/rodauth/oauth/refinements.rb +48 -0
  56. data/lib/rodauth/oauth/version.rb +1 -1
  57. data/locales/en.yml +28 -12
  58. data/templates/authorize.str +7 -7
  59. data/templates/client_secret_field.str +2 -2
  60. data/templates/description_field.str +1 -1
  61. data/templates/device_search.str +11 -0
  62. data/templates/device_verification.str +24 -0
  63. data/templates/homepage_url_field.str +2 -2
  64. data/templates/jws_jwk_field.str +4 -0
  65. data/templates/jwt_public_key_field.str +4 -0
  66. data/templates/name_field.str +1 -1
  67. data/templates/new_oauth_application.str +9 -0
  68. data/templates/oauth_application.str +7 -3
  69. data/templates/oauth_application_oauth_tokens.str +51 -0
  70. data/templates/oauth_applications.str +2 -2
  71. data/templates/oauth_tokens.str +9 -11
  72. data/templates/redirect_uri_field.str +2 -2
  73. metadata +71 -3
  74. data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:oauth_device_grant, :OauthDeviceGrant) do
5
+ depends :oauth_base
6
+
7
+ auth_value_method :use_oauth_device_code_grant_type?, false
8
+
9
+ before "device_authorization"
10
+ before "device_verification"
11
+
12
+ notice_flash "The device is verified", "device_verification"
13
+ error_flash "No device to authorize with the given user code", "user_code_not_found"
14
+
15
+ view "device_verification", "Device Verification", "device_verification"
16
+ view "device_search", "Device Search", "device_search"
17
+
18
+ button "Verify", "oauth_device_verification"
19
+ button "Search", "oauth_device_search"
20
+
21
+ auth_value_method :oauth_grants_user_code_column, :user_code
22
+ auth_value_method :oauth_grants_last_polled_at_column, :last_polled_at
23
+
24
+ translatable_method :expired_token_message, "the device code has expired"
25
+ translatable_method :access_denied_message, "the authorization request has been denied"
26
+ translatable_method :authorization_pending_message, "the authorization request is still pending"
27
+ translatable_method :slow_down_message, "authorization request is still pending but poll interval should be increased"
28
+
29
+ auth_value_method :oauth_device_code_grant_polling_interval, 5 # seconds
30
+ auth_value_method :oauth_device_code_grant_user_code_size, 8 # characters
31
+ %w[user_code].each do |param|
32
+ auth_value_method :"oauth_grant_#{param}_param", param
33
+ end
34
+ translatable_method :oauth_grant_user_code_label, "User code"
35
+
36
+ auth_value_methods(
37
+ :generate_user_code
38
+ )
39
+
40
+ # /device-authorization
41
+ route(:device_authorization) do |r|
42
+ next unless is_authorization_server? && use_oauth_device_code_grant_type?
43
+
44
+ before_device_authorization_route
45
+
46
+ r.post do
47
+ require_oauth_application
48
+
49
+ user_code = generate_user_code
50
+ device_code = transaction do
51
+ before_device_authorization
52
+ create_oauth_grant(oauth_grants_user_code_column => user_code)
53
+ end
54
+
55
+ json_response_success \
56
+ "device_code" => device_code,
57
+ "user_code" => user_code,
58
+ "verification_uri" => device_url,
59
+ "verification_uri_complete" => device_url(user_code: user_code),
60
+ "expires_in" => oauth_grant_expires_in,
61
+ "interval" => oauth_device_code_grant_polling_interval
62
+ end
63
+ end
64
+
65
+ # /device
66
+ route(:device) do |r|
67
+ next unless is_authorization_server? && use_oauth_device_code_grant_type?
68
+
69
+ before_device_route
70
+ require_authorizable_account
71
+
72
+ r.get do
73
+ if (user_code = param_or_nil("user_code"))
74
+ oauth_grant = db[oauth_grants_table].where(
75
+ oauth_grants_user_code_column => user_code,
76
+ oauth_grants_revoked_at_column => nil
77
+ ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP).first
78
+
79
+ unless oauth_grant
80
+ set_redirect_error_flash user_code_not_found_error_flash
81
+ redirect device_path
82
+ end
83
+
84
+ scope.instance_variable_set(:@oauth_grant, oauth_grant)
85
+ device_verification_view
86
+ else
87
+ device_search_view
88
+ end
89
+ end
90
+
91
+ r.post do
92
+ catch_error do
93
+ unless param_or_nil("user_code")
94
+ set_redirect_error_flash invalid_grant_message
95
+ redirect device_path
96
+ end
97
+
98
+ transaction do
99
+ before_device_verification
100
+ create_oauth_token("device_code")
101
+ end
102
+ end
103
+ set_notice_flash device_verification_notice_flash
104
+ redirect device_path
105
+ end
106
+ end
107
+
108
+ def check_csrf?
109
+ case request.path
110
+ when device_authorization_path
111
+ false
112
+ else
113
+ super
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def generate_user_code
120
+ user_code_size = oauth_device_code_grant_user_code_size
121
+ SecureRandom.random_number(36**user_code_size)
122
+ .to_s(36) # 0 to 9, a to z
123
+ .upcase
124
+ .rjust(user_code_size, "0")
125
+ end
126
+
127
+ def authorized_oauth_application?(oauth_application, client_secret)
128
+ # skip if using device grant
129
+ #
130
+ # requests may be performed by devices with no knowledge of client secret.
131
+ return true if !client_secret && oauth_application && use_oauth_device_code_grant_type?
132
+
133
+ super
134
+ end
135
+
136
+ def create_oauth_token(grant_type)
137
+ case grant_type
138
+ when "urn:ietf:params:oauth:grant-type:device_code"
139
+ throw_json_response_error(invalid_oauth_response_status, "invalid_grant_type") unless use_oauth_device_code_grant_type?
140
+
141
+ oauth_grant = db[oauth_grants_table].where(
142
+ oauth_grants_code_column => param("device_code"),
143
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
144
+ ).for_update.first
145
+
146
+ throw_json_response_error(invalid_oauth_response_status, "invalid_grant") unless oauth_grant
147
+
148
+ now = Time.now
149
+
150
+ if oauth_grant[oauth_grants_revoked_at_column]
151
+ oauth_token = db[oauth_tokens_table]
152
+ .where(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
153
+ .where(Sequel[oauth_tokens_table][oauth_tokens_revoked_at_column] => nil)
154
+ .where(oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column])
155
+ .first
156
+
157
+ throw_json_response_error(invalid_oauth_response_status, "access_denied") unless oauth_token
158
+ elsif oauth_grant[oauth_grants_expires_in_column] < now
159
+ throw_json_response_error(invalid_oauth_response_status, "expired_token")
160
+ else
161
+ last_polled_at = oauth_grant[oauth_grants_last_polled_at_column]
162
+ if last_polled_at && convert_timestamp(last_polled_at) + oauth_device_code_grant_polling_interval > now
163
+ throw_json_response_error(invalid_oauth_response_status, "slow_down")
164
+ else
165
+ db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
166
+ .update(oauth_grants_last_polled_at_column => Sequel::CURRENT_TIMESTAMP)
167
+ throw_json_response_error(invalid_oauth_response_status, "authorization_pending")
168
+ end
169
+ end
170
+ oauth_token
171
+ when "device_code"
172
+ redirect_response_error("invalid_grant_type") unless use_oauth_device_code_grant_type?
173
+
174
+ # fetch oauth grant
175
+ oauth_grant = db[oauth_grants_table].where(
176
+ oauth_grants_user_code_column => param("user_code"),
177
+ oauth_grants_revoked_at_column => nil
178
+ ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
179
+ .for_update
180
+ .first
181
+
182
+ return unless oauth_grant
183
+
184
+ # update ownership
185
+ db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
186
+ .update(
187
+ oauth_grants_user_code_column => nil,
188
+ oauth_grants_account_id_column => account_id
189
+ )
190
+
191
+ create_params = {
192
+ oauth_tokens_account_id_column => account_id,
193
+ oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
194
+ oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
195
+ oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
196
+ }
197
+ create_oauth_token_from_authorization_code(oauth_grant, create_params)
198
+ else
199
+ super
200
+ end
201
+ end
202
+
203
+ def validate_oauth_token_params
204
+ grant_type = param_or_nil("grant_type")
205
+
206
+ if grant_type == "urn:ietf:params:oauth:grant-type:device_code" && !param_or_nil("device_code")
207
+ redirect_response_error("invalid_request")
208
+ end
209
+ super
210
+ end
211
+
212
+ def oauth_server_metadata_body(*)
213
+ super.tap do |data|
214
+ if use_oauth_device_code_grant_type?
215
+ data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:device_code"
216
+ data[:device_authorization_endpoint] = device_authorization_url
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -1,28 +1,10 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require "rodauth/oauth/refinements"
4
+
3
5
  module Rodauth
4
6
  Feature.define(:oauth_http_mac, :OauthHttpMac) do
5
- unless String.method_defined?(:delete_prefix)
6
- module PrefixExtensions
7
- refine(String) do
8
- def delete_suffix(suffix)
9
- suffix = suffix.to_s
10
- len = suffix.length
11
- return dup unless len.positive? && index(suffix, -len)
12
-
13
- self[0...-len]
14
- end
15
-
16
- def delete_prefix(prefix)
17
- prefix = prefix.to_s
18
- return dup unless rindex(prefix, 0)
19
-
20
- self[prefix.length..-1]
21
- end
22
- end
23
- end
24
- using(PrefixExtensions)
25
- end
7
+ using PrefixExtensions
26
8
 
27
9
  depends :oauth
28
10
 
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:oauth_implicit_grant, :OauthImplicitGrant) do
5
+ depends :oauth_base
6
+
7
+ auth_value_method :use_oauth_implicit_grant_type?, false
8
+
9
+ private
10
+
11
+ def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
12
+ return super unless param("response_type") == "token" && use_oauth_implicit_grant_type?
13
+
14
+ response_mode ||= "fragment"
15
+ response_params.replace(_do_authorize_token)
16
+
17
+ response_params["state"] = param("state") if param_or_nil("state")
18
+
19
+ [response_params, response_mode]
20
+ end
21
+
22
+ def _do_authorize_token
23
+ create_params = {
24
+ oauth_tokens_account_id_column => account_id,
25
+ oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
26
+ oauth_tokens_scopes_column => scopes
27
+ }
28
+ oauth_token = generate_oauth_token(create_params, false)
29
+
30
+ json_access_token_payload(oauth_token)
31
+ end
32
+
33
+ def authorize_response(params, mode)
34
+ return super unless mode == "fragment"
35
+
36
+ redirect_url = URI.parse(redirect_uri)
37
+ params = params.map { |k, v| "#{k}=#{v}" }
38
+ params << redirect_url.query if redirect_url.query
39
+ redirect_url.fragment = params.join("&")
40
+ redirect(redirect_url.to_s)
41
+ end
42
+
43
+ def oauth_server_metadata_body(*)
44
+ super.tap do |data|
45
+ if use_oauth_implicit_grant_type?
46
+ data[:response_types_supported] << "token"
47
+ data[:response_modes_supported] << "fragment"
48
+ data[:grant_types_supported] << "implicit"
49
+ end
50
+ end
51
+ end
52
+
53
+ def check_valid_response_type?
54
+ return true if use_oauth_implicit_grant_type? && param_or_nil("response_type") == "token"
55
+
56
+ super
57
+ end
58
+ end
59
+ end
@@ -15,7 +15,13 @@ module Rodauth
15
15
 
16
16
  auth_value_method :oauth_jwt_token_issuer, nil
17
17
 
18
- auth_value_method :oauth_application_jws_jwk_column, nil
18
+ auth_value_method :oauth_applications_jws_jwk_column, :jws_jwk
19
+ auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
20
+
21
+ translatable_method :oauth_applications_jws_jwk_label, "JSON Web Keys"
22
+ translatable_method :oauth_applications_jwt_public_key_label, "Public key"
23
+ auth_value_method :oauth_application_jws_jwk_param, :jws_jwk
24
+ auth_value_method :oauth_application_jwt_public_key_param, :jwt_public_key
19
25
 
20
26
  auth_value_method :oauth_jwt_key, nil
21
27
  auth_value_method :oauth_jwt_public_key, nil
@@ -113,9 +119,7 @@ module Rodauth
113
119
 
114
120
  return super unless request_object && oauth_application
115
121
 
116
- jws_jwk = if oauth_application[oauth_application_jws_jwk_column]
117
- jwk = oauth_application[oauth_application_jws_jwk_column]
118
-
122
+ jws_jwk = if (jwk = oauth_application[oauth_applications_jws_jwk_column])
119
123
  jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
120
124
  else
121
125
  redirect_response_error("invalid_request_object")
@@ -145,51 +149,6 @@ module Rodauth
145
149
 
146
150
  # /token
147
151
 
148
- def require_oauth_application
149
- # requset authentication optional for assertions
150
- return super unless param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
151
-
152
- claims = jwt_decode(param("assertion"))
153
-
154
- redirect_response_error("invalid_grant") unless claims
155
-
156
- @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
157
-
158
- authorization_required unless @oauth_application
159
- end
160
-
161
- def validate_oauth_token_params
162
- if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
163
- redirect_response_error("invalid_client") unless param_or_nil("assertion")
164
- else
165
- super
166
- end
167
- end
168
-
169
- def create_oauth_token
170
- if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
171
- create_oauth_token_from_assertion
172
- else
173
- super
174
- end
175
- end
176
-
177
- def create_oauth_token_from_assertion
178
- claims = jwt_decode(param("assertion"))
179
-
180
- account = account_ds(claims["sub"]).first
181
-
182
- redirect_response_error("invalid_client") unless oauth_application && account
183
-
184
- create_params = {
185
- oauth_tokens_account_id_column => claims["sub"],
186
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
187
- oauth_tokens_scopes_column => claims["scope"]
188
- }
189
-
190
- generate_oauth_token(create_params, false)
191
- end
192
-
193
152
  def generate_oauth_token(params = {}, should_generate_refresh_token = true)
194
153
  create_params = {
195
154
  oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
@@ -295,7 +254,17 @@ module Rodauth
295
254
  end
296
255
 
297
256
  def _jwt_key
298
- @_jwt_key ||= oauth_jwt_key || (oauth_application[oauth_applications_client_secret_column] if oauth_application)
257
+ @_jwt_key ||= oauth_jwt_key || begin
258
+ if oauth_application
259
+
260
+ if (jwk = oauth_application[oauth_applications_jws_jwk_column])
261
+ jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
262
+ jwk
263
+ else
264
+ oauth_application[oauth_applications_jwt_public_key_column]
265
+ end
266
+ end
267
+ end
299
268
  end
300
269
 
301
270
  # Resource Server only!
@@ -346,8 +315,8 @@ module Rodauth
346
315
  generate_jti(claims) == jti
347
316
  end
348
317
 
349
- def verify_aud(aud, claims)
350
- aud == (oauth_jwt_audience || claims["client_id"])
318
+ def verify_aud(expected_aud, aud)
319
+ expected_aud == aud
351
320
  end
352
321
 
353
322
  if defined?(JSON::JWT)
@@ -379,6 +348,8 @@ module Rodauth
379
348
  jws_key: oauth_jwt_public_key || _jwt_key,
380
349
  verify_claims: true,
381
350
  verify_jti: true,
351
+ verify_iss: true,
352
+ verify_aud: false,
382
353
  **
383
354
  )
384
355
  token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
@@ -393,11 +364,15 @@ module Rodauth
393
364
  JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
394
365
  end
395
366
 
396
- if verify_claims && !(claims[:iss] == issuer &&
397
- verify_aud(claims[:aud], claims) &&
398
- (!claims[:iat] || Time.at(claims[:iat]) > (Time.now - oauth_token_expires_in)) &&
399
- (!claims[:exp] || Time.at(claims[:exp]) > Time.now) &&
400
- (!verify_jti || verify_jti(claims[:jti], claims)))
367
+ now = Time.now
368
+ if verify_claims && (
369
+ (!claims[:exp] || Time.at(claims[:exp]) < now) &&
370
+ (claims[:nbf] && Time.at(claims[:nbf]) < now) &&
371
+ (claims[:iat] && Time.at(claims[:iat]) < now) &&
372
+ (verify_iss && claims[:iss] != issuer) &&
373
+ (verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
374
+ (verify_jti && !verify_jti(claims[:jti], claims))
375
+ )
401
376
  return
402
377
  end
403
378
 
@@ -456,7 +431,9 @@ module Rodauth
456
431
  jws_key: oauth_jwt_public_key || _jwt_key,
457
432
  jws_algorithm: oauth_jwt_algorithm,
458
433
  verify_claims: true,
459
- verify_jti: true
434
+ verify_jti: true,
435
+ verify_iss: true,
436
+ verify_aud: false
460
437
  )
461
438
  # decrypt jwe
462
439
  token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
@@ -472,7 +449,7 @@ module Rodauth
472
449
  #
473
450
  verify_claims_params = if verify_claims
474
451
  {
475
- verify_iss: true,
452
+ verify_iss: verify_iss,
476
453
  iss: issuer,
477
454
  # can't use stock aud verification, as it's dependent on the client application id
478
455
  verify_aud: false,
@@ -496,7 +473,7 @@ module Rodauth
496
473
  JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
497
474
  end
498
475
 
499
- return if verify_claims && !verify_aud(claims["aud"], claims)
476
+ return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
500
477
 
501
478
  claims
502
479
  rescue JWT::DecodeError, JWT::JWKError
@@ -0,0 +1,59 @@
1
+ # frozen-string-literal: true
2
+
3
+ require "rodauth/oauth/ttl_store"
4
+
5
+ module Rodauth
6
+ Feature.define(:oauth_jwt_bearer_grant, :OauthJwtBearerGrant) do
7
+ depends :oauth_assertion_base, :oauth_jwt
8
+
9
+ auth_value_methods(
10
+ :require_oauth_application_from_jwt_bearer_assertion_issuer,
11
+ :require_oauth_application_from_jwt_bearer_assertion_subject,
12
+ :account_from_jwt_bearer_assertion
13
+ )
14
+
15
+ private
16
+
17
+ def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
18
+ claims = jwt_assertion(assertion)
19
+
20
+ return unless claims
21
+
22
+ db[oauth_applications_table].where(
23
+ oauth_applications_client_id_column => claims["iss"]
24
+ ).first
25
+ end
26
+
27
+ def require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
28
+ claims = jwt_assertion(assertion)
29
+
30
+ return unless claims
31
+
32
+ db[oauth_applications_table].where(
33
+ oauth_applications_client_id_column => claims["sub"]
34
+ ).first
35
+ end
36
+
37
+ def account_from_jwt_bearer_assertion(assertion)
38
+ claims = jwt_assertion(assertion)
39
+
40
+ return unless claims
41
+
42
+ account_from_bearer_assertion_subject(claims["sub"])
43
+ end
44
+
45
+ def jwt_assertion(assertion)
46
+ claims = jwt_decode(assertion, verify_iss: false, verify_aud: false)
47
+ return unless verify_aud(token_url, claims["aud"])
48
+
49
+ claims
50
+ end
51
+
52
+ def oauth_server_metadata_body(*)
53
+ super.tap do |data|
54
+ data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:jwt-bearer"
55
+ data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth/refinements"
4
+
5
+ module Rodauth
6
+ Feature.define(:oauth_pkce, :OauthPkce) do
7
+ using PrefixExtensions
8
+
9
+ depends :oauth_authorization_code_grant
10
+
11
+ auth_value_method :use_oauth_pkce?, true
12
+
13
+ auth_value_method :oauth_require_pkce, false
14
+ auth_value_method :oauth_pkce_challenge_method, "S256"
15
+
16
+ auth_value_method :oauth_grants_code_challenge_column, :code_challenge
17
+ auth_value_method :oauth_grants_code_challenge_method_column, :code_challenge_method
18
+
19
+ auth_value_method :code_challenge_required_error_code, "invalid_request"
20
+ translatable_method :code_challenge_required_message, "code challenge required"
21
+ auth_value_method :unsupported_transform_algorithm_error_code, "invalid_request"
22
+ translatable_method :unsupported_transform_algorithm_message, "transform algorithm not supported"
23
+
24
+ private
25
+
26
+ def authorized_oauth_application?(oauth_application, client_secret)
27
+ return true if use_oauth_pkce? && param_or_nil("code_verifier")
28
+
29
+ super
30
+ end
31
+
32
+ def validate_oauth_grant_params
33
+ validate_pkce_challenge_params if use_oauth_pkce?
34
+
35
+ super
36
+ end
37
+
38
+ def create_oauth_grant(create_params = {})
39
+ # PKCE flow
40
+ if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
41
+ code_challenge_method = param_or_nil("code_challenge_method")
42
+
43
+ create_params[oauth_grants_code_challenge_column] = code_challenge
44
+ create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
45
+ end
46
+
47
+ super
48
+ end
49
+
50
+ def create_oauth_token_from_authorization_code(oauth_grant, create_params)
51
+ if use_oauth_pkce?
52
+ if oauth_grant[oauth_grants_code_challenge_column]
53
+ code_verifier = param_or_nil("code_verifier")
54
+
55
+ redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
56
+ elsif oauth_require_pkce
57
+ redirect_response_error("code_challenge_required")
58
+ end
59
+ end
60
+
61
+ super
62
+ end
63
+
64
+ def validate_pkce_challenge_params
65
+ if param_or_nil("code_challenge")
66
+
67
+ challenge_method = param_or_nil("code_challenge_method")
68
+ redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
69
+ else
70
+ return unless oauth_require_pkce
71
+
72
+ redirect_response_error("code_challenge_required")
73
+ end
74
+ end
75
+
76
+ def check_valid_grant_challenge?(grant, verifier)
77
+ challenge = grant[oauth_grants_code_challenge_column]
78
+
79
+ case grant[oauth_grants_code_challenge_method_column]
80
+ when "plain"
81
+ challenge == verifier
82
+ when "S256"
83
+ generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
84
+ generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
85
+
86
+ challenge == generated_challenge
87
+ else
88
+ redirect_response_error("unsupported_transform_algorithm")
89
+ end
90
+ end
91
+
92
+ def oauth_server_metadata_body(*)
93
+ super.tap do |data|
94
+ data[:code_challenge_methods_supported] = oauth_pkce_challenge_method if use_oauth_pkce?
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:oauth_resource_server, :OauthResourceServer) do
5
+ def authorization_token
6
+ return @authorization_token if defined?(@authorization_token)
7
+
8
+ # check if there is a token
9
+ bearer_token = fetch_access_token
10
+
11
+ return unless bearer_token
12
+
13
+ # where in resource server, NOT the authorization server.
14
+ payload = introspection_request("access_token", bearer_token)
15
+
16
+ return unless payload["active"]
17
+
18
+ @authorization_token = payload
19
+ end
20
+ end
21
+ end