rodauth-oauth 0.3.0 → 0.4.0

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