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 +4 -4
- data/CHANGELOG.md +21 -2
- data/lib/rodauth/features/oauth.rb +52 -19
- data/lib/rodauth/features/oauth_http_mac.rb +6 -10
- data/lib/rodauth/features/oidc.rb +43 -32
- data/lib/rodauth/oauth/ttl_store.rb +1 -1
- data/lib/rodauth/oauth/version.rb +1 -1
- data/templates/authorize.str +1 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b09ecfc1d3a8ee0f5b890620baa14ca6d847362bf38dd158e02bd2c8ebfc204e
|
4
|
+
data.tar.gz: 89f0e82d7721f7ee175b1c53b7b3e0cc534e6983fe37dfc02e433df77b58225d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d00a178f561ddecacff0587e1120b68bb22cd10b76b106b00f41167ba9c8bd8b2b8958fd629588924e502be8c947a81d3722102038cd329c006f4b4daf6efada
|
7
|
+
data.tar.gz: 328542ba8ce7ef8e8f605056a9a8cbf6599136232d93f7246b13fe037ebc07225e2051b3ee454eb90ec4ae480e2b493d662d1cbbcd0ae5cc7e57a0ff29b10696
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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
|
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
|
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(
|
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
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
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
|
-
|
954
|
+
response_params["state"] = param("state") if param_or_nil("state")
|
921
955
|
|
922
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
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[
|
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
|
-
|
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 =
|
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
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
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
|
-
|
267
|
-
|
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 "`
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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:
|
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],
|
data/templates/authorize.str
CHANGED
@@ -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.
|
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-
|
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.
|
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.
|