rodauth-oauth 0.4.3 → 0.6.1

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: 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.