rodauth-oauth 0.4.0 → 0.5.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: 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.