rodauth-oauth 0.4.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7c9cc026f547d781b05599d177237498390c8347791aaf5960e7447d2640b0b
4
- data.tar.gz: 290ec103b22d394fbae7f153430605fa032b8baf6b6083e31ad8af8cd3d422b8
3
+ metadata.gz: d21e4fc67f961c41299cbd79176ed284729c5d4198dd38008edee29d455baaeb
4
+ data.tar.gz: 5274aa48c6192b7182764d762fb55a4d025aefef8ee85693b770c8ce691a0de2
5
5
  SHA512:
6
- metadata.gz: 64c22bd200ff9dcb5e8406ace5f4eb34625bcee5a381e52b6b7e960b614ec3941c0460997542d843ed4eaa843a85a7f2592a027c741d5380a7572edb974ca3a9
7
- data.tar.gz: 0a6c93bc131d2fcb45e400173ced20096caa191e3ef73f4e67ab0fc12d5ead9b9f9e6867d163612299141b96b7691429cb5e5b263036887134396f244c3dd4f7
6
+ metadata.gz: 0aa9e79243f70753fd3741f21f862f0f8795b21eea16bba81319a18183a43027c344f099ab2b2663b84e30e7453cde33e9fceb4d015c32057b79fb4dc10a4680
7
+ data.tar.gz: d2dcb2edcca49fa0d9f29e321bd52cb26d40e466e10c1936a564925be9976051b9ef730a32e9a9fdc7fed9ba00778749ddc78fc9db60af227926285fc46fa285
data/CHANGELOG.md CHANGED
@@ -2,36 +2,82 @@
2
2
 
3
3
  ## master
4
4
 
5
- ### 0.4.1
5
+ ### 0.6.0 (21/05/2021)
6
6
 
7
7
  ### Improvements
8
8
 
9
+ * RBS signatures
10
+
11
+ ### Chore
12
+
13
+ * Ruby 3 and Truffleruby are now officially supported and tested in CI.
14
+
15
+ ### 0.5.1 (19/03/2021)
16
+
17
+ #### Improvements
18
+
19
+ * Changing "Callback URL" to "Redirect URL" in default templates;
20
+
21
+ #### Bugfixes
22
+
23
+ * (rails integration) Fixed templates location;
24
+ * (rails integration) Fixed migration name from generator;
25
+ * (rails integration) fixed links, html tags, styling and unassigned variables from a few view templates;
26
+ * `oauth_application_path` is now compliant with prefixes and other url helpers, while now having a `oauth_application_url` counterpart;
27
+ * (rails integration) skipping csrf checks for "/userinfo" request (OIDC)
28
+
29
+ ### 0.5.0 (08/02/2021)
30
+
31
+ #### RP-Initiated Logout
32
+
33
+ The `:oidc` plugin can now do [RP-Initiated Logout](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/RP-Initiated-Logout). It's disabled by default, so read the docs to learn how to enable it.
34
+
35
+ #### Security
36
+
37
+ The `:oauth_jwt` (and by association, `:oidc`) plugin(s) verifies the claims of used JWT tokens. This is a **very important security fix**, as without it, there is no protection against replay attacks and other types of misuse of the JWT token.
38
+
39
+ A new auth method, `generate_jti(claims)`, was [added to the list of oauth_jwt plugin options](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/JWT-Access-Tokens#rodauth-options). By default, it'll hash the `aud` and `iat` claims together, but you can overwrite how this is done.
40
+
41
+ ### 0.4.3 (09/12/2020)
42
+
43
+ * Introspection requests made to an Authorization Server in "resource server" mode are not correctly encoding the body using the "application/x-www-form-urlencoded" format.
44
+
45
+ ### 0.4.2 (24/11/2020)
46
+
47
+ #### Bugfixes
48
+
49
+ * database extensions were being run in resource server mode, when it's not expected that the oauth db tables are around.
50
+
51
+ ### 0.4.1 (24/11/2020)
52
+
53
+ #### Improvements
54
+
9
55
  When in "Resource Server" mode, calling `rodauth.authorization_token` will now return an hash of the JSON payload that the Authorization Server responds, and which was already previously used to authorize access to protected resources.
10
56
 
11
- ### Bugfixes
57
+ #### Bugfixes
12
58
 
13
- * An error ocurred if the client passed an empty authorization header (`Authorization: ` or `Authorization: Bearer `), causing an unexpected error; It now responds with the proper `401 Unauthorized` status code.
59
+ * An error occurred if the client passed an empty authorization header (`Authorization: ` or `Authorization: Bearer `), causing an unexpected error; It now responds with the proper `401 Unauthorized` status code.
14
60
 
15
- ### 0.4.0
61
+ ### 0.4.0 (13/11/2020)
16
62
 
17
- ### Features
63
+ #### Features
18
64
 
19
65
  * A new method, `get_additional_param(account, claim)`, is now exposed; this method will be called whenever non-OIDC scopes are requested in the emission of the ID token.
20
66
 
21
67
  * The `form_post` response is now supported, either by passing the `response_mode=form_post` request param in the authorization URL, or by setting `oauth_response_mode "form_post"` option. This improves the overall security of an Authorization server even more, as authorization codes are sent to client applications via a POST request to the redirect URI.
22
68
 
23
69
 
24
- ### Improvements
70
+ #### Improvements
25
71
 
26
72
  * For the OIDC `address` scope, proper claims are now emitted as per the standard, i.e. the "formatted", "street_address", "locality", "region", "postal_code", "country". These will be the ones referenced in the `get_oidc_param` method.
27
73
 
28
- ### Bugfixes
74
+ #### Bugfixes
29
75
 
30
76
  * The rails templates were missing declarations from a few params, which made some of the flows (the PKCE for example) not work out-of-the box;
31
77
  * rails tests were silently not running in CI;
32
78
  * The CI suite was revamped, so that all Oauth tests would be run under rails as well. All versions from rails equal or above 5.0 are now targeted;
33
79
 
34
- ### 0.3.0
80
+ ### 0.3.0 (8/10/2020)
35
81
 
36
82
  #### Features
37
83
 
@@ -60,7 +106,7 @@ Use `rodauth.convert_timestamp` in the templates, whenever dates are displayed.
60
106
 
61
107
  Set HTTP Cache headers for metadata responses, such as `/.well-known/oauth-authorization-server` and `/.well-known/openid-configuration`, so they can be stored at the edge. The cache will be valid for 1 day (this value isn't set by an option yet).
62
108
 
63
- ### 0.2.0
109
+ ### 0.2.0 (9/9/2020)
64
110
 
65
111
  #### Features
66
112
 
@@ -104,9 +150,7 @@ Fixed some mishandling of HTTP headers when in in resource-server mode.
104
150
  * 97.7% test coverage;
105
151
  * `rodauth-oauth` CI tests run against sqlite, postgresql and mysql.
106
152
 
107
- ### 0.1.0
108
-
109
- (31/7/2020)
153
+ ### 0.1.0 (31/7/2020)
110
154
 
111
155
  #### Features
112
156
 
@@ -152,9 +196,7 @@ URI schemes for client applications redirect URIs have to be `https`. In order t
152
196
  * fixed trailing "/" in the "issuer" value in server metadata (`https://server.com/` -> `https://server.com`).
153
197
 
154
198
 
155
- ### 0.0.6
156
-
157
- (6/7/2020)
199
+ ### 0.0.6 (6/7/2020)
158
200
 
159
201
  #### Features
160
202
 
@@ -177,9 +219,7 @@ The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (se
177
219
  Removed React Javascript from example applications.
178
220
 
179
221
 
180
- ### 0.0.5
181
-
182
- (26/6/2020)
222
+ ### 0.0.5 (26/6/2020)
183
223
 
184
224
  #### Features
185
225
 
@@ -216,9 +256,7 @@ It **requires** the authorization to implement the server metadata endpoint (`/.
216
256
  * option `scopes_param` renamed to `scope_param`;
217
257
  *
218
258
 
219
- ## 0.0.4
220
-
221
- (13/6/2020)
259
+ ## 0.0.4 (13/6/2020)
222
260
 
223
261
  ### Features
224
262
 
@@ -255,9 +293,7 @@ The `oauth_jwt` feature now allows the usage of access tokens to authorize the g
255
293
 
256
294
  * Fixed scope claim of JWT ("scopes" -> "scope");
257
295
 
258
- ## 0.0.3
259
-
260
- (5/6/2020)
296
+ ## 0.0.3 (5/6/2020)
261
297
 
262
298
  ### Features
263
299
 
@@ -289,9 +325,7 @@ end
289
325
  * renamed the existing `use_oauth_implicit_grant_type` to `use_oauth_implicit_grant_type?`;
290
326
  * It's now usable as JSON API (small caveat: POST authorize will still redirect on success...);
291
327
 
292
- ## 0.0.2
293
-
294
- (29/5/2020)
328
+ ## 0.0.2 (29/5/2020)
295
329
 
296
330
  ### Features
297
331
 
@@ -307,8 +341,6 @@ end
307
341
 
308
342
  * usage of client secret for authorizing the generation of tokens, as the spec mandates (and refraining from them when doing PKCE).
309
343
 
310
- ## 0.0.1
311
-
312
- (14/5/2020)
344
+ ## 0.0.1 (14/5/2020)
313
345
 
314
346
  Initial implementation of the Oauth 2.0 framework, with an example app done using roda.
data/README.md CHANGED
@@ -25,7 +25,12 @@ This gem implements the following RFCs and features of OAuth:
25
25
  * [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
26
26
  * OAuth application and token management dashboards;
27
27
 
28
- It also implements the [OpenID Connect layer](https://openid.net/connect/) on top of the OAuth features it provides.
28
+ It also implements the [OpenID Connect layer](https://openid.net/connect/) on top of the OAuth features it provides, including:
29
+
30
+ * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html);
31
+ * [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0-29.html);
32
+ * [OpenID Multiple Response Types](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html);
33
+ * [RP Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html);
29
34
 
30
35
  This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
31
36
 
@@ -104,7 +109,7 @@ For OpenID, it's very similar to the example above:
104
109
  ```ruby
105
110
  plugin :rodauth do
106
111
  # enable it in the plugin
107
- enable :login, :openid
112
+ enable :login, :oidc
108
113
  oauth_application_default_scope %w[openid]
109
114
  oauth_application_scopes %w[openid email profile]
110
115
  end
@@ -628,11 +633,11 @@ Although very handy for the mentioned use case, one can't revoke a JWT token on
628
633
 
629
634
  ## Ruby support policy
630
635
 
631
- The minimum Ruby version required to run `rodauth-oauth` is 2.3 . Besides that, it should support all rubies that rodauth and roda support, including JRuby and (potentially, I don't know yet) truffleruby.
636
+ The minimum Ruby version required to run `rodauth-oauth` is 2.3 . Besides that, it should support all rubies that rodauth and roda support, including JRuby and truffleruby.
632
637
 
633
- ### JRuby
638
+ ### Rails
634
639
 
635
- If you're interested in using this library in rails, be sure to check `rodauth-rails` policy, as it supports rails 5.2 upwards.
640
+ If you're interested in using this library with rails, be sure to check `rodauth-rails` policy, as it supports rails 5.2 upwards.
636
641
 
637
642
  ## Development
638
643
 
@@ -1,4 +1,4 @@
1
- class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
1
+ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :oauth_applications do |t|
4
4
  t.integer :account_id
@@ -9,7 +9,7 @@ require "rodauth/oauth/ttl_store"
9
9
  require "rodauth/oauth/database_extensions"
10
10
 
11
11
  module Rodauth
12
- Feature.define(:oauth) do
12
+ Feature.define(:oauth, :Oauth) do
13
13
  # RUBY EXTENSIONS
14
14
  unless Regexp.method_defined?(:match?)
15
15
  # If you wonder why this is there: the oauth feature uses a refinement to enhance the
@@ -139,7 +139,15 @@ module Rodauth
139
139
  auth_value_method :already_in_use_response_status, 409
140
140
 
141
141
  # OAuth Applications
142
- auth_value_method :oauth_applications_path, "oauth-applications"
142
+ auth_value_method :oauth_applications_route, "oauth-applications"
143
+ def oauth_applications_path(opts = {})
144
+ route_path(oauth_applications_route, opts)
145
+ end
146
+
147
+ def oauth_applications_url(opts = {})
148
+ route_url(oauth_applications_route, opts)
149
+ end
150
+
143
151
  auth_value_method :oauth_applications_table, :oauth_applications
144
152
 
145
153
  auth_value_method :oauth_applications_id_column, :id
@@ -192,6 +200,7 @@ module Rodauth
192
200
  auth_value_method :oauth_unique_id_generation_retries, 3
193
201
 
194
202
  auth_value_methods(
203
+ :oauth_application_path,
195
204
  :fetch_access_token,
196
205
  :oauth_unique_id_generator,
197
206
  :secret_matches?,
@@ -363,9 +372,13 @@ module Rodauth
363
372
  end
364
373
  end
365
374
 
375
+ def oauth_application_path(id)
376
+ "#{oauth_applications_path}/#{id}"
377
+ end
378
+
366
379
  # /oauth-applications routes
367
380
  def oauth_applications
368
- request.on(oauth_applications_path) do
381
+ request.on(oauth_applications_route) do
369
382
  require_account
370
383
 
371
384
  request.get "new" do
@@ -422,16 +435,20 @@ module Rodauth
422
435
  false
423
436
  when revoke_path
424
437
  !json_request?
425
- when authorize_path, %r{/#{oauth_applications_path}}
438
+ when authorize_path, oauth_applications_path
426
439
  only_json? ? false : super
427
440
  else
428
441
  super
429
442
  end
430
443
  end
431
444
 
432
- # Overrides logged_in?, so that a valid authorization token also authnenticates a request
433
- def logged_in?
434
- super || authorization_token
445
+ # Overrides session_value, so that a valid authorization token also authenticates a request
446
+ def session_value
447
+ super || begin
448
+ return unless authorization_token
449
+
450
+ authorization_token[oauth_tokens_account_id_column]
451
+ end
435
452
  end
436
453
 
437
454
  def accepts_json?
@@ -449,10 +466,6 @@ module Rodauth
449
466
  end
450
467
  end
451
468
 
452
- def initialize(scope)
453
- @scope = scope
454
- end
455
-
456
469
  def scopes
457
470
  scope = request.params["scope"]
458
471
  case scope
@@ -543,15 +556,19 @@ module Rodauth
543
556
 
544
557
  def post_configure
545
558
  super
559
+
560
+ # all of the extensions below involve DB changes. Resource server mode doesn't use
561
+ # database functions for OAuth though.
562
+ return unless is_authorization_server?
563
+
546
564
  self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
547
565
 
548
566
  # Check whether we can reutilize db entries for the same account / application pair
549
- one_oauth_token_per_account = begin
550
- db.indexes(oauth_tokens_table).values.any? do |definition|
551
- definition[:unique] &&
552
- definition[:columns] == oauth_tokens_unique_columns
553
- end
567
+ one_oauth_token_per_account = db.indexes(oauth_tokens_table).values.any? do |definition|
568
+ definition[:unique] &&
569
+ definition[:columns] == oauth_tokens_unique_columns
554
570
  end
571
+
555
572
  self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
556
573
  end
557
574
 
@@ -619,9 +636,9 @@ module Rodauth
619
636
  http.use_ssl = auth_url.scheme == "https"
620
637
 
621
638
  request = Net::HTTP::Post.new(introspect_path)
622
- request["content-type"] = json_response_content_type
639
+ request["content-type"] = "application/x-www-form-urlencoded"
623
640
  request["accept"] = json_response_content_type
624
- request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
641
+ request.set_form_data({ "token_type_hint" => token_type_hint, "token" => token })
625
642
 
626
643
  before_introspection_request(request)
627
644
  response = http.request(request)
@@ -1345,7 +1362,7 @@ module Rodauth
1345
1362
  issuer: issuer,
1346
1363
  authorization_endpoint: authorize_url,
1347
1364
  token_endpoint: token_url,
1348
- registration_endpoint: route_url(oauth_applications_path),
1365
+ registration_endpoint: oauth_applications_url,
1349
1366
  scopes_supported: oauth_application_scopes,
1350
1367
  response_types_supported: responses_supported,
1351
1368
  response_modes_supported: response_modes_supported,
@@ -1,7 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module Rodauth
4
- Feature.define(:oauth_http_mac) do
4
+ Feature.define(:oauth_http_mac, :OauthHttpMac) do
5
5
  unless String.method_defined?(:delete_prefix)
6
6
  module PrefixExtensions
7
7
  refine(String) do
@@ -3,11 +3,13 @@
3
3
  require "rodauth/oauth/ttl_store"
4
4
 
5
5
  module Rodauth
6
- Feature.define(:oauth_jwt) do
6
+ Feature.define(:oauth_jwt, :OauthJwt) do
7
7
  depends :oauth
8
8
 
9
9
  JWKS = OAuth::TtlStore.new
10
10
 
11
+ # Recommended to have hmac_secret as well
12
+
11
13
  auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
12
14
  auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
13
15
 
@@ -38,7 +40,8 @@ module Rodauth
38
40
  :jwt_encode,
39
41
  :jwt_decode,
40
42
  :jwks_set,
41
- :last_account_login_at
43
+ :last_account_login_at,
44
+ :generate_jti
42
45
  )
43
46
 
44
47
  route(:jwks) do |r|
@@ -59,6 +62,15 @@ module Rodauth
59
62
  authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
60
63
  end
61
64
 
65
+ # Overrides session_value, so that a valid authorization token also authenticates a request
66
+ def session_value
67
+ super || begin
68
+ return unless authorization_token
69
+
70
+ authorization_token["sub"]
71
+ end
72
+ end
73
+
62
74
  private
63
75
 
64
76
  unless method_defined?(:last_account_login_at)
@@ -67,6 +79,10 @@ module Rodauth
67
79
  end
68
80
  end
69
81
 
82
+ def issuer
83
+ @issuer ||= oauth_jwt_token_issuer || authorization_server_url
84
+ end
85
+
70
86
  def authorization_token
71
87
  return @authorization_token if defined?(@authorization_token)
72
88
 
@@ -79,7 +95,7 @@ module Rodauth
79
95
 
80
96
  return unless jwt_token
81
97
 
82
- return if jwt_token["iss"] != (oauth_jwt_token_issuer || authorization_server_url) ||
98
+ return if jwt_token["iss"] != issuer ||
83
99
  (oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
84
100
  !jwt_token["sub"]
85
101
 
@@ -105,7 +121,7 @@ module Rodauth
105
121
  redirect_response_error("invalid_request_object")
106
122
  end
107
123
 
108
- claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg])
124
+ claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg], verify_jti: false)
109
125
 
110
126
  redirect_response_error("invalid_request_object") unless claims
111
127
 
@@ -118,7 +134,7 @@ module Rodauth
118
134
  claims.delete("iss")
119
135
  audience = claims.delete("aud")
120
136
 
121
- redirect_response_error("invalid_request_object") if audience && audience != authorization_server_url
137
+ redirect_response_error("invalid_request_object") if audience && audience != issuer
122
138
 
123
139
  claims.each do |k, v|
124
140
  request.params[k.to_s] = v
@@ -209,7 +225,7 @@ module Rodauth
209
225
  issued_at = Time.now.to_i
210
226
 
211
227
  claims = {
212
- iss: (oauth_jwt_token_issuer || authorization_server_url), # issuer
228
+ iss: issuer, # issuer
213
229
  iat: issued_at, # issued at
214
230
  #
215
231
  # sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
@@ -317,6 +333,23 @@ module Rodauth
317
333
  end
318
334
  end
319
335
 
336
+ def generate_jti(payload)
337
+ # Use the key and iat to create a unique key per request to prevent replay attacks
338
+ jti_raw = [
339
+ payload[:aud] || payload["aud"],
340
+ payload[:iat] || payload["iat"]
341
+ ].join(":").to_s
342
+ Digest::SHA256.hexdigest(jti_raw)
343
+ end
344
+
345
+ def verify_jti(jti, claims)
346
+ generate_jti(claims) == jti
347
+ end
348
+
349
+ def verify_aud(aud, claims)
350
+ aud == (oauth_jwt_audience || claims["client_id"])
351
+ end
352
+
320
353
  if defined?(JSON::JWT)
321
354
 
322
355
  def jwk_import(data)
@@ -325,6 +358,7 @@ module Rodauth
325
358
 
326
359
  # json-jwt
327
360
  def jwt_encode(payload)
361
+ payload[:jti] = generate_jti(payload)
328
362
  jwt = JSON::JWT.new(payload)
329
363
  jwk = JSON::JWK.new(_jwt_key)
330
364
 
@@ -340,18 +374,34 @@ module Rodauth
340
374
  jwt.to_s
341
375
  end
342
376
 
343
- def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, **)
377
+ def jwt_decode(
378
+ token,
379
+ jws_key: oauth_jwt_public_key || _jwt_key,
380
+ verify_claims: true,
381
+ verify_jti: true,
382
+ **
383
+ )
344
384
  token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
345
385
 
346
- if is_authorization_server?
347
- if oauth_jwt_legacy_public_key
348
- JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
349
- elsif jws_key
350
- JSON::JWT.decode(token, jws_key)
351
- end
352
- elsif (jwks = auth_server_jwks_set)
353
- JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
386
+ claims = if is_authorization_server?
387
+ if oauth_jwt_legacy_public_key
388
+ JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
389
+ elsif jws_key
390
+ JSON::JWT.decode(token, jws_key)
391
+ end
392
+ elsif (jwks = auth_server_jwks_set)
393
+ JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
394
+ end
395
+
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)))
401
+ return
354
402
  end
403
+
404
+ claims
355
405
  rescue JSON::JWT::Exception
356
406
  nil
357
407
  end
@@ -384,12 +434,8 @@ module Rodauth
384
434
  key = jwk.keypair
385
435
  end
386
436
 
387
- # Use the key and iat to create a unique key per request to prevent replay attacks
388
- jti_raw = [key, payload[:iat]].join(":").to_s
389
- jti = Digest::SHA256.hexdigest(jti_raw)
390
-
391
437
  # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
392
- payload[:jti] = jti
438
+ payload[:jti] = generate_jti(payload)
393
439
  token = JWT.encode(payload, key, oauth_jwt_algorithm, headers)
394
440
 
395
441
  if oauth_jwt_jwe_key
@@ -405,21 +451,54 @@ module Rodauth
405
451
  token
406
452
  end
407
453
 
408
- def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm)
454
+ def jwt_decode(
455
+ token,
456
+ jws_key: oauth_jwt_public_key || _jwt_key,
457
+ jws_algorithm: oauth_jwt_algorithm,
458
+ verify_claims: true,
459
+ verify_jti: true
460
+ )
409
461
  # decrypt jwe
410
462
  token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
463
+
464
+ # verifying the JWT implies verifying:
465
+ #
466
+ # issuer: check that server generated the token
467
+ # aud: check the audience field (client is who he says he is)
468
+ # iat: check that the token didn't expire
469
+ #
470
+ # subject can't be verified automatically without having access to the account id,
471
+ # which we don't because that's the whole point.
472
+ #
473
+ verify_claims_params = if verify_claims
474
+ {
475
+ verify_iss: true,
476
+ iss: issuer,
477
+ # can't use stock aud verification, as it's dependent on the client application id
478
+ verify_aud: false,
479
+ verify_jti: (verify_jti ? method(:verify_jti) : false),
480
+ verify_iat: true
481
+ }
482
+ else
483
+ {}
484
+ end
485
+
411
486
  # decode jwt
412
- if is_authorization_server?
413
- if oauth_jwt_legacy_public_key
414
- algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
415
- JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms).first
416
- elsif jws_key
417
- JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first
418
- end
419
- elsif (jwks = auth_server_jwks_set)
420
- algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
421
- JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms).first
422
- end
487
+ claims = if is_authorization_server?
488
+ if oauth_jwt_legacy_public_key
489
+ algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
490
+ JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
491
+ elsif jws_key
492
+ JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
493
+ end
494
+ elsif (jwks = auth_server_jwks_set)
495
+ algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
496
+ JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
497
+ end
498
+
499
+ return if verify_claims && !verify_aud(claims["aud"], claims)
500
+
501
+ claims
423
502
  rescue JWT::DecodeError, JWT::JWKError
424
503
  nil
425
504
  end
@@ -3,7 +3,7 @@
3
3
  require "onelogin/ruby-saml"
4
4
 
5
5
  module Rodauth
6
- Feature.define(:oauth_saml) do
6
+ Feature.define(:oauth_saml, :OauthSaml) do
7
7
  depends :oauth
8
8
 
9
9
  auth_value_method :oauth_saml_cert_fingerprint, "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
@@ -1,7 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module Rodauth
4
- Feature.define(:oidc) do
4
+ Feature.define(:oidc, :Oidc) do
5
5
  # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
6
6
  OIDC_SCOPES_MAP = {
7
7
  "profile" => %i[name family_name given_name middle_name nickname preferred_username
@@ -14,6 +14,7 @@ module Rodauth
14
14
  VALID_METADATA_KEYS = %i[
15
15
  issuer
16
16
  authorization_endpoint
17
+ end_session_endpoint
17
18
  token_endpoint
18
19
  userinfo_endpoint
19
20
  jwks_uri
@@ -75,6 +76,10 @@ module Rodauth
75
76
  auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
76
77
  auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
77
78
 
79
+ # logout
80
+ auth_value_method :oauth_applications_post_logout_redirect_uri_column, :post_logout_redirect_uri
81
+ auth_value_method :use_rp_initiated_logout?, false
82
+
78
83
  auth_value_methods(:get_oidc_param, :get_additional_param)
79
84
 
80
85
  # /userinfo
@@ -108,10 +113,81 @@ module Rodauth
108
113
  end
109
114
  end
110
115
 
111
- def openid_configuration(issuer = nil)
116
+ # /oidc-logout
117
+ route(:oidc_logout) do |r|
118
+ next unless use_rp_initiated_logout?
119
+
120
+ before_oidc_logout_route
121
+ require_authorizable_account
122
+
123
+ # OpenID Providers MUST support the use of the HTTP GET and POST methods
124
+ r.on method: %i[get post] do
125
+ catch_error do
126
+ validate_oidc_logout_params
127
+
128
+ #
129
+ # why this is done:
130
+ #
131
+ # we need to decode the id token in order to get the application, because, if the
132
+ # signing key is application-specific, we don't know how to verify the signature
133
+ # beforehand. Hence, we have to do it twice: decode-and-do-not-verify, initialize
134
+ # the @oauth_application, and then decode-and-verify.
135
+ #
136
+ oauth_token = jwt_decode(param("id_token_hint"), verify_claims: false)
137
+ oauth_application_id = oauth_token["client_id"]
138
+
139
+ # check whether ID token belongs to currently logged-in user
140
+ redirect_response_error("invalid_request") unless oauth_token["sub"] == jwt_subject(
141
+ oauth_tokens_account_id_column => account_id,
142
+ oauth_tokens_oauth_application_id_column => oauth_application_id
143
+ )
144
+
145
+ # When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token.
146
+ redirect_response_error("invalid_request") unless oauth_token && oauth_token["iss"] == issuer
147
+
148
+ # now let's logout from IdP
149
+ transaction do
150
+ before_logout
151
+ logout
152
+ after_logout
153
+ end
154
+
155
+ if (post_logout_redirect_uri = param_or_nil("post_logout_redirect_uri"))
156
+ catch(:default_logout_redirect) do
157
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => oauth_token["client_id"]).first
158
+
159
+ throw(:default_logout_redirect) unless oauth_application
160
+
161
+ post_logout_redirect_uris = oauth_application[oauth_applications_post_logout_redirect_uri_column].split(" ")
162
+
163
+ throw(:default_logout_redirect) unless post_logout_redirect_uris.include?(post_logout_redirect_uri)
164
+
165
+ if (state = param_or_nil("state"))
166
+ post_logout_redirect_uri = URI(post_logout_redirect_uri)
167
+ params = ["state=#{state}"]
168
+ params << post_logout_redirect_uri.query if post_logout_redirect_uri.query
169
+ post_logout_redirect_uri.query = params.join("&")
170
+ post_logout_redirect_uri = post_logout_redirect_uri.to_s
171
+ end
172
+
173
+ redirect(post_logout_redirect_uri)
174
+ end
175
+
176
+ end
177
+
178
+ # regular logout procedure
179
+ set_notice_flash(logout_notice_flash)
180
+ redirect(logout_redirect)
181
+ end
182
+
183
+ redirect_response_error("invalid_request")
184
+ end
185
+ end
186
+
187
+ def openid_configuration(alt_issuer = nil)
112
188
  request.on(".well-known/openid-configuration") do
113
189
  request.get do
114
- json_response_success(openid_configuration_body(issuer), cache: true)
190
+ json_response_success(openid_configuration_body(alt_issuer), cache: true)
115
191
  end
116
192
  end
117
193
  end
@@ -139,6 +215,15 @@ module Rodauth
139
215
  end
140
216
  end
141
217
 
218
+ def check_csrf?
219
+ case request.path
220
+ when userinfo_path
221
+ false
222
+ else
223
+ super
224
+ end
225
+ end
226
+
142
227
  private
143
228
 
144
229
  def require_authorizable_account
@@ -342,6 +427,18 @@ module Rodauth
342
427
  params
343
428
  end
344
429
 
430
+ # Logout
431
+
432
+ def validate_oidc_logout_params
433
+ redirect_response_error("invalid_request") unless param_or_nil("id_token_hint")
434
+ # check if valid token hint type
435
+ return unless (redirect_uri = param_or_nil("post_logout_redirect_uri"))
436
+
437
+ return if check_valid_uri?(redirect_uri)
438
+
439
+ redirect_response_error("invalid_request")
440
+ end
441
+
345
442
  # Metadata
346
443
 
347
444
  def openid_configuration_body(path)
@@ -368,6 +465,7 @@ module Rodauth
368
465
 
369
466
  metadata.merge(
370
467
  userinfo_endpoint: userinfo_url,
468
+ end_session_endpoint: (oidc_logout_url if use_rp_initiated_logout?),
371
469
  response_types_supported: response_types_supported,
372
470
  subject_types_supported: [oauth_jwt_subject_type],
373
471
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.4.1"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth-oauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-24 00:00:00.000000000 Z
11
+ date: 2021-05-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Implementation of the OAuth 2.0 protocol on top of rodauth.
14
14
  email:
@@ -23,12 +23,12 @@ files:
23
23
  - CHANGELOG.md
24
24
  - LICENSE.txt
25
25
  - README.md
26
- - lib/generators/roda/oauth/install_generator.rb
27
- - lib/generators/roda/oauth/templates/app/models/oauth_application.rb
28
- - lib/generators/roda/oauth/templates/app/models/oauth_grant.rb
29
- - lib/generators/roda/oauth/templates/app/models/oauth_token.rb
30
- - lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb
31
- - lib/generators/roda/oauth/views_generator.rb
26
+ - lib/generators/rodauth/oauth/install_generator.rb
27
+ - lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
28
+ - lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
29
+ - lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb
30
+ - lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb
31
+ - lib/generators/rodauth/oauth/views_generator.rb
32
32
  - lib/rodauth/features/oauth.rb
33
33
  - lib/rodauth/features/oauth_http_mac.rb
34
34
  - lib/rodauth/features/oauth_jwt.rb
@@ -71,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
71
  - !ruby/object:Gem::Version
72
72
  version: '0'
73
73
  requirements: []
74
- rubygems_version: 3.1.4
74
+ rubygems_version: 3.2.15
75
75
  signing_key:
76
76
  specification_version: 4
77
77
  summary: Implementation of the OAuth 2.0 protocol on top of rodauth.