rodauth-oauth 0.7.4 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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