rodauth-oauth 0.4.0 → 0.5.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: b09ecfc1d3a8ee0f5b890620baa14ca6d847362bf38dd158e02bd2c8ebfc204e
4
- data.tar.gz: 89f0e82d7721f7ee175b1c53b7b3e0cc534e6983fe37dfc02e433df77b58225d
3
+ metadata.gz: 366fa7201a8cb26525b2abdfab0c4108a4afbef4fa8697cf57e874919eda2afd
4
+ data.tar.gz: 958b9fd8b4cd2996a85b96fac6dff6a6b35832adcdaed4d81e20842041a1299e
5
5
  SHA512:
6
- metadata.gz: d00a178f561ddecacff0587e1120b68bb22cd10b76b106b00f41167ba9c8bd8b2b8958fd629588924e502be8c947a81d3722102038cd329c006f4b4daf6efada
7
- data.tar.gz: 328542ba8ce7ef8e8f605056a9a8cbf6599136232d93f7246b13fe037ebc07225e2051b3ee454eb90ec4ae480e2b493d662d1cbbcd0ae5cc7e57a0ff29b10696
6
+ metadata.gz: fb496f7d438c0447ba4ef899f9ec37549cea355fb5c670818cd040970c1316c13442caa58aebc4ea66cd92f2f369d6ff7c39b502190e2d4fec2f95d5e27b4279
7
+ data.tar.gz: 73048d860285b29056ade97d9092103e2cae18fa9967df603ddce9c83c10fd5c44891e7282f3195d0b79b2d73bf12c748483a811f404d50fb29df40da26a6bf4
data/CHANGELOG.md CHANGED
@@ -2,26 +2,72 @@
2
2
 
3
3
  ## master
4
4
 
5
- ### 0.4.0
5
+ ### 0.5.1 (19/03/2021)
6
6
 
7
- ### Features
7
+ #### Improvements
8
+
9
+ * Changing "Callback URL" to "Redirect URL" in default templates;
10
+
11
+ #### Bugfixes
12
+
13
+ * (rails integration) Fixed templates location;
14
+ * (rails integration) Fixed migration name from generator;
15
+ * (rails integration) fixed links, html tags, styling and unassigned variables from a few view templates;
16
+ * `oauth_application_path` is now compliant with prefixes and other url helpers, while now having a `oauth_application_url` counterpart;
17
+ * (rails integration) skipping csrf checks for "/userinfo" request (OIDC)
18
+
19
+ ### 0.5.0 (08/02/2021)
20
+
21
+ #### RP-Initiated Logout
22
+
23
+ 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.
24
+
25
+ #### Security
26
+
27
+ 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.
28
+
29
+ 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.
30
+
31
+ ### 0.4.3 (09/12/2020)
32
+
33
+ * 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.
34
+
35
+ ### 0.4.2 (24/11/2020)
36
+
37
+ #### Bugfixes
38
+
39
+ * database extensions were being run in resource server mode, when it's not expected that the oauth db tables are around.
40
+
41
+ ### 0.4.1 (24/11/2020)
42
+
43
+ #### Improvements
44
+
45
+ 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.
46
+
47
+ #### Bugfixes
48
+
49
+ * 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.
50
+
51
+ ### 0.4.0 (13/11/2020)
52
+
53
+ #### Features
8
54
 
9
55
  * 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.
10
56
 
11
57
  * 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.
12
58
 
13
59
 
14
- ### Improvements
60
+ #### Improvements
15
61
 
16
62
  * 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.
17
63
 
18
- ### Bugfixes
64
+ #### Bugfixes
19
65
 
20
66
  * 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;
21
67
  * rails tests were silently not running in CI;
22
68
  * 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;
23
69
 
24
- ### 0.3.0
70
+ ### 0.3.0 (8/10/2020)
25
71
 
26
72
  #### Features
27
73
 
@@ -50,7 +96,7 @@ Use `rodauth.convert_timestamp` in the templates, whenever dates are displayed.
50
96
 
51
97
  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).
52
98
 
53
- ### 0.2.0
99
+ ### 0.2.0 (9/9/2020)
54
100
 
55
101
  #### Features
56
102
 
@@ -94,9 +140,7 @@ Fixed some mishandling of HTTP headers when in in resource-server mode.
94
140
  * 97.7% test coverage;
95
141
  * `rodauth-oauth` CI tests run against sqlite, postgresql and mysql.
96
142
 
97
- ### 0.1.0
98
-
99
- (31/7/2020)
143
+ ### 0.1.0 (31/7/2020)
100
144
 
101
145
  #### Features
102
146
 
@@ -142,9 +186,7 @@ URI schemes for client applications redirect URIs have to be `https`. In order t
142
186
  * fixed trailing "/" in the "issuer" value in server metadata (`https://server.com/` -> `https://server.com`).
143
187
 
144
188
 
145
- ### 0.0.6
146
-
147
- (6/7/2020)
189
+ ### 0.0.6 (6/7/2020)
148
190
 
149
191
  #### Features
150
192
 
@@ -167,9 +209,7 @@ The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (se
167
209
  Removed React Javascript from example applications.
168
210
 
169
211
 
170
- ### 0.0.5
171
-
172
- (26/6/2020)
212
+ ### 0.0.5 (26/6/2020)
173
213
 
174
214
  #### Features
175
215
 
@@ -206,9 +246,7 @@ It **requires** the authorization to implement the server metadata endpoint (`/.
206
246
  * option `scopes_param` renamed to `scope_param`;
207
247
  *
208
248
 
209
- ## 0.0.4
210
-
211
- (13/6/2020)
249
+ ## 0.0.4 (13/6/2020)
212
250
 
213
251
  ### Features
214
252
 
@@ -245,9 +283,7 @@ The `oauth_jwt` feature now allows the usage of access tokens to authorize the g
245
283
 
246
284
  * Fixed scope claim of JWT ("scopes" -> "scope");
247
285
 
248
- ## 0.0.3
249
-
250
- (5/6/2020)
286
+ ## 0.0.3 (5/6/2020)
251
287
 
252
288
  ### Features
253
289
 
@@ -279,9 +315,7 @@ end
279
315
  * renamed the existing `use_oauth_implicit_grant_type` to `use_oauth_implicit_grant_type?`;
280
316
  * It's now usable as JSON API (small caveat: POST authorize will still redirect on success...);
281
317
 
282
- ## 0.0.2
283
-
284
- (29/5/2020)
318
+ ## 0.0.2 (29/5/2020)
285
319
 
286
320
  ### Features
287
321
 
@@ -297,8 +331,6 @@ end
297
331
 
298
332
  * usage of client secret for authorizing the generation of tokens, as the spec mandates (and refraining from them when doing PKCE).
299
333
 
300
- ## 0.0.1
301
-
302
- (14/5/2020)
334
+ ## 0.0.1 (14/5/2020)
303
335
 
304
336
  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
@@ -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
@@ -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?
@@ -489,13 +506,13 @@ module Rodauth
489
506
  def fetch_access_token
490
507
  value = request.env["HTTP_AUTHORIZATION"]
491
508
 
492
- return unless value
509
+ return unless value && !value.empty?
493
510
 
494
511
  scheme, token = value.split(" ", 2)
495
512
 
496
513
  return unless scheme.downcase == oauth_token_type
497
514
 
498
- return if token.empty?
515
+ return if token.nil? || token.empty?
499
516
 
500
517
  token
501
518
  end
@@ -508,31 +525,34 @@ module Rodauth
508
525
 
509
526
  return unless bearer_token
510
527
 
511
- # check if token has not expired
512
- # check if token has been revoked
513
- @authorization_token = oauth_token_by_token(bearer_token)
528
+ @authorization_token = if is_authorization_server?
529
+ # check if token has not expired
530
+ # check if token has been revoked
531
+ oauth_token_by_token(bearer_token)
532
+ else
533
+ # where in resource server, NOT the authorization server.
534
+ payload = introspection_request("access_token", bearer_token)
535
+
536
+ return unless payload["active"]
537
+
538
+ payload
539
+ end
514
540
  end
515
541
 
516
542
  def require_oauth_authorization(*scopes)
517
- token_scopes = if is_authorization_server?
518
- authorization_required unless authorization_token
543
+ authorization_required unless authorization_token
519
544
 
520
- scopes << oauth_application_default_scope if scopes.empty?
545
+ scopes << oauth_application_default_scope if scopes.empty?
521
546
 
547
+ token_scopes = if is_authorization_server?
522
548
  authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
523
549
  else
524
- bearer_token = fetch_access_token
525
-
526
- authorization_required unless bearer_token
527
-
528
- scopes << oauth_application_default_scope if scopes.empty?
529
-
530
- # where in resource server, NOT the authorization server.
531
- payload = introspection_request("access_token", bearer_token)
532
-
533
- authorization_required unless payload["active"]
534
-
535
- payload["scope"].split(oauth_scope_separator)
550
+ aux_scopes = authorization_token["scope"]
551
+ if aux_scopes
552
+ aux_scopes.split(oauth_scope_separator)
553
+ else
554
+ []
555
+ end
536
556
  end
537
557
 
538
558
  authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
@@ -540,6 +560,11 @@ module Rodauth
540
560
 
541
561
  def post_configure
542
562
  super
563
+
564
+ # all of the extensions below involve DB changes. Resource server mode doesn't use
565
+ # database functions for OAuth though.
566
+ return unless is_authorization_server?
567
+
543
568
  self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
544
569
 
545
570
  # Check whether we can reutilize db entries for the same account / application pair
@@ -616,9 +641,9 @@ module Rodauth
616
641
  http.use_ssl = auth_url.scheme == "https"
617
642
 
618
643
  request = Net::HTTP::Post.new(introspect_path)
619
- request["content-type"] = json_response_content_type
644
+ request["content-type"] = "application/x-www-form-urlencoded"
620
645
  request["accept"] = json_response_content_type
621
- request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
646
+ request.set_form_data({ "token_type_hint" => token_type_hint, "token" => token })
622
647
 
623
648
  before_introspection_request(request)
624
649
  response = http.request(request)
@@ -1342,7 +1367,7 @@ module Rodauth
1342
1367
  issuer: issuer,
1343
1368
  authorization_endpoint: authorize_url,
1344
1369
  token_endpoint: token_url,
1345
- registration_endpoint: route_url(oauth_applications_path),
1370
+ registration_endpoint: oauth_applications_url,
1346
1371
  scopes_supported: oauth_application_scopes,
1347
1372
  response_types_supported: responses_supported,
1348
1373
  response_modes_supported: response_modes_supported,
@@ -8,6 +8,8 @@ module Rodauth
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
@@ -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.0"
5
+ VERSION = "0.5.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.0
4
+ version: 0.5.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-11-13 00:00:00.000000000 Z
11
+ date: 2021-03-19 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.3
75
75
  signing_key:
76
76
  specification_version: 4
77
77
  summary: Implementation of the OAuth 2.0 protocol on top of rodauth.