rodauth-oauth 1.2.0 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/doc/release_notes/1_1_0.md +1 -1
- data/doc/release_notes/1_3_0.md +38 -0
- data/doc/release_notes/1_3_1.md +10 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +24 -21
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +11 -10
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +54 -32
- data/lib/rodauth/features/oauth_authorize_base.rb +8 -0
- data/lib/rodauth/features/oauth_base.rb +19 -17
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +7 -7
- data/lib/rodauth/features/oauth_implicit_grant.rb +22 -4
- data/lib/rodauth/features/oauth_jwt_secured_authorization_response_mode.rb +126 -0
- data/lib/rodauth/features/oauth_management_base.rb +1 -3
- data/lib/rodauth/features/oauth_pkce.rb +1 -2
- data/lib/rodauth/features/oidc.rb +87 -50
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +39 -1
- data/lib/rodauth/features/oidc_self_issued.rb +73 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/templates/authorize.str +1 -0
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac42aa0fb7d65030b403957a408d6d5b1f999614c957ec8176d814030ddb9381
|
4
|
+
data.tar.gz: f67cf98dfdb162d1e015d3930b569756f2a25737a64e68ed36fcf31e2d672be2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e86da1d43f30dfb18e1ae530dc5b1cc9cf69903d32a39bade2dc5881075140d79ef5644159ff40b1406b58dfb12197c79d13f683f20b431330d02d452b3cf87e
|
7
|
+
data.tar.gz: ad35b6bc881f1e22f7cb7227115daee9556ebeada9e10b21e8ecfeb9dc0e7a51ddd606ef481f1125064e49d6ee39076bd8bc2043d58d0d103df6a27813a022db
|
data/README.md
CHANGED
@@ -17,6 +17,7 @@ This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framew
|
|
17
17
|
* Config OP
|
18
18
|
* Dynamic OP
|
19
19
|
* Form Post OP
|
20
|
+
* 3rd Party-Init OP
|
20
21
|
|
21
22
|
(it also passes the conformance tests for the RP-Initiated Logout OP).
|
22
23
|
|
@@ -39,6 +40,7 @@ This gem implements the following RFCs and features of OAuth:
|
|
39
40
|
* `oauth_tls_client_auth` - [Mutual-TLS Client Authentication](https://datatracker.ietf.org/doc/html/rfc8705);
|
40
41
|
* `oauth_jwt` - [JWT Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
|
41
42
|
* `oauth_jwt_secured_authorization_request` - [JWT Secured Authorization Request](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
|
43
|
+
* `oauth_jwt_secured_authorization_response_mode` - [JWT Secured Authorization Response_mode](https://openid.net/specs/openid-financial-api-jarm.html);
|
42
44
|
* `oauth_resource_indicators` - [Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707);
|
43
45
|
* Access Type (Token refresh online and offline);
|
44
46
|
* `oauth_http_mac` - [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
|
data/doc/release_notes/1_1_0.md
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
## 1.3.0 (02/04/2023)
|
2
|
+
|
3
|
+
## Features
|
4
|
+
|
5
|
+
### Self-Signed Issued Tokens
|
6
|
+
|
7
|
+
`rodauth-oauth` supports self-signed issued tokens, via the `oidc_self_issued` feature.
|
8
|
+
|
9
|
+
More info about the feature [in the docs](https://gitlab.com/os85/rodauth-oauth/-/wikis/Self-Issued-OpenID).
|
10
|
+
|
11
|
+
#### JARM
|
12
|
+
|
13
|
+
`rodauth-oauth` supports JWT-secured Authorization Response Mode, also known as JARM, via the `oauth_jwt_secured_authorization_response_mode`.
|
14
|
+
|
15
|
+
More info about the feature [in the docs](https://gitlab.com/os85/rodauth-oauth/-/wikis/JWT-Secured-Authorization-Response-Mode).
|
16
|
+
|
17
|
+
## Improvements
|
18
|
+
|
19
|
+
### `fill_with_account_claims` auth method
|
20
|
+
|
21
|
+
`fill_with_account_claims` is now exposed as an auth method. This allows one to override to be able to cover certain requirements, such as aggregated and distributed claims. Here's a [link to the docs](https://gitlab.com/os85/rodauth-oauth/-/wikis/Id-Token-Authentication#claim-types) explaining how to do it.
|
22
|
+
|
23
|
+
### oidc: only generate refresh token when `offline_access` scope is used.
|
24
|
+
|
25
|
+
When the `oidc` feature is used, refresh tokens won't be generated anymore by default; in order to do so, the `offline_access` needs to be requested for in the respective authorization request, [as the spec mandates](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess).
|
26
|
+
|
27
|
+
### oidc: implicit grant loaded by default
|
28
|
+
|
29
|
+
The `oidc` feature now loads the `oauth_implicit_grant` feature by default. This hadn't been done before due to the wish to ship a secure integration by default, but since then, spec compliance became more prioritary, and this is a requirement.
|
30
|
+
|
31
|
+
## Bugfixes
|
32
|
+
|
33
|
+
* rails integration: activerecord migrations fixes:
|
34
|
+
* use `bigint` for foreign keys;
|
35
|
+
* index creation instruction with the wrong syntax;
|
36
|
+
* set precision 6 for default timestamps, to comply with AR defaults;
|
37
|
+
* add missing `code` column to the `oauth_pushed_requests` table;
|
38
|
+
* oidc: when using the `id_token` , or any composite response type including `id_token`, using any response mode other than `fragment` will result in an invalid request.
|
@@ -0,0 +1,10 @@
|
|
1
|
+
### 1.3.1 (27/06/2023)
|
2
|
+
|
3
|
+
#### Bugfixes
|
4
|
+
|
5
|
+
* Set 401 error response when `client_id` parameter is invalid, or from an unexisting client application, instead of failing with a 500 (@igor-alexandrov).
|
6
|
+
* update rails authorize form to use roda request params instead, as plain params was breaking JAR and PAR-based authorize forms in rails applications.
|
7
|
+
|
8
|
+
#### Chore
|
9
|
+
|
10
|
+
* set `:padding` to `false` in `Base64.urlsafe_encode64` calls (@felipe.zavan)
|
@@ -37,10 +37,10 @@
|
|
37
37
|
</div>
|
38
38
|
<% end %>
|
39
39
|
<% end %>
|
40
|
-
<%= hidden_field_tag :client_id,
|
41
|
-
<% %
|
42
|
-
<% if
|
43
|
-
<%= hidden_field_tag oauth_param,
|
40
|
+
<%= hidden_field_tag :client_id, rodauth.raw_param("client_id") %>
|
41
|
+
<% %w[access_type response_type response_mode state redirect_uri].each do |oauth_param| %>
|
42
|
+
<% if rodauth.raw_param(oauth_param) %>
|
43
|
+
<%= hidden_field_tag oauth_param, rodauth.raw_param(oauth_param) %>
|
44
44
|
<% end %>
|
45
45
|
<% end %>
|
46
46
|
<% if rodauth.features.include?(:oauth_resource_indicators) && rodauth.resource_indicators %>
|
@@ -49,36 +49,39 @@
|
|
49
49
|
<% end %>
|
50
50
|
<% end %>
|
51
51
|
<% if rodauth.features.include?(:oauth_pkce) %>
|
52
|
-
<% if
|
53
|
-
<%= hidden_field_tag :code_challenge,
|
52
|
+
<% if rodauth.raw_param("code_challenge") %>
|
53
|
+
<%= hidden_field_tag :code_challenge, rodauth.raw_param("code_challenge") %>
|
54
54
|
<% end %>
|
55
|
-
<% if
|
56
|
-
<%= hidden_field_tag :code_challenge_method,
|
55
|
+
<% if rodauth.raw_param("code_challenge_method") %>
|
56
|
+
<%= hidden_field_tag :code_challenge_method, rodauth.raw_param("code_challenge_method") %>
|
57
57
|
<% end %>
|
58
58
|
<% end %>
|
59
59
|
<% if rodauth.features.include?(:oidc) %>
|
60
|
-
<% if
|
61
|
-
<%= hidden_field_tag :prompt,
|
60
|
+
<% if rodauth.raw_param("prompt") %>
|
61
|
+
<%= hidden_field_tag :prompt, rodauth.raw_param("prompt") %>
|
62
62
|
<% end %>
|
63
|
-
<% if
|
64
|
-
<%= hidden_field_tag :nonce,
|
63
|
+
<% if rodauth.raw_param("nonce") %>
|
64
|
+
<%= hidden_field_tag :nonce, rodauth.raw_param("nonce") %>
|
65
65
|
<% end %>
|
66
|
-
<% if
|
67
|
-
<%= hidden_field_tag :ui_locales,
|
66
|
+
<% if rodauth.raw_param("ui_locales") %>
|
67
|
+
<%= hidden_field_tag :ui_locales, rodauth.raw_param("ui_locales") %>
|
68
68
|
<% end %>
|
69
|
-
<% if
|
70
|
-
<%= hidden_field_tag :claims_locales,
|
69
|
+
<% if rodauth.raw_param("claims_locales") %>
|
70
|
+
<%= hidden_field_tag :claims_locales, rodauth.raw_param("claims_locales") %>
|
71
71
|
<% end %>
|
72
|
-
<% if
|
73
|
-
<%= hidden_field_tag :claims,
|
72
|
+
<% if rodauth.raw_param("claims") %>
|
73
|
+
<%= hidden_field_tag :claims, sanitize(rodauth.raw_param("claims")) %>
|
74
74
|
<% end %>
|
75
|
-
<% if
|
76
|
-
<%= hidden_field_tag :acr_values,
|
75
|
+
<% if rodauth.raw_param("acr_values") %>
|
76
|
+
<%= hidden_field_tag :acr_values, rodauth.raw_param("acr_values") %>
|
77
|
+
<% end %>
|
78
|
+
<% if rodauth.raw_param("registration") %>
|
79
|
+
<%= hidden_field_tag :registration, rodauth.raw_param("registration") %>
|
77
80
|
<% end %>
|
78
81
|
<% end %>
|
79
82
|
</div>
|
80
83
|
<p class="text-center">
|
81
84
|
<%= submit_tag rodauth.oauth_authorize_button, class: "btn btn-outline-primary" %>
|
82
|
-
<%= link_to rodauth.oauth_cancel_button, "#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{"&state=\#{CGI.escape(rodauth.state)}" if
|
85
|
+
<%= link_to rodauth.oauth_cancel_button, "#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{"&state=\#{CGI.escape(rodauth.state)}" if rodauth.raw_param("state") }", class: "btn btn-outline-danger" %>
|
83
86
|
</p>
|
84
87
|
<% end %>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
2
2
|
def change
|
3
3
|
create_table :oauth_applications do |t|
|
4
|
-
t.
|
4
|
+
t.bigint :account_id
|
5
5
|
t.foreign_key :accounts, column: :account_id
|
6
6
|
t.string :name, null: false
|
7
7
|
t.string :description, null: true
|
@@ -11,7 +11,7 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
11
11
|
t.string :client_secret, null: false, index: { unique: true }
|
12
12
|
t.string :registration_access_token, null: true
|
13
13
|
t.string :scopes, null: false
|
14
|
-
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
14
|
+
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP(6)" }
|
15
15
|
|
16
16
|
# :oauth_dynamic_client_configuration enabled, extra optional params
|
17
17
|
t.string :token_endpoint_auth_method, null: true
|
@@ -61,20 +61,20 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
61
61
|
end
|
62
62
|
|
63
63
|
create_table :oauth_grants do |t|
|
64
|
-
t.
|
64
|
+
t.bigint :account_id
|
65
65
|
t.foreign_key :accounts, column: :account_id
|
66
|
-
t.
|
66
|
+
t.bigint :oauth_application_id
|
67
67
|
t.foreign_key :oauth_applications, column: :oauth_application_id
|
68
68
|
t.string :type, null: true
|
69
69
|
t.string :code, null: true
|
70
70
|
t.index(%i[oauth_application_id code], unique: true)
|
71
|
-
t.string :token, unique: true
|
72
|
-
t.string :refresh_token, unique: true
|
71
|
+
t.string :token, index: { unique: true }
|
72
|
+
t.string :refresh_token, index: { unique: true }
|
73
73
|
t.datetime :expires_in, null: false
|
74
74
|
t.string :redirect_uri
|
75
75
|
t.datetime :revoked_at
|
76
76
|
t.string :scopes, null: false
|
77
|
-
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
77
|
+
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP(6)" }
|
78
78
|
t.string :access_type, null: false, default: "offline"
|
79
79
|
|
80
80
|
# :oauth_pkce enabled
|
@@ -82,7 +82,7 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
82
82
|
t.string :code_challenge_method
|
83
83
|
|
84
84
|
# :oauth_device_code_grant enabled
|
85
|
-
t.string :user_code, null: true, unique: true
|
85
|
+
t.string :user_code, null: true, index: { unique: true }
|
86
86
|
t.datetime :last_polled_at, null: true
|
87
87
|
|
88
88
|
# :oauth_tls_client_auth
|
@@ -99,11 +99,12 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
99
99
|
end
|
100
100
|
|
101
101
|
create_table :oauth_pushed_requests do |t|
|
102
|
-
t.
|
102
|
+
t.bigint :oauth_application_id
|
103
103
|
t.foreign_key :oauth_applications, column: :oauth_application_id
|
104
|
+
t.string :code, null: false, index: { unique: true }
|
104
105
|
t.string :params, null: false
|
105
106
|
t.datetime :expires_in, null: false
|
106
107
|
t.index %i[oauth_application_id code], unique: true
|
107
108
|
end
|
108
109
|
end
|
109
|
-
end
|
110
|
+
end
|
@@ -27,7 +27,19 @@ module Rodauth
|
|
27
27
|
|
28
28
|
response_mode = param_or_nil("response_mode")
|
29
29
|
|
30
|
-
|
30
|
+
return unless response_mode
|
31
|
+
|
32
|
+
redirect_response_error("invalid_request") unless oauth_response_modes_supported.include?(response_mode)
|
33
|
+
|
34
|
+
response_type = param_or_nil("response_type")
|
35
|
+
|
36
|
+
return unless response_type.nil? || response_type == "code"
|
37
|
+
|
38
|
+
redirect_response_error("invalid_request") unless oauth_response_modes_for_code_supported.include?(response_mode)
|
39
|
+
end
|
40
|
+
|
41
|
+
def oauth_response_modes_for_code_supported
|
42
|
+
%w[query form_post]
|
31
43
|
end
|
32
44
|
|
33
45
|
def validate_token_params
|
@@ -67,55 +79,65 @@ module Rodauth
|
|
67
79
|
redirect_url = URI.parse(redirect_uri)
|
68
80
|
case mode
|
69
81
|
when "query"
|
70
|
-
params =
|
82
|
+
params = [URI.encode_www_form(params)]
|
71
83
|
params << redirect_url.query if redirect_url.query
|
72
84
|
redirect_url.query = params.join("&")
|
73
85
|
redirect(redirect_url.to_s)
|
74
86
|
when "form_post"
|
75
|
-
|
76
|
-
|
77
|
-
<
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
params.map do |name, value|
|
82
|
-
"<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
|
83
|
-
end.join
|
84
|
-
}
|
85
|
-
<input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
|
86
|
-
</form>
|
87
|
-
</body>
|
88
|
-
</html>
|
89
|
-
FORM
|
87
|
+
inline_html = form_post_response_html(redirect_uri) do
|
88
|
+
params.map do |name, value|
|
89
|
+
"<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
|
90
|
+
end.join
|
91
|
+
end
|
92
|
+
scope.view layout: false, inline: inline_html
|
90
93
|
end
|
91
94
|
end
|
92
95
|
|
93
|
-
def _redirect_response_error(redirect_url,
|
96
|
+
def _redirect_response_error(redirect_url, params)
|
94
97
|
response_mode = param_or_nil("response_mode") || oauth_response_mode
|
95
98
|
|
96
99
|
case response_mode
|
97
100
|
when "form_post"
|
98
101
|
response["Content-Type"] = "text/html"
|
99
|
-
|
100
|
-
|
101
|
-
<
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
query_params.map do |name, value|
|
106
|
-
"<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
|
107
|
-
end.join
|
108
|
-
}
|
109
|
-
</form>
|
110
|
-
</body>
|
111
|
-
</html>
|
112
|
-
FORM
|
102
|
+
error_body = form_post_error_response_html(redirect_url) do
|
103
|
+
params.map do |name, value|
|
104
|
+
"<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
|
105
|
+
end.join
|
106
|
+
end
|
107
|
+
response.write(error_body)
|
113
108
|
request.halt
|
114
109
|
else
|
115
110
|
super
|
116
111
|
end
|
117
112
|
end
|
118
113
|
|
114
|
+
def form_post_response_html(url)
|
115
|
+
<<-FORM
|
116
|
+
<html>
|
117
|
+
<head><title>Authorized</title></head>
|
118
|
+
<body onload="javascript:document.forms[0].submit()">
|
119
|
+
<form method="post" action="#{url}">
|
120
|
+
#{yield}
|
121
|
+
<input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
|
122
|
+
</form>
|
123
|
+
</body>
|
124
|
+
</html>
|
125
|
+
FORM
|
126
|
+
end
|
127
|
+
|
128
|
+
def form_post_error_response_html(url)
|
129
|
+
<<-FORM
|
130
|
+
<html>
|
131
|
+
<head><title></title></head>
|
132
|
+
<body onload="javascript:document.forms[0].submit()">
|
133
|
+
<form method="post" action="#{url}">
|
134
|
+
#{yield}
|
135
|
+
</form>
|
136
|
+
</body>
|
137
|
+
</html>
|
138
|
+
FORM
|
139
|
+
end
|
140
|
+
|
119
141
|
def create_token(grant_type)
|
120
142
|
return super unless supported_grant_type?(grant_type, "authorization_code")
|
121
143
|
|
@@ -92,6 +92,14 @@ module Rodauth
|
|
92
92
|
try_approval_prompt if use_oauth_access_type? && request.get?
|
93
93
|
|
94
94
|
redirect_response_error("invalid_scope") if (request.post? || param_or_nil("scope")) && !check_valid_scopes?
|
95
|
+
|
96
|
+
response_mode = param_or_nil("response_mode")
|
97
|
+
|
98
|
+
redirect_response_error("invalid_request") unless response_mode.nil? || oauth_response_modes_supported.include?(response_mode)
|
99
|
+
end
|
100
|
+
|
101
|
+
def check_valid_scopes?(scp = scopes)
|
102
|
+
super(scp - %w[offline_access])
|
95
103
|
end
|
96
104
|
|
97
105
|
def check_valid_response_type?
|
@@ -367,7 +367,7 @@ module Rodauth
|
|
367
367
|
end
|
368
368
|
|
369
369
|
def require_oauth_application_from_client_secret_basic(token)
|
370
|
-
client_id, client_secret = Base64.decode64(token).split(
|
370
|
+
client_id, client_secret = Base64.decode64(token).split(":", 2)
|
371
371
|
authorization_required unless client_id
|
372
372
|
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
373
373
|
authorization_required unless supports_auth_method?(oauth_application,
|
@@ -389,6 +389,8 @@ module Rodauth
|
|
389
389
|
end
|
390
390
|
|
391
391
|
def supports_auth_method?(oauth_application, auth_method)
|
392
|
+
return false unless oauth_application
|
393
|
+
|
392
394
|
supported_auth_methods = if oauth_application[oauth_applications_token_endpoint_auth_method_column]
|
393
395
|
oauth_application[oauth_applications_token_endpoint_auth_method_column].split(/ +/)
|
394
396
|
else
|
@@ -762,31 +764,31 @@ module Rodauth
|
|
762
764
|
throw_json_response_error(status_code, error_code)
|
763
765
|
else
|
764
766
|
redirect_url = URI.parse(redirect_url)
|
765
|
-
|
767
|
+
params = []
|
766
768
|
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
769
|
+
params << if respond_to?(:"oauth_#{error_code}_error_code")
|
770
|
+
["error", send(:"oauth_#{error_code}_error_code")]
|
771
|
+
else
|
772
|
+
["error", error_code]
|
773
|
+
end
|
772
774
|
|
773
775
|
if respond_to?(:"oauth_#{error_code}_message")
|
774
776
|
message = send(:"oauth_#{error_code}_message")
|
775
|
-
|
777
|
+
params << ["error_description", CGI.escape(message)]
|
776
778
|
end
|
777
779
|
|
778
780
|
state = param_or_nil("state")
|
779
781
|
|
780
|
-
|
782
|
+
params << ["state", state] if state
|
781
783
|
|
782
|
-
_redirect_response_error(redirect_url,
|
784
|
+
_redirect_response_error(redirect_url, params)
|
783
785
|
end
|
784
786
|
end
|
785
787
|
|
786
|
-
def _redirect_response_error(redirect_url,
|
787
|
-
|
788
|
-
|
789
|
-
redirect_url.query =
|
788
|
+
def _redirect_response_error(redirect_url, params)
|
789
|
+
params = params.map { |k, v| "#{k}=#{v}" }
|
790
|
+
params << redirect_url.query if redirect_url.query
|
791
|
+
redirect_url.query = params.join("&")
|
790
792
|
redirect(redirect_url.to_s)
|
791
793
|
end
|
792
794
|
|
@@ -841,10 +843,10 @@ module Rodauth
|
|
841
843
|
throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
|
842
844
|
end
|
843
845
|
|
844
|
-
def check_valid_scopes?
|
845
|
-
return false unless
|
846
|
+
def check_valid_scopes?(scp = scopes)
|
847
|
+
return false unless scp
|
846
848
|
|
847
|
-
(
|
849
|
+
(scp - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
|
848
850
|
end
|
849
851
|
|
850
852
|
def check_valid_uri?(uri)
|
@@ -118,8 +118,8 @@ module Rodauth
|
|
118
118
|
}
|
119
119
|
end
|
120
120
|
|
121
|
-
def validate_client_registration_params
|
122
|
-
@oauth_application_params =
|
121
|
+
def validate_client_registration_params(request_params = request.params)
|
122
|
+
@oauth_application_params = request_params.each_with_object({}) do |(key, value), params|
|
123
123
|
case key
|
124
124
|
when "redirect_uris"
|
125
125
|
if value.is_a?(Array)
|
@@ -152,7 +152,7 @@ module Rodauth
|
|
152
152
|
key = oauth_applications_grant_types_column
|
153
153
|
when "response_types"
|
154
154
|
if value.is_a?(Array)
|
155
|
-
grant_types =
|
155
|
+
grant_types = request_params["grant_types"] || %w[authorization_code]
|
156
156
|
value = value.each do |response_type|
|
157
157
|
unless oauth_response_types_supported.include?(response_type)
|
158
158
|
register_throw_json_response_error("invalid_client_metadata",
|
@@ -172,7 +172,7 @@ module Rodauth
|
|
172
172
|
when "client_uri"
|
173
173
|
key = oauth_applications_homepage_url_column
|
174
174
|
when "jwks_uri"
|
175
|
-
if
|
175
|
+
if request_params.key?("jwks")
|
176
176
|
register_throw_json_response_error("invalid_client_metadata",
|
177
177
|
register_invalid_jwks_param_message(key, "jwks"))
|
178
178
|
end
|
@@ -180,7 +180,7 @@ module Rodauth
|
|
180
180
|
key = __send__(:"oauth_applications_#{key}_column")
|
181
181
|
when "jwks"
|
182
182
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
|
183
|
-
if
|
183
|
+
if request_params.key?("jwks_uri")
|
184
184
|
register_throw_json_response_error("invalid_client_metadata",
|
185
185
|
register_invalid_jwks_param_message(key, "jwks_uri"))
|
186
186
|
end
|
@@ -205,14 +205,14 @@ module Rodauth
|
|
205
205
|
register_throw_json_response_error("invalid_client_metadata",
|
206
206
|
register_invalid_param_message(key))
|
207
207
|
end
|
208
|
-
|
208
|
+
request_params[key] = value = convert_to_boolean(key, value)
|
209
209
|
|
210
210
|
key = oauth_applications_require_pushed_authorization_requests_column
|
211
211
|
when "tls_client_certificate_bound_access_tokens"
|
212
212
|
property = :oauth_applications_tls_client_certificate_bound_access_tokens_column
|
213
213
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) unless respond_to?(property)
|
214
214
|
|
215
|
-
|
215
|
+
request_params[key] = value = convert_to_boolean(key, value)
|
216
216
|
|
217
217
|
key = oauth_applications_tls_client_certificate_bound_access_tokens_column
|
218
218
|
when /\Atls_client_auth_/
|
@@ -20,6 +20,24 @@ module Rodauth
|
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
+
def validate_authorize_params
|
24
|
+
super
|
25
|
+
|
26
|
+
response_mode = param_or_nil("response_mode")
|
27
|
+
|
28
|
+
return unless response_mode
|
29
|
+
|
30
|
+
response_type = param_or_nil("response_type")
|
31
|
+
|
32
|
+
return unless response_type == "token"
|
33
|
+
|
34
|
+
redirect_response_error("invalid_request") unless oauth_response_modes_for_token_supported.include?(response_mode)
|
35
|
+
end
|
36
|
+
|
37
|
+
def oauth_response_modes_for_token_supported
|
38
|
+
%w[fragment]
|
39
|
+
end
|
40
|
+
|
23
41
|
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
|
24
42
|
response_type = param("response_type")
|
25
43
|
return super unless response_type == "token" && supported_response_type?(response_type)
|
@@ -48,13 +66,13 @@ module Rodauth
|
|
48
66
|
generate_token(grant_params, false)
|
49
67
|
end
|
50
68
|
|
51
|
-
def _redirect_response_error(redirect_url,
|
69
|
+
def _redirect_response_error(redirect_url, params)
|
52
70
|
response_types = param("response_type").split(/ +/)
|
53
71
|
|
54
72
|
return super if response_types.empty? || response_types == %w[code]
|
55
73
|
|
56
|
-
|
57
|
-
redirect_url.fragment =
|
74
|
+
params = params.map { |k, v| "#{k}=#{v}" }
|
75
|
+
redirect_url.fragment = params.join("&")
|
58
76
|
redirect(redirect_url.to_s)
|
59
77
|
end
|
60
78
|
|
@@ -62,7 +80,7 @@ module Rodauth
|
|
62
80
|
return super unless mode == "fragment"
|
63
81
|
|
64
82
|
redirect_url = URI.parse(redirect_uri)
|
65
|
-
params = params
|
83
|
+
params = [URI.encode_www_form(params)]
|
66
84
|
params << redirect_url.query if redirect_url.query
|
67
85
|
redirect_url.fragment = params.join("&")
|
68
86
|
redirect(redirect_url.to_s)
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_jwt_secured_authorization_response_mode, :OauthJwtSecuredAuthorizationResponseMode) do
|
7
|
+
depends :oauth_authorize_base, :oauth_jwt_base
|
8
|
+
|
9
|
+
auth_value_method :oauth_authorization_response_mode_expires_in, 60 * 5 # 5 minutes
|
10
|
+
|
11
|
+
auth_value_method :oauth_applications_authorization_signed_response_alg_column, :authorization_signed_response_alg
|
12
|
+
auth_value_method :oauth_applications_authorization_encrypted_response_alg_column, :authorization_encrypted_response_alg
|
13
|
+
auth_value_method :oauth_applications_authorization_encrypted_response_enc_column, :authorization_encrypted_response_enc
|
14
|
+
|
15
|
+
auth_value_methods(
|
16
|
+
:authorization_signing_alg_values_supported,
|
17
|
+
:authorization_encryption_alg_values_supported,
|
18
|
+
:authorization_encryption_enc_values_supported
|
19
|
+
)
|
20
|
+
|
21
|
+
def oauth_response_modes_supported
|
22
|
+
jwt_response_modes = %w[jwt]
|
23
|
+
jwt_response_modes.push("query.jwt", "form_post.jwt") if features.include?(:oauth_authorization_code_grant)
|
24
|
+
jwt_response_modes << "fragment.jwt" if features.include?(:oauth_implicit_grant)
|
25
|
+
|
26
|
+
super | jwt_response_modes
|
27
|
+
end
|
28
|
+
|
29
|
+
def authorization_signing_alg_values_supported
|
30
|
+
oauth_jwt_jws_algorithms_supported
|
31
|
+
end
|
32
|
+
|
33
|
+
def authorization_encryption_alg_values_supported
|
34
|
+
oauth_jwt_jwe_algorithms_supported
|
35
|
+
end
|
36
|
+
|
37
|
+
def authorization_encryption_enc_values_supported
|
38
|
+
oauth_jwt_jwe_encryption_methods_supported
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def oauth_response_modes_for_code_supported
|
44
|
+
return [] unless features.include?(:oauth_authorization_code_grant)
|
45
|
+
|
46
|
+
super | %w[query.jwt form_post.jwt jwt]
|
47
|
+
end
|
48
|
+
|
49
|
+
def oauth_response_modes_for_token_supported
|
50
|
+
return [] unless features.include?(:oauth_implicit_grant)
|
51
|
+
|
52
|
+
super | %w[fragment.jwt jwt]
|
53
|
+
end
|
54
|
+
|
55
|
+
def authorize_response(params, mode)
|
56
|
+
return super unless mode.end_with?("jwt")
|
57
|
+
|
58
|
+
response_type = param_or_nil("response_type")
|
59
|
+
|
60
|
+
redirect_url = URI.parse(redirect_uri)
|
61
|
+
|
62
|
+
jwt = jwt_encode_authorization_response_mode(params)
|
63
|
+
|
64
|
+
if mode == "query.jwt" || (mode == "jwt" && response_type == "code")
|
65
|
+
return super unless features.include?(:oauth_authorization_code_grant)
|
66
|
+
|
67
|
+
params = ["response=#{CGI.escape(jwt)}"]
|
68
|
+
params << redirect_url.query if redirect_url.query
|
69
|
+
redirect_url.query = params.join("&")
|
70
|
+
redirect(redirect_url.to_s)
|
71
|
+
elsif mode == "form_post.jwt"
|
72
|
+
return super unless features.include?(:oauth_authorization_code_grant)
|
73
|
+
|
74
|
+
response["Content-Type"] = "text/html"
|
75
|
+
body = form_post_response_html(redirect_url) do
|
76
|
+
"<input type=\"hidden\" name=\"response\" value=\"#{scope.h(jwt)}\" />"
|
77
|
+
end
|
78
|
+
response.write(body)
|
79
|
+
request.halt
|
80
|
+
elsif mode == "fragment.jwt" || (mode == "jwt" && response_type == "token")
|
81
|
+
return super unless features.include?(:oauth_implicit_grant)
|
82
|
+
|
83
|
+
params = ["response=#{CGI.escape(jwt)}"]
|
84
|
+
params << redirect_url.query if redirect_url.query
|
85
|
+
redirect_url.fragment = params.join("&")
|
86
|
+
redirect(redirect_url.to_s)
|
87
|
+
else
|
88
|
+
super
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def _redirect_response_error(redirect_url, params)
|
93
|
+
response_mode = param_or_nil("response_mode")
|
94
|
+
return super unless response_mode.end_with?("jwt")
|
95
|
+
|
96
|
+
authorize_response(Hash[params], response_mode)
|
97
|
+
end
|
98
|
+
|
99
|
+
def jwt_encode_authorization_response_mode(params)
|
100
|
+
now = Time.now.to_i
|
101
|
+
claims = {
|
102
|
+
iss: oauth_jwt_issuer,
|
103
|
+
aud: oauth_application[oauth_applications_client_id_column],
|
104
|
+
exp: now + oauth_authorization_response_mode_expires_in,
|
105
|
+
iat: now
|
106
|
+
}.merge(params)
|
107
|
+
|
108
|
+
encode_params = {
|
109
|
+
jwks: oauth_application_jwks(oauth_application),
|
110
|
+
signing_algorithm: oauth_application[oauth_applications_authorization_signed_response_alg_column],
|
111
|
+
encryption_algorithm: oauth_application[oauth_applications_authorization_encrypted_response_alg_column],
|
112
|
+
encryption_method: oauth_application[oauth_applications_authorization_encrypted_response_enc_column]
|
113
|
+
}.compact
|
114
|
+
|
115
|
+
jwt_encode(claims, **encode_params)
|
116
|
+
end
|
117
|
+
|
118
|
+
def oauth_server_metadata_body(*)
|
119
|
+
super.tap do |data|
|
120
|
+
data[:authorization_signing_alg_values_supported] = authorization_signing_alg_values_supported
|
121
|
+
data[:authorization_encryption_alg_values_supported] = authorization_encryption_alg_values_supported
|
122
|
+
data[:authorization_encryption_enc_values_supported] = authorization_encryption_enc_values_supported
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -23,9 +23,7 @@ module Rodauth
|
|
23
23
|
classes += " disabled" if current || !page
|
24
24
|
classes += " active" if current
|
25
25
|
if page
|
26
|
-
params = request.GET.merge("page" => page)
|
27
|
-
v ? "#{CGI.escape(String(k))}=#{CGI.escape(String(v))}" : CGI.escape(String(k))
|
28
|
-
end.join("&")
|
26
|
+
params = URI.encode_www_form(request.GET.merge("page" => page))
|
29
27
|
|
30
28
|
href = "#{request.path}?#{params}"
|
31
29
|
|
@@ -76,8 +76,7 @@ module Rodauth
|
|
76
76
|
when "plain"
|
77
77
|
challenge == verifier
|
78
78
|
when "S256"
|
79
|
-
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
|
80
|
-
generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
|
79
|
+
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
|
81
80
|
|
82
81
|
challenge == generated_challenge
|
83
82
|
else
|
@@ -63,7 +63,7 @@ module Rodauth
|
|
63
63
|
id_token_signing_alg_values_supported
|
64
64
|
].freeze
|
65
65
|
|
66
|
-
depends :account_expiration, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant
|
66
|
+
depends :account_expiration, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant, :oauth_implicit_grant
|
67
67
|
|
68
68
|
auth_value_method :oauth_application_scopes, %w[openid]
|
69
69
|
|
@@ -89,9 +89,16 @@ module Rodauth
|
|
89
89
|
auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
|
90
90
|
|
91
91
|
auth_value_methods(
|
92
|
+
:userinfo_signing_alg_values_supported,
|
93
|
+
:userinfo_encryption_alg_values_supported,
|
94
|
+
:userinfo_encryption_enc_values_supported,
|
95
|
+
:request_object_signing_alg_values_supported,
|
96
|
+
:request_object_encryption_alg_values_supported,
|
97
|
+
:request_object_encryption_enc_values_supported,
|
92
98
|
:oauth_acr_values_supported,
|
93
99
|
:get_oidc_account_last_login_at,
|
94
100
|
:oidc_authorize_on_prompt_none?,
|
101
|
+
:fill_with_account_claims,
|
95
102
|
:get_oidc_param,
|
96
103
|
:get_additional_param,
|
97
104
|
:require_acr_value_phr,
|
@@ -233,6 +240,30 @@ module Rodauth
|
|
233
240
|
end
|
234
241
|
end
|
235
242
|
|
243
|
+
def userinfo_signing_alg_values_supported
|
244
|
+
oauth_jwt_jws_algorithms_supported
|
245
|
+
end
|
246
|
+
|
247
|
+
def userinfo_encryption_alg_values_supported
|
248
|
+
oauth_jwt_jwe_algorithms_supported
|
249
|
+
end
|
250
|
+
|
251
|
+
def userinfo_encryption_enc_values_supported
|
252
|
+
oauth_jwt_jwe_encryption_methods_supported
|
253
|
+
end
|
254
|
+
|
255
|
+
def request_object_signing_alg_values_supported
|
256
|
+
oauth_jwt_jws_algorithms_supported
|
257
|
+
end
|
258
|
+
|
259
|
+
def request_object_encryption_alg_values_supported
|
260
|
+
oauth_jwt_jwe_algorithms_supported
|
261
|
+
end
|
262
|
+
|
263
|
+
def request_object_encryption_enc_values_supported
|
264
|
+
oauth_jwt_jwe_encryption_methods_supported
|
265
|
+
end
|
266
|
+
|
236
267
|
def oauth_acr_values_supported
|
237
268
|
acr_values = []
|
238
269
|
acr_values << "phrh" if features.include?(:webauthn_login)
|
@@ -274,29 +305,33 @@ module Rodauth
|
|
274
305
|
|
275
306
|
sc = scopes
|
276
307
|
|
277
|
-
|
278
|
-
|
308
|
+
# MUST ensure that the prompt parameter contains consent
|
309
|
+
# MUST ignore the offline_access request unless the Client
|
310
|
+
# is using a response_type value that would result in an
|
311
|
+
# Authorization Code
|
312
|
+
if sc && sc.include?("offline_access") && !(param_or_nil("prompt") == "consent" && (
|
313
|
+
(response_type = param_or_nil("response_type")) && response_type.split(" ").include?("code")
|
314
|
+
))
|
279
315
|
sc.delete("offline_access")
|
280
316
|
|
281
|
-
# MUST ensure that the prompt parameter contains consent
|
282
|
-
# MUST ignore the offline_access request unless the Client
|
283
|
-
# is using a response_type value that would result in an
|
284
|
-
# Authorization Code
|
285
|
-
if param_or_nil("prompt") == "consent" && (
|
286
|
-
(response_type = param_or_nil("response_type")) && response_type.split(" ").include?("code")
|
287
|
-
)
|
288
|
-
request.params["access_type"] = "offline"
|
289
|
-
end
|
290
|
-
|
291
317
|
request.params["scope"] = sc.join(" ")
|
292
318
|
end
|
293
319
|
|
294
320
|
super
|
295
321
|
|
296
|
-
|
297
|
-
|
322
|
+
response_type = param_or_nil("response_type")
|
323
|
+
|
324
|
+
is_id_token_response_type = response_type.include?("id_token")
|
325
|
+
|
326
|
+
redirect_response_error("invalid_request") if is_id_token_response_type && !param_or_nil("nonce")
|
327
|
+
|
328
|
+
return unless is_id_token_response_type || response_type == "code token"
|
329
|
+
|
330
|
+
response_mode = param_or_nil("response_mode")
|
331
|
+
|
332
|
+
# id_token: The default Response Mode for this Response Type is the fragment encoding and the query encoding MUST NOT be used.
|
298
333
|
|
299
|
-
redirect_response_error("invalid_request") unless
|
334
|
+
redirect_response_error("invalid_request") unless response_mode.nil? || response_mode == "fragment"
|
300
335
|
end
|
301
336
|
|
302
337
|
def require_authorizable_account
|
@@ -463,24 +498,7 @@ module Rodauth
|
|
463
498
|
signing_algorithm = oauth_application[oauth_applications_id_token_signed_response_alg_column] ||
|
464
499
|
oauth_jwt_keys.keys.first
|
465
500
|
|
466
|
-
|
467
|
-
|
468
|
-
id_token_claims[:nonce] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
|
469
|
-
|
470
|
-
id_token_claims[:acr] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
|
471
|
-
|
472
|
-
# Time when the End-User authentication occurred.
|
473
|
-
id_token_claims[:auth_time] = get_oidc_account_last_login_at(oauth_grant[oauth_grants_account_id_column]).to_i
|
474
|
-
|
475
|
-
# Access Token hash value.
|
476
|
-
if (access_token = oauth_grant[oauth_grants_token_column])
|
477
|
-
id_token_claims[:at_hash] = id_token_hash(access_token, signing_algorithm)
|
478
|
-
end
|
479
|
-
|
480
|
-
# code hash value.
|
481
|
-
if (code = oauth_grant[oauth_grants_code_column])
|
482
|
-
id_token_claims[:c_hash] = id_token_hash(code, signing_algorithm)
|
483
|
-
end
|
501
|
+
id_claims = id_token_claims(oauth_grant, signing_algorithm)
|
484
502
|
|
485
503
|
account = db[accounts_table].where(account_id_column => oauth_grant[oauth_grants_account_id_column]).first
|
486
504
|
|
@@ -500,7 +518,7 @@ module Rodauth
|
|
500
518
|
|
501
519
|
# 5.4 - However, when no Access Token is issued (which is the case for the response_type value id_token),
|
502
520
|
# the resulting Claims are returned in the ID Token.
|
503
|
-
fill_with_account_claims(
|
521
|
+
fill_with_account_claims(id_claims, account, oauth_scopes, param_or_nil("claims_locales")) if include_claims
|
504
522
|
|
505
523
|
params = {
|
506
524
|
jwks: oauth_application_jwks(oauth_application),
|
@@ -509,7 +527,30 @@ module Rodauth
|
|
509
527
|
encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
|
510
528
|
}.compact
|
511
529
|
|
512
|
-
oauth_grant[:id_token] = jwt_encode(
|
530
|
+
oauth_grant[:id_token] = jwt_encode(id_claims, **params)
|
531
|
+
end
|
532
|
+
|
533
|
+
def id_token_claims(oauth_grant, signing_algorithm)
|
534
|
+
claims = jwt_claims(oauth_grant)
|
535
|
+
|
536
|
+
claims[:nonce] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
|
537
|
+
|
538
|
+
claims[:acr] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
|
539
|
+
|
540
|
+
# Time when the End-User authentication occurred.
|
541
|
+
claims[:auth_time] = get_oidc_account_last_login_at(oauth_grant[oauth_grants_account_id_column]).to_i
|
542
|
+
|
543
|
+
# Access Token hash value.
|
544
|
+
if (access_token = oauth_grant[oauth_grants_token_column])
|
545
|
+
claims[:at_hash] = id_token_hash(access_token, signing_algorithm)
|
546
|
+
end
|
547
|
+
|
548
|
+
# code hash value.
|
549
|
+
if (code = oauth_grant[oauth_grants_code_column])
|
550
|
+
claims[:c_hash] = id_token_hash(code, signing_algorithm)
|
551
|
+
end
|
552
|
+
|
553
|
+
claims
|
513
554
|
end
|
514
555
|
|
515
556
|
# aka fill_with_standard_claims
|
@@ -627,10 +668,9 @@ module Rodauth
|
|
627
668
|
|
628
669
|
def check_valid_response_type?
|
629
670
|
case param_or_nil("response_type")
|
630
|
-
when "none", "id_token", "code id_token" # multiple
|
671
|
+
when "none", "id_token", "code id_token", # multiple
|
672
|
+
"code token", "id_token token", "code id_token token"
|
631
673
|
true
|
632
|
-
when "code token", "id_token token", "code id_token token"
|
633
|
-
supports_token_response_type?
|
634
674
|
else
|
635
675
|
super
|
636
676
|
end
|
@@ -642,10 +682,6 @@ module Rodauth
|
|
642
682
|
param("response_type") == "none"
|
643
683
|
end
|
644
684
|
|
645
|
-
def supports_token_response_type?
|
646
|
-
features.include?(:oauth_implicit_grant)
|
647
|
-
end
|
648
|
-
|
649
685
|
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
|
650
686
|
response_type = param("response_type")
|
651
687
|
case response_type
|
@@ -654,8 +690,6 @@ module Rodauth
|
|
654
690
|
generate_id_token(grant_params, true)
|
655
691
|
response_params.replace("id_token" => grant_params[:id_token])
|
656
692
|
when "code token"
|
657
|
-
redirect_response_error("invalid_request") unless supports_token_response_type?
|
658
|
-
|
659
693
|
response_params.replace(create_oauth_grant_with_token)
|
660
694
|
when "code id_token"
|
661
695
|
params = _do_authorize_code
|
@@ -666,16 +700,12 @@ module Rodauth
|
|
666
700
|
"code" => params["code"]
|
667
701
|
)
|
668
702
|
when "id_token token"
|
669
|
-
redirect_response_error("invalid_request") unless supports_token_response_type?
|
670
|
-
|
671
703
|
grant_params = oidc_grant_params.merge(oauth_grants_type_column => "hybrid")
|
672
704
|
oauth_grant = _do_authorize_token(grant_params)
|
673
705
|
generate_id_token(oauth_grant)
|
674
706
|
|
675
707
|
response_params.replace(json_access_token_payload(oauth_grant))
|
676
708
|
when "code id_token token"
|
677
|
-
redirect_response_error("invalid_request") unless supports_token_response_type?
|
678
|
-
|
679
709
|
params = create_oauth_grant_with_token
|
680
710
|
oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => params["code"]).first
|
681
711
|
oauth_grant[oauth_grants_token_column] = params["access_token"]
|
@@ -694,7 +724,8 @@ module Rodauth
|
|
694
724
|
grant_params = {
|
695
725
|
**resource_owner_params,
|
696
726
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
697
|
-
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
727
|
+
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
728
|
+
oauth_grants_redirect_uri_column => param_or_nil("redirect_uri")
|
698
729
|
}
|
699
730
|
if (nonce = param_or_nil("nonce"))
|
700
731
|
grant_params[oauth_grants_nonce_column] = nonce
|
@@ -709,6 +740,12 @@ module Rodauth
|
|
709
740
|
grant_params
|
710
741
|
end
|
711
742
|
|
743
|
+
def generate_token(grant_params = {}, should_generate_refresh_token = true)
|
744
|
+
scopes = grant_params[oauth_grants_scopes_column].split(oauth_scope_separator)
|
745
|
+
|
746
|
+
super(grant_params, scopes.include?("offline_access") && should_generate_refresh_token)
|
747
|
+
end
|
748
|
+
|
712
749
|
def authorize_response(params, mode)
|
713
750
|
redirect_url = URI.parse(redirect_uri)
|
714
751
|
redirect(redirect_url.to_s) if mode == "none"
|
@@ -10,7 +10,7 @@ module Rodauth
|
|
10
10
|
|
11
11
|
private
|
12
12
|
|
13
|
-
def validate_client_registration_params
|
13
|
+
def validate_client_registration_params(*)
|
14
14
|
super
|
15
15
|
|
16
16
|
if (value = @oauth_application_params[oauth_applications_application_type_column])
|
@@ -174,6 +174,44 @@ module Rodauth
|
|
174
174
|
register_throw_json_response_error("invalid_client_metadata",
|
175
175
|
register_invalid_client_metadata_message("userinfo_encrypted_response_enc", value))
|
176
176
|
end
|
177
|
+
|
178
|
+
if features.include?(:oauth_jwt_secured_authorization_response_mode)
|
179
|
+
if defined?(oauth_applications_authorization_signed_response_alg_column) &&
|
180
|
+
(value = @oauth_application_params[oauth_applications_authorization_signed_response_alg_column]) &&
|
181
|
+
(!oauth_jwt_jws_algorithms_supported.include?(value) || value == "none")
|
182
|
+
register_throw_json_response_error("invalid_client_metadata",
|
183
|
+
register_invalid_client_metadata_message("authorization_signed_response_alg", value))
|
184
|
+
end
|
185
|
+
|
186
|
+
if defined?(oauth_applications_authorization_encrypted_response_alg_column) &&
|
187
|
+
(value = @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]) &&
|
188
|
+
!oauth_jwt_jwe_algorithms_supported.include?(value)
|
189
|
+
register_throw_json_response_error("invalid_client_metadata",
|
190
|
+
register_invalid_client_metadata_message("authorization_encrypted_response_alg", value))
|
191
|
+
end
|
192
|
+
|
193
|
+
if defined?(oauth_applications_authorization_encrypted_response_enc_column)
|
194
|
+
if (value = @oauth_application_params[oauth_applications_authorization_encrypted_response_enc_column])
|
195
|
+
|
196
|
+
unless @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]
|
197
|
+
# When authorization_encrypted_response_enc is included, authorization_encrypted_response_alg MUST also be provided.
|
198
|
+
register_throw_json_response_error("invalid_client_metadata",
|
199
|
+
register_invalid_client_metadata_message("authorization_encrypted_response_alg", value))
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
|
204
|
+
register_throw_json_response_error("invalid_client_metadata",
|
205
|
+
register_invalid_client_metadata_message("authorization_encrypted_response_enc", value))
|
206
|
+
end
|
207
|
+
elsif @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]
|
208
|
+
# If authorization_encrypted_response_alg is specified, the default for this value is A128CBC-HS256.
|
209
|
+
@oauth_application_params[oauth_applications_authorization_encrypted_response_enc_column] = "A128CBC-HS256"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
@oauth_application_params
|
177
215
|
end
|
178
216
|
|
179
217
|
def validate_client_registration_response_type(response_type, grant_types)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oidc_self_issued, :OidcSelfIssued) do
|
7
|
+
depends :oidc, :oidc_dynamic_client_registration
|
8
|
+
|
9
|
+
auth_value_method :oauth_application_scopes, %w[openid profile email address phone]
|
10
|
+
auth_value_method :oauth_jwt_jws_algorithms_supported, %w[RS256]
|
11
|
+
|
12
|
+
SELF_ISSUED_DEFAULT_APPLICATION_PARAMS = {
|
13
|
+
"scope" => "openid profile email address phone",
|
14
|
+
"response_types" => ["id_token"],
|
15
|
+
"subject_type" => "pairwise",
|
16
|
+
"id_token_signed_response_alg" => "RS256",
|
17
|
+
"request_object_signing_alg" => "RS256",
|
18
|
+
"grant_types" => %w[implicit]
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def oauth_application
|
22
|
+
return @oauth_application if defined?(@oauth_application)
|
23
|
+
|
24
|
+
return super unless (registration = param_or_nil("registration"))
|
25
|
+
|
26
|
+
# self-issued!
|
27
|
+
redirect_uri = param_or_nil("client_id")
|
28
|
+
|
29
|
+
registration_params = JSON.parse(registration)
|
30
|
+
|
31
|
+
registration_params = SELF_ISSUED_DEFAULT_APPLICATION_PARAMS.merge(registration_params)
|
32
|
+
|
33
|
+
client_params = validate_client_registration_params(registration_params)
|
34
|
+
|
35
|
+
request.params["redirect_uri"] = client_params[oauth_applications_client_id_column] = redirect_uri
|
36
|
+
client_params[oauth_applications_redirect_uri_column] ||= redirect_uri
|
37
|
+
|
38
|
+
@oauth_application = client_params
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def oauth_response_types_supported
|
44
|
+
%w[id_token]
|
45
|
+
end
|
46
|
+
|
47
|
+
def request_object_signing_alg_values_supported
|
48
|
+
%w[none RS256]
|
49
|
+
end
|
50
|
+
|
51
|
+
def id_token_claims(oauth_grant, signing_algorithm)
|
52
|
+
claims = super
|
53
|
+
|
54
|
+
return claims unless claims[:client_id] == oauth_grant[oauth_grants_redirect_uri_column]
|
55
|
+
|
56
|
+
# https://openid.net/specs/openid-connect-core-1_0.html#SelfIssued - 7.4
|
57
|
+
|
58
|
+
pub_key = oauth_jwt_public_keys[signing_algorithm]
|
59
|
+
pub_key = pub_key.first if pub_key.is_a?(Array)
|
60
|
+
claims[:sub_jwk] = sub_jwk = jwk_export(pub_key)
|
61
|
+
|
62
|
+
claims[:iss] = "https://self-issued.me"
|
63
|
+
|
64
|
+
claims[:aud] = oauth_grant[oauth_grants_redirect_uri_column]
|
65
|
+
|
66
|
+
jwk_thumbprint = jwk_thumbprint(sub_jwk)
|
67
|
+
|
68
|
+
claims[:sub] = Base64.urlsafe_encode64(jwk_thumbprint, padding: false)
|
69
|
+
|
70
|
+
claims
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/templates/authorize.str
CHANGED
@@ -88,6 +88,7 @@
|
|
88
88
|
#{"<input type=\"hidden\" name=\"claims_locales\" value=\"#{rodauth.param("claims_locales")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("claims_locales")}
|
89
89
|
#{"<input type=\"hidden\" name=\"claims\" value=\"#{h(rodauth.param("claims"))}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("claims")}
|
90
90
|
#{"<input type=\"hidden\" name=\"acr_values\" value=\"#{rodauth.param("acr_values")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("acr_values")}
|
91
|
+
#{"<input type=\"hidden\" name=\"registration\" value=\"#{h(rodauth.param("registration"))}\"/>" if rodauth.features.include?(:oidc_self_issued) && rodauth.param_or_nil("registration")}
|
91
92
|
#{
|
92
93
|
if rodauth.features.include?(:oauth_resource_indicators) && rodauth.resource_indicators
|
93
94
|
rodauth.resource_indicators.map do |resource|
|
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: 1.
|
4
|
+
version: 1.3.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: 2023-
|
11
|
+
date: 2023-06-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rodauth
|
@@ -69,6 +69,8 @@ extra_rdoc_files:
|
|
69
69
|
- doc/release_notes/1_0_0.md
|
70
70
|
- doc/release_notes/1_1_0.md
|
71
71
|
- doc/release_notes/1_2_0.md
|
72
|
+
- doc/release_notes/1_3_0.md
|
73
|
+
- doc/release_notes/1_3_1.md
|
72
74
|
files:
|
73
75
|
- CHANGELOG.md
|
74
76
|
- LICENSE.txt
|
@@ -109,6 +111,8 @@ files:
|
|
109
111
|
- doc/release_notes/1_0_0.md
|
110
112
|
- doc/release_notes/1_1_0.md
|
111
113
|
- doc/release_notes/1_2_0.md
|
114
|
+
- doc/release_notes/1_3_0.md
|
115
|
+
- doc/release_notes/1_3_1.md
|
112
116
|
- lib/generators/rodauth/oauth/install_generator.rb
|
113
117
|
- lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
|
114
118
|
- lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
|
@@ -138,6 +142,7 @@ files:
|
|
138
142
|
- lib/rodauth/features/oauth_jwt_bearer_grant.rb
|
139
143
|
- lib/rodauth/features/oauth_jwt_jwks.rb
|
140
144
|
- lib/rodauth/features/oauth_jwt_secured_authorization_request.rb
|
145
|
+
- lib/rodauth/features/oauth_jwt_secured_authorization_response_mode.rb
|
141
146
|
- lib/rodauth/features/oauth_management_base.rb
|
142
147
|
- lib/rodauth/features/oauth_pkce.rb
|
143
148
|
- lib/rodauth/features/oauth_pushed_authorization_request.rb
|
@@ -150,6 +155,7 @@ files:
|
|
150
155
|
- lib/rodauth/features/oidc.rb
|
151
156
|
- lib/rodauth/features/oidc_dynamic_client_registration.rb
|
152
157
|
- lib/rodauth/features/oidc_rp_initiated_logout.rb
|
158
|
+
- lib/rodauth/features/oidc_self_issued.rb
|
153
159
|
- lib/rodauth/oauth.rb
|
154
160
|
- lib/rodauth/oauth/database_extensions.rb
|
155
161
|
- lib/rodauth/oauth/http_extensions.rb
|
@@ -200,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
200
206
|
- !ruby/object:Gem::Version
|
201
207
|
version: '0'
|
202
208
|
requirements: []
|
203
|
-
rubygems_version: 3.
|
209
|
+
rubygems_version: 3.4.10
|
204
210
|
signing_key:
|
205
211
|
specification_version: 4
|
206
212
|
summary: Implementation of the OAuth 2.0 protocol on top of rodauth.
|