rodauth-oauth 0.3.0 → 0.4.0

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: 24f3563d467a065dc119cbe53c0f14c8f28654d6512de0b7fd09a99e3f4aabdb
4
- data.tar.gz: 8abc6dc62b463885f7198cafec57fe707f99885e67754be1d7cefbbefa31a834
3
+ metadata.gz: b09ecfc1d3a8ee0f5b890620baa14ca6d847362bf38dd158e02bd2c8ebfc204e
4
+ data.tar.gz: 89f0e82d7721f7ee175b1c53b7b3e0cc534e6983fe37dfc02e433df77b58225d
5
5
  SHA512:
6
- metadata.gz: 9fd172c9930f1cf88239a8f5ba7d5c93dc9c92b05a601c6f909b5ad12e4319ce0aa093831b93dad93542fdda0cdc1694b001ca78922239e80a2370472373b9a5
7
- data.tar.gz: 26e8e9c619425213f2cd5f21e7710f9561c0b0395bd8928656584c88586f4197bf80a765d9a90baea604ef9fdaff5c27f4b8447adf3fa617463e26b7b0a08470
6
+ metadata.gz: d00a178f561ddecacff0587e1120b68bb22cd10b76b106b00f41167ba9c8bd8b2b8958fd629588924e502be8c947a81d3722102038cd329c006f4b4daf6efada
7
+ data.tar.gz: 328542ba8ce7ef8e8f605056a9a8cbf6599136232d93f7246b13fe037ebc07225e2051b3ee454eb90ec4ae480e2b493d662d1cbbcd0ae5cc7e57a0ff29b10696
@@ -2,18 +2,37 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ### 0.4.0
6
+
7
+ ### Features
8
+
9
+ * 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
+
11
+ * 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
+
13
+
14
+ ### Improvements
15
+
16
+ * 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
+
18
+ ### Bugfixes
19
+
20
+ * 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
+ * rails tests were silently not running in CI;
22
+ * 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
+
5
24
  ### 0.3.0
6
25
 
7
26
  #### Features
8
27
 
9
- * `oauth_refresh_token_protection_policy` is a new option, which can be used to set a protection policy around usage of refresh tokens. By default it's `none`, for backwards-compatibility. However, when set to `rotation`, refresh tokens will be "use-once", i.e. a token refresh request will generate a new refresh token. Also, refresh token requests doen with already-used refresh tokens will be interpreted as a security breach, i.e. all tokens linked to the compromised refresh token will be revoked.
28
+ * `oauth_refresh_token_protection_policy` is a new option, which can be used to set a protection policy around usage of refresh tokens. By default it's `none`, for backwards-compatibility. However, when set to `rotation`, refresh tokens will be "use-once", i.e. a token refresh request will generate a new refresh token. Also, refresh token requests performed with already-used refresh tokens will be interpreted as a security breach, i.e. all tokens linked to the compromised refresh token will be revoked.
10
29
 
11
30
  #### Improvements
12
31
 
13
32
 
14
33
  * Support for the OIDC authorize [`prompt` parameter](https://openid.net/specs/openid-connect-core-1_0.html) (sectionn 3.1.2.1). It supports the `none`, `login` and `consent` out-of-the-box, while providing support for `select-account` when paired with [rodauth-select-account, a rodauth feature to handle multiple accounts in the same session](https://gitlab.com/honeyryderchuck/rodauth-select-account).
15
34
 
16
- * Refresh Tokens are now expired. The refresh token expiration period is governed by the `oauth_refresh_token_expires_in` option (default: 1 year), and is the period for which a refresh token can be used after its respective token expired.
35
+ * Refresh Tokens are now expirable. The refresh token expiration period is governed by the `oauth_refresh_token_expires_in` option (default: 1 year), and is the period for which a refresh token can be used after its respective access token expired.
17
36
 
18
37
  #### Bugfixes
19
38
 
@@ -84,6 +84,7 @@ module Rodauth
84
84
 
85
85
  auth_value_method :oauth_require_pkce, false
86
86
  auth_value_method :oauth_pkce_challenge_method, "S256"
87
+ auth_value_method :oauth_response_mode, "query"
87
88
 
88
89
  auth_value_method :oauth_valid_uri_schemes, %w[https]
89
90
 
@@ -100,6 +101,7 @@ module Rodauth
100
101
  button "Register", "oauth_application"
101
102
  button "Authorize", "oauth_authorize"
102
103
  button "Revoke", "oauth_token_revoke"
104
+ button "Back to Client Application", "oauth_authorize_post"
103
105
 
104
106
  # OAuth Token
105
107
  auth_value_method :oauth_tokens_path, "oauth-tokens"
@@ -313,11 +315,41 @@ module Rodauth
313
315
  r.post do
314
316
  redirect_url = URI.parse(redirect_uri)
315
317
 
316
- transaction do
318
+ params, mode = transaction do
317
319
  before_authorize
318
- do_authorize(redirect_url)
320
+ do_authorize
321
+ end
322
+
323
+ case mode
324
+ when "query"
325
+ params = params.map { |k, v| "#{k}=#{v}" }
326
+ params << redirect_url.query if redirect_url.query
327
+ redirect_url.query = params.join("&")
328
+ redirect(redirect_url.to_s)
329
+ when "fragment"
330
+ params = params.map { |k, v| "#{k}=#{v}" }
331
+ params << redirect_url.query if redirect_url.query
332
+ redirect_url.fragment = params.join("&")
333
+ redirect(redirect_url.to_s)
334
+ when "form_post"
335
+ scope.view layout: false, inline: <<-FORM
336
+ <html>
337
+ <head><title>Authorized</title></head>
338
+ <body onload="javascript:document.forms[0].submit()">
339
+ <form method="post" action="#{redirect_uri}">
340
+ #{
341
+ params.map do |name, value|
342
+ "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
343
+ end.join
344
+ }
345
+ <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
346
+ </form>
347
+ </body>
348
+ </html>
349
+ FORM
350
+ when "none"
351
+ redirect(redirect_url.to_s)
319
352
  end
320
- redirect(redirect_url.to_s)
321
353
  end
322
354
  end
323
355
 
@@ -848,6 +880,9 @@ module Rodauth
848
880
  end
849
881
  redirect_response_error("invalid_scope") unless check_valid_scopes?
850
882
 
883
+ if (response_mode = param_or_nil("response_mode")) && response_mode != "form_post"
884
+ redirect_response_error("invalid_request")
885
+ end
851
886
  validate_pkce_challenge_params if use_oauth_pkce?
852
887
  end
853
888
 
@@ -899,28 +934,26 @@ module Rodauth
899
934
  create_params[oauth_grants_code_column]
900
935
  end
901
936
 
902
- def do_authorize(redirect_url, query_params = [], fragment_params = [])
937
+ def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
903
938
  case param("response_type")
904
939
  when "token"
905
940
  redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
906
941
 
907
- fragment_params.replace(_do_authorize_token.map { |k, v| "#{k}=#{v}" })
908
- when "code", "", nil
909
- query_params.replace(_do_authorize_code.map { |k, v| "#{k}=#{v}" })
910
- end
911
-
912
- if param_or_nil("state")
913
- if !fragment_params.empty?
914
- fragment_params << "state=#{param('state')}"
915
- else
916
- query_params << "state=#{param('state')}"
917
- end
942
+ response_mode ||= "fragment"
943
+ response_params.replace(_do_authorize_token)
944
+ when "code"
945
+ response_mode ||= "query"
946
+ response_params.replace(_do_authorize_code)
947
+ when "none"
948
+ response_mode ||= "none"
949
+ when "", nil
950
+ response_mode ||= oauth_response_mode
951
+ response_params.replace(_do_authorize_code)
918
952
  end
919
953
 
920
- query_params << redirect_url.query if redirect_url.query
954
+ response_params["state"] = param("state") if param_or_nil("state")
921
955
 
922
- redirect_url.query = query_params.join("&") unless query_params.empty?
923
- redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
956
+ [response_params, response_mode]
924
957
  end
925
958
 
926
959
  def _do_authorize_code
@@ -1296,7 +1329,7 @@ module Rodauth
1296
1329
  issuer += "/#{path}" if path
1297
1330
 
1298
1331
  responses_supported = %w[code]
1299
- response_modes_supported = %w[query]
1332
+ response_modes_supported = %w[query form_post]
1300
1333
  grant_types_supported = %w[authorization_code]
1301
1334
 
1302
1335
  if use_oauth_implicit_grant_type?
@@ -8,20 +8,16 @@ module Rodauth
8
8
  def delete_suffix(suffix)
9
9
  suffix = suffix.to_s
10
10
  len = suffix.length
11
- if len.positive? && index(suffix, -len)
12
- self[0...-len]
13
- else
14
- dup
15
- end
11
+ return dup unless len.positive? && index(suffix, -len)
12
+
13
+ self[0...-len]
16
14
  end
17
15
 
18
16
  def delete_prefix(prefix)
19
17
  prefix = prefix.to_s
20
- if rindex(prefix, 0)
21
- self[prefix.length..-1]
22
- else
23
- dup
24
- end
18
+ return dup unless rindex(prefix, 0)
19
+
20
+ self[prefix.length..-1]
25
21
  end
26
22
  end
27
23
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:oidc) do
5
+ # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
5
6
  OIDC_SCOPES_MAP = {
6
7
  "profile" => %i[name family_name given_name middle_name nickname preferred_username
7
8
  profile picture website gender birthdate zoneinfo locale updated_at].freeze,
8
9
  "email" => %i[email email_verified].freeze,
9
- "address" => %i[address].freeze,
10
+ "address" => %i[formatted street_address locality region postal_code country].freeze,
10
11
  "phone" => %i[phone_number phone_number_verified].freeze
11
12
  }.freeze
12
13
 
@@ -74,7 +75,7 @@ module Rodauth
74
75
  auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
75
76
  auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
76
77
 
77
- auth_value_methods(:get_oidc_param)
78
+ auth_value_methods(:get_oidc_param, :get_additional_param)
78
79
 
79
80
  # /userinfo
80
81
  route(:userinfo) do |r|
@@ -245,8 +246,11 @@ module Rodauth
245
246
  oauth_token[:id_token] = jwt_encode(id_token_claims)
246
247
  end
247
248
 
249
+ # aka fill_with_standard_claims
248
250
  def fill_with_account_claims(claims, account, scopes)
249
- scopes_by_oidc = scopes.each_with_object({}) do |scope, by_oidc|
251
+ scopes_by_claim = scopes.each_with_object({}) do |scope, by_oidc|
252
+ next if scope == "openid"
253
+
250
254
  oidc, param = scope.split(".", 2)
251
255
 
252
256
  by_oidc[oidc] ||= []
@@ -254,21 +258,33 @@ module Rodauth
254
258
  by_oidc[oidc] << param.to_sym if param
255
259
  end
256
260
 
257
- oidc_scopes = (OIDC_SCOPES_MAP.keys & scopes_by_oidc.keys)
258
-
259
- return if oidc_scopes.empty?
261
+ oidc_scopes, additional_scopes = scopes_by_claim.keys.partition { |key| OIDC_SCOPES_MAP.key?(key) }
260
262
 
261
- if respond_to?(:get_oidc_param)
262
- oidc_scopes.each do |scope|
263
- params = scopes_by_oidc[scope]
264
- params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
263
+ unless oidc_scopes.empty?
264
+ if respond_to?(:get_oidc_param)
265
+ oidc_scopes.each do |scope|
266
+ scope_claims = claims
267
+ params = scopes_by_claim[scope]
268
+ params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
265
269
 
266
- params.each do |param|
267
- claims[param] = __send__(:get_oidc_param, account, param)
270
+ scope_claims = (claims["address"] = {}) if scope == "address"
271
+ params.each do |param|
272
+ scope_claims[param] = __send__(:get_oidc_param, account, param)
273
+ end
268
274
  end
275
+ else
276
+ warn "`get_oidc_param(account, claim)` must be implemented to use oidc scopes."
277
+ end
278
+ end
279
+
280
+ return if additional_scopes.empty?
281
+
282
+ if respond_to?(:get_additional_param)
283
+ additional_scopes.each do |scope|
284
+ claims[scope] = __send__(:get_additional_param, account, scope.to_sym)
269
285
  end
270
286
  else
271
- warn "`get_oidc_param(token, param)` must be implemented to use oidc scopes."
287
+ warn "`get_additional_param(account, claim)` must be implemented to use oidc scopes."
272
288
  end
273
289
  end
274
290
 
@@ -290,33 +306,27 @@ module Rodauth
290
306
  end
291
307
  end
292
308
 
293
- def do_authorize(redirect_url, query_params = [], fragment_params = [])
309
+ def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
294
310
  return super unless use_oauth_implicit_grant_type?
295
311
 
296
312
  case param("response_type")
297
313
  when "id_token"
298
- fragment_params.replace(_do_authorize_id_token.map { |k, v| "#{k}=#{v}" })
314
+ response_params.replace(_do_authorize_id_token)
299
315
  when "code token"
300
316
  redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
301
317
 
302
- params = _do_authorize_code.merge(_do_authorize_token)
303
-
304
- fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
318
+ response_params.replace(_do_authorize_code.merge(_do_authorize_token))
305
319
  when "code id_token"
306
- params = _do_authorize_code.merge(_do_authorize_id_token)
307
-
308
- fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
320
+ response_params.replace(_do_authorize_code.merge(_do_authorize_id_token))
309
321
  when "id_token token"
310
- params = _do_authorize_id_token.merge(_do_authorize_token)
311
-
312
- fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
322
+ response_params.replace(_do_authorize_id_token.merge(_do_authorize_token))
313
323
  when "code id_token token"
314
- params = _do_authorize_code.merge(_do_authorize_id_token).merge(_do_authorize_token)
315
324
 
316
- fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
325
+ response_params.replace(_do_authorize_code.merge(_do_authorize_id_token).merge(_do_authorize_token))
317
326
  end
327
+ response_mode ||= "fragment" unless response_params.empty?
318
328
 
319
- super(redirect_url, query_params, fragment_params)
329
+ super(response_params, response_mode)
320
330
  end
321
331
 
322
332
  def _do_authorize_id_token
@@ -351,13 +361,14 @@ module Rodauth
351
361
 
352
362
  scope_claims.unshift("auth_time") if last_account_login_at
353
363
 
364
+ response_types_supported = metadata[:response_types_supported]
365
+ if use_oauth_implicit_grant_type?
366
+ response_types_supported += ["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"]
367
+ end
368
+
354
369
  metadata.merge(
355
370
  userinfo_endpoint: userinfo_url,
356
- response_types_supported: metadata[:response_types_supported] +
357
- ["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"],
358
- response_modes_supported: %w[query fragment],
359
- grant_types_supported: %w[authorization_code implicit],
360
-
371
+ response_types_supported: response_types_supported,
361
372
  subject_types_supported: [oauth_jwt_subject_type],
362
373
 
363
374
  id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
@@ -13,7 +13,7 @@ class Rodauth::OAuth::TtlStore
13
13
 
14
14
  def initialize
15
15
  @store_mutex = Mutex.new
16
- @store = Hash.new {}
16
+ @store = {}
17
17
  end
18
18
 
19
19
  def [](key)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -20,6 +20,7 @@
20
20
 
21
21
  #{"<input type=\"hidden\" name=\"access_type\" value=\"#{rodauth.param("access_type")}\"/>" if rodauth.param_or_nil("access_type")}
22
22
  #{"<input type=\"hidden\" name=\"response_type\" value=\"#{rodauth.param("response_type")}\"/>" if rodauth.param_or_nil("response_type")}
23
+ #{"<input type=\"hidden\" name=\"response_mode\" value=\"#{rodauth.param("response_mode")}\"/>" if rodauth.param_or_nil("response_mode")}
23
24
  #{"<input type=\"hidden\" name=\"state\" value=\"#{rodauth.param("state")}\"/>" if rodauth.param_or_nil("state")}
24
25
  #{"<input type=\"hidden\" name=\"nonce\" value=\"#{rodauth.param("nonce")}\"/>" if rodauth.param_or_nil("nonce")}
25
26
  #{"<input type=\"hidden\" name=\"redirect_uri\" value=\"#{rodauth.redirect_uri}\"/>" if rodauth.param_or_nil("redirect_uri")}
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-08 00:00:00.000000000 Z
11
+ date: 2020-11-13 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:
@@ -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.2
74
+ rubygems_version: 3.1.4
75
75
  signing_key:
76
76
  specification_version: 4
77
77
  summary: Implementation of the OAuth 2.0 protocol on top of rodauth.