rodauth-oauth 0.4.3 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96756ac8a30c904c5b832b64c47a00af9524810561d58c909b6f322da7348e8c
4
- data.tar.gz: 965f6ff260bd86c2fcb7bbd2ba2bd131b453f04a39b73f99ef0860d2bc95b0e0
3
+ metadata.gz: c0c72cd872103e1d10929ad5934312a123a42b3c9cb55c06c118fbcb0d83f4a7
4
+ data.tar.gz: 57bbcef2981c20627cfc9239b30781af03898e34b5d2861b84a820d778e1dac3
5
5
  SHA512:
6
- metadata.gz: e7e257a12204599a27d0917f2b31c32906f0d4c566d51ee6d4fde146e2340e36afb9a932cff8bf37872d59259f4d43d423d1c1266f3066063c70aa334f83e119
7
- data.tar.gz: 07c0e564e7636893f736f6e05f634684cd7bc28e9d0acfb53ba518357fab198bc878792a68bde6b988b8c8ddf2d3e2bb4d4ecebcd9c4bf68d85f75178cdd0fdf
6
+ metadata.gz: a4c48e1ce93074c5dff85f506c8c5b8c7f024409f9c58ae942bb2adb5241303586cb0930a1c849566c793d6d9e508a73b0f5fc5772a82e3c87852415997e7889
7
+ data.tar.gz: 54e5777b2506ea99f830cd3d9b66ca1755372681cd19013638bc25680b0ce601275fb5fd732b123fcefddb7f56ba0ac1fc6a069f028c377be5a7408d92debb9e
data/CHANGELOG.md CHANGED
@@ -2,40 +2,83 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ### 0.6.1 (08/09/2021)
6
+
7
+ #### Bugfixes
8
+
9
+ * Fixed rails view templates escaping.
10
+ * Fixed declaration of authorize template in the generator.
11
+
12
+ ### 0.6.0 (21/05/2021)
13
+
14
+ ### Improvements
15
+
16
+ * RBS signatures
17
+
18
+ ### Chore
19
+
20
+ * Ruby 3 and Truffleruby are now officially supported and tested in CI.
21
+
22
+ ### 0.5.1 (19/03/2021)
23
+
24
+ #### Improvements
25
+
26
+ * Changing "Callback URL" to "Redirect URL" in default templates;
27
+
28
+ #### Bugfixes
29
+
30
+ * (rails integration) Fixed templates location;
31
+ * (rails integration) Fixed migration name from generator;
32
+ * (rails integration) fixed links, html tags, styling and unassigned variables from a few view templates;
33
+ * `oauth_application_path` is now compliant with prefixes and other url helpers, while now having a `oauth_application_url` counterpart;
34
+ * (rails integration) skipping csrf checks for "/userinfo" request (OIDC)
35
+
36
+ ### 0.5.0 (08/02/2021)
37
+
38
+ #### RP-Initiated Logout
39
+
40
+ 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.
41
+
42
+ #### Security
43
+
44
+ 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.
45
+
46
+ 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.
47
+
5
48
  ### 0.4.3 (09/12/2020)
6
49
 
7
50
  * 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.
8
51
 
9
52
  ### 0.4.2 (24/11/2020)
10
53
 
11
- ### Bugfixes
54
+ #### Bugfixes
12
55
 
13
56
  * database extensions were being run in resource server mode, when it's not expected that the oauth db tables are around.
14
57
 
15
58
  ### 0.4.1 (24/11/2020)
16
59
 
17
- ### Improvements
60
+ #### Improvements
18
61
 
19
62
  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.
20
63
 
21
- ### Bugfixes
64
+ #### Bugfixes
22
65
 
23
66
  * 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.
24
67
 
25
68
  ### 0.4.0 (13/11/2020)
26
69
 
27
- ### Features
70
+ #### Features
28
71
 
29
72
  * 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.
30
73
 
31
74
  * 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.
32
75
 
33
76
 
34
- ### Improvements
77
+ #### Improvements
35
78
 
36
79
  * 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.
37
80
 
38
- ### Bugfixes
81
+ #### Bugfixes
39
82
 
40
83
  * 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;
41
84
  * rails tests were silently not running in CI;
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 @@ module Rodauth::OAuth
9
9
  source_root "#{__dir__}/templates"
10
10
  namespace "rodauth:oauth:views"
11
11
 
12
- DEFAULT = %w[oauth_authorize].freeze
12
+ DEFAULT = %w[authorize].freeze
13
13
  VIEWS = {
14
14
  oauth_authorize: DEFAULT,
15
15
  oauth_applications: %w[oauth_applications oauth_application new_oauth_application]
@@ -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
@@ -551,12 +564,11 @@ module Rodauth
551
564
  self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
552
565
 
553
566
  # Check whether we can reutilize db entries for the same account / application pair
554
- one_oauth_token_per_account = begin
555
- db.indexes(oauth_tokens_table).values.any? do |definition|
556
- definition[:unique] &&
557
- definition[:columns] == oauth_tokens_unique_columns
558
- 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
559
570
  end
571
+
560
572
  self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
561
573
  end
562
574
 
@@ -1350,7 +1362,7 @@ module Rodauth
1350
1362
  issuer: issuer,
1351
1363
  authorization_endpoint: authorize_url,
1352
1364
  token_endpoint: token_url,
1353
- registration_endpoint: route_url(oauth_applications_path),
1365
+ registration_endpoint: oauth_applications_url,
1354
1366
  scopes_supported: oauth_application_scopes,
1355
1367
  response_types_supported: responses_supported,
1356
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.3"
5
+ VERSION = "0.6.1"
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.3
4
+ version: 0.6.1
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-12-10 00:00:00.000000000 Z
11
+ date: 2021-09-08 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.