rodauth-oauth 1.1.0 → 1.2.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 +4 -4
- data/README.md +9 -8
- data/doc/release_notes/1_2_0.md +36 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +22 -0
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +1 -1
- data/lib/rodauth/features/oauth_authorize_base.rb +17 -3
- data/lib/rodauth/features/oauth_device_code_grant.rb +1 -2
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +178 -25
- data/lib/rodauth/features/oauth_implicit_grant.rb +1 -1
- data/lib/rodauth/features/oauth_jwt.rb +2 -0
- data/lib/rodauth/features/oauth_jwt_base.rb +52 -11
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +30 -22
- data/lib/rodauth/features/oauth_pushed_authorization_request.rb +135 -0
- data/lib/rodauth/features/oauth_tls_client_auth.rb +170 -0
- data/lib/rodauth/features/oidc.rb +10 -9
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +13 -1
- data/lib/rodauth/features/oidc_rp_initiated_logout.rb +3 -4
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa95c4db43b0068e5a7c1f8af1d904a26351a007f203be233803bae0ed471f32
|
4
|
+
data.tar.gz: fe0da3df41a98b6411fd20378eb41f22e5c4766cf42fd3061a45d0c6b8606bcf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 910bb63ff3a0be7cb035d54ed4eb48916b2137d886523403154d0eefd5d72bdb4375a268e8c94eb8047592bddfb2dd79b7022611243e18902ec33f467450e013
|
7
|
+
data.tar.gz: 332125982a596d3ea0c773c7e84e5a4e4cdeed2bc9de0069f742d9b2a10de0b2182fcaea69c658fefe88b1a67e4c8b81c2dbafa3dc4c155d0100a64b1d444bb9
|
data/README.md
CHANGED
@@ -24,7 +24,7 @@ This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framew
|
|
24
24
|
|
25
25
|
This gem implements the following RFCs and features of OAuth:
|
26
26
|
|
27
|
-
* `oauth` - [The OAuth 2.0 protocol framework](
|
27
|
+
* `oauth` - [The OAuth 2.0 protocol framework](-/wikis/home#oauth-20-protocol-framework):
|
28
28
|
* [Access Token generation](https://tools.ietf.org/html/rfc6749#section-1.4);
|
29
29
|
* [Access Token refresh token grant](https://tools.ietf.org/html/rfc6749#section-1.5);
|
30
30
|
* `oauth_authorization_code_grant` - [Authorization code grant](https://tools.ietf.org/html/rfc6749#section-1.3);
|
@@ -33,8 +33,10 @@ This gem implements the following RFCs and features of OAuth:
|
|
33
33
|
* `oauth_device_code_grant` - [Device code grant (off by default)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-device-flow-15);
|
34
34
|
* `oauth_token_revocation` - [Token revocation](https://tools.ietf.org/html/rfc7009);
|
35
35
|
* `oauth_token_introspection` - [Token introspection](https://tools.ietf.org/html/rfc7662);
|
36
|
+
* `oauth_pushed_authorization_request` - [Pushed Authorization Request](https://datatracker.ietf.org/doc/html/rfc9126);
|
36
37
|
* [Authorization Server Metadata](https://tools.ietf.org/html/rfc8414);
|
37
38
|
* `oauth_pkce` - [PKCE](https://tools.ietf.org/html/rfc7636);
|
39
|
+
* `oauth_tls_client_auth` - [Mutual-TLS Client Authentication](https://datatracker.ietf.org/doc/html/rfc8705);
|
38
40
|
* `oauth_jwt` - [JWT Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
|
39
41
|
* `oauth_jwt_secured_authorization_request` - [JWT Secured Authorization Request](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
|
40
42
|
* `oauth_resource_indicators` - [Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707);
|
@@ -44,18 +46,17 @@ This gem implements the following RFCs and features of OAuth:
|
|
44
46
|
* `oauth_saml_bearer_grant` - [SAML 2.0 Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7522);
|
45
47
|
* `oauth_jwt_bearer_grant` - [JWT Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7523);
|
46
48
|
|
47
|
-
* `oauth_dynamic_client_registration` - [Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591);
|
49
|
+
* `oauth_dynamic_client_registration` - [Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591) and [Dynamic Client Registration Management](https://www.rfc-editor.org/rfc/rfc7592);
|
48
50
|
* OAuth application and token management dashboards;
|
49
51
|
* The recommendations for [Native Apps](https://www.rfc-editor.org/rfc/rfc8252);
|
50
52
|
|
51
53
|
It also implements the [OpenID Connect layer](https://openid.net/connect/) (via the `openid` feature) on top of the OAuth features it provides, including:
|
52
54
|
|
53
|
-
*
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
*
|
58
|
-
* `oidc_rp_initiated_logout` - [RP Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html);
|
55
|
+
* [OpenID Connect Core](https://gitlab.com/os85/rodauth-oauth/-/wikis/Id-Token-Authentication);
|
56
|
+
* [OpenID Connect Discovery](https://gitlab.com/os85/rodauth-oauth/-/wikis/OIDC-Dynamic-Client-Registration);
|
57
|
+
* [OpenID Multiple Response Types](https://gitlab.com/os85/rodauth-oauth/-/wikis/Hybrid-flow);
|
58
|
+
* [OpenID Connect Dynamic Client Registration](https://gitlab.com/os85/rodauth-oauth/-/wikis/OIDC-Dynamic-Client-Registration);
|
59
|
+
* [RP Initiated Logout](https://gitlab.com/os85/rodauth-oauth/-/wikis/RP-Initiated-Logout);
|
59
60
|
|
60
61
|
This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
|
61
62
|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
## 1.2.0 (13/02/2023)
|
2
|
+
|
3
|
+
### Features
|
4
|
+
|
5
|
+
#### Pushed Authorization Requests (PAR)
|
6
|
+
|
7
|
+
RFC: https://datatracker.ietf.org/doc/html/rfc9126
|
8
|
+
|
9
|
+
`rodauth-oauth` supports Pushed Authorization Requests, via the `:oauth_pushed_authorization_request` feature.
|
10
|
+
|
11
|
+
More info about the feature [in the wiki](https://gitlab.com/os85/rodauth-oauth/-/wikis/Pushed-Authorization-Requests).
|
12
|
+
|
13
|
+
#### mTLS Client Auth (+ certificate-bound access tokens)
|
14
|
+
|
15
|
+
RFC: https://www.rfc-editor.org/rfc/rfc8705
|
16
|
+
|
17
|
+
The `:oauth_tls_client_auth` feature adds support for the variants of mTLS Client Authentication "PKI Mutual-TLS Method" and 2Self-Signed Certificate Mutual-TLS Method". It also supports client certificate bound access tokens.
|
18
|
+
|
19
|
+
More about it [in the wiki](https://gitlab.com/os85/rodauth-oauth/-/wikis/mTLS-Client-Authentication).
|
20
|
+
|
21
|
+
#### Dynamic Client Registration management
|
22
|
+
|
23
|
+
RFC: https://www.rfc-editor.org/rfc/rfc7592
|
24
|
+
|
25
|
+
Support for dynamci client registration management was added to the `:oauth_dynamic_client_registration` feature.
|
26
|
+
|
27
|
+
More info about it [in the wiki](https://gitlab.com/os85/rodauth-oauth/-/wikis/Dynamic-Client-Registration#getputdelete-registerclient_id).
|
28
|
+
|
29
|
+
### Improvements
|
30
|
+
|
31
|
+
* Support for 3rd-party initiated login was added, by including support for the `initiate_login_uri` attribute in the register route from the `:oauth_dynamic_client_registration` feature.
|
32
|
+
* Support for multitenant resource ownership was added, here's a [description from the wiki](https://gitlab.com/os85/rodauth-oauth/-/wikis/How-to#scoping-grants-from-the-same-resource-owner).
|
33
|
+
|
34
|
+
### Bugfixes
|
35
|
+
|
36
|
+
* oidc: userinfo claims were not including claims with value `false`, such as `"email_verified"`. This behaviour has been fixed, and only claims of value `null` are omitted.
|
@@ -9,6 +9,7 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
9
9
|
t.string :redirect_uri, null: false
|
10
10
|
t.string :client_id, null: false, index: { unique: true }
|
11
11
|
t.string :client_secret, null: false, index: { unique: true }
|
12
|
+
t.string :registration_access_token, null: true
|
12
13
|
t.string :scopes, null: false
|
13
14
|
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
14
15
|
|
@@ -29,6 +30,7 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
29
30
|
# :oidc_dynamic_client_configuration enabled, extra optional params
|
30
31
|
t.string :sector_identifier_uri, null: true
|
31
32
|
t.string :application_type, null: true
|
33
|
+
t.string :initiate_login_uri, null: true
|
32
34
|
|
33
35
|
# :oidc enabled
|
34
36
|
t.string :subject_type, null: true
|
@@ -44,6 +46,15 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
44
46
|
t.string :request_object_encryption_alg, null: true
|
45
47
|
t.string :request_object_encryption_enc, null: true
|
46
48
|
t.string :request_uris, null: true
|
49
|
+
t.boolean :require_pushed_authorization_requests, null: false, default: false
|
50
|
+
|
51
|
+
# :oauth_tls_client_auth
|
52
|
+
t.string :tls_client_auth_subject_dn, null: true
|
53
|
+
t.string :tls_client_auth_san_dns, null: true
|
54
|
+
t.string :tls_client_auth_san_uri, null: true
|
55
|
+
t.string :tls_client_auth_san_ip, null: true
|
56
|
+
t.string :tls_client_auth_san_email, null: true
|
57
|
+
t.boolean :tls_client_certificate_bound_access_tokens, default: false
|
47
58
|
|
48
59
|
# :oidc_rp_initiated_logout enabled
|
49
60
|
t.string :post_logout_redirect_uris, null: false
|
@@ -74,6 +85,9 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
74
85
|
t.string :user_code, null: true, unique: true
|
75
86
|
t.datetime :last_polled_at, null: true
|
76
87
|
|
88
|
+
# :oauth_tls_client_auth
|
89
|
+
t.string :certificate_thumbprint, null: true
|
90
|
+
|
77
91
|
# :resource_indicators enabled
|
78
92
|
t.string :resource
|
79
93
|
|
@@ -83,5 +97,13 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
|
|
83
97
|
t.string :claims_locales
|
84
98
|
t.string :claims
|
85
99
|
end
|
100
|
+
|
101
|
+
create_table :oauth_pushed_requests do |t|
|
102
|
+
t.integer :oauth_application_id
|
103
|
+
t.foreign_key :oauth_applications, column: :oauth_application_id
|
104
|
+
t.string :params, null: false
|
105
|
+
t.datetime :expires_in, null: false
|
106
|
+
t.index %i[oauth_application_id code], unique: true
|
107
|
+
end
|
86
108
|
end
|
87
109
|
end
|
@@ -28,6 +28,11 @@ module Rodauth
|
|
28
28
|
translatable_method :oauth_unsupported_response_type_message, "Unsupported response type"
|
29
29
|
translatable_method :oauth_authorize_parameter_required, "Invalid or missing '%<parameter>s'"
|
30
30
|
|
31
|
+
auth_value_methods(
|
32
|
+
:resource_owner_params,
|
33
|
+
:oauth_grants_resource_owner_columns
|
34
|
+
)
|
35
|
+
|
31
36
|
# /authorize
|
32
37
|
auth_server_route(:authorize) do |r|
|
33
38
|
require_authorizable_account
|
@@ -73,7 +78,9 @@ module Rodauth
|
|
73
78
|
|
74
79
|
if (redirect_uri = param_or_nil("redirect_uri"))
|
75
80
|
normalized_redirect_uri = normalize_redirect_uri_for_comparison(redirect_uri)
|
76
|
-
|
81
|
+
unless redirect_uris.include?(normalized_redirect_uri) || redirect_uris.include?(redirect_uri)
|
82
|
+
redirect_authorize_error("redirect_uri")
|
83
|
+
end
|
77
84
|
elsif redirect_uris.size > 1
|
78
85
|
redirect_authorize_error("redirect_uri")
|
79
86
|
end
|
@@ -109,13 +116,20 @@ module Rodauth
|
|
109
116
|
!approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
|
110
117
|
end
|
111
118
|
|
119
|
+
def resource_owner_params
|
120
|
+
{ oauth_grants_account_id_column => account_id }
|
121
|
+
end
|
122
|
+
|
123
|
+
def oauth_grants_resource_owner_columns
|
124
|
+
[oauth_grants_account_id_column]
|
125
|
+
end
|
126
|
+
|
112
127
|
def try_approval_prompt
|
113
128
|
approval_prompt = param_or_nil("approval_prompt")
|
114
129
|
|
115
130
|
return unless approval_prompt && approval_prompt == "auto"
|
116
131
|
|
117
|
-
return if db[oauth_grants_table].where(
|
118
|
-
oauth_grants_account_id_column => account_id,
|
132
|
+
return if db[oauth_grants_table].where(resource_owner_params).where(
|
119
133
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
120
134
|
oauth_grants_redirect_uri_column => redirect_uri,
|
121
135
|
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
@@ -193,9 +193,8 @@ module Rodauth
|
|
193
193
|
|
194
194
|
# do not clean up device code just yet
|
195
195
|
update_params.delete(oauth_grants_code_column)
|
196
|
-
|
197
196
|
update_params[oauth_grants_user_code_column] = nil
|
198
|
-
update_params
|
197
|
+
update_params.merge!(resource_params)
|
199
198
|
|
200
199
|
super(grant_params, update_params)
|
201
200
|
end
|
@@ -9,16 +9,78 @@ module Rodauth
|
|
9
9
|
before "register"
|
10
10
|
|
11
11
|
auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name]
|
12
|
+
auth_value_method :oauth_applications_registration_access_token_column, :registration_access_token
|
13
|
+
auth_value_method :registration_client_uri_route, "register"
|
12
14
|
|
13
15
|
PROTECTED_APPLICATION_ATTRIBUTES = %w[account_id client_id].freeze
|
14
16
|
|
17
|
+
def load_registration_client_uri_routes
|
18
|
+
request.on(registration_client_uri_route) do
|
19
|
+
# CLIENT REGISTRATION URI
|
20
|
+
request.on(String) do |client_id|
|
21
|
+
(token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Bearer (.*)\Z/, 1]))
|
22
|
+
|
23
|
+
next unless token
|
24
|
+
|
25
|
+
oauth_application = db[oauth_applications_table]
|
26
|
+
.where(oauth_applications_client_id_column => client_id)
|
27
|
+
.first
|
28
|
+
next unless oauth_application
|
29
|
+
|
30
|
+
authorization_required unless password_hash_match?(oauth_application[oauth_applications_registration_access_token_column], token)
|
31
|
+
|
32
|
+
request.is do
|
33
|
+
request.get do
|
34
|
+
json_response_oauth_application(oauth_application)
|
35
|
+
end
|
36
|
+
request.on method: :put do
|
37
|
+
%w[client_id registration_access_token registration_client_uri client_secret_expires_at
|
38
|
+
client_id_issued_at].each do |prohibited_param|
|
39
|
+
if request.params.key?(prohibited_param)
|
40
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(prohibited_param))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
validate_client_registration_params
|
44
|
+
|
45
|
+
# if the client includes the "client_secret" field in the request, the value of this field MUST match the currently
|
46
|
+
# issued client secret for that client. The client MUST NOT be allowed to overwrite its existing client secret with
|
47
|
+
# its own chosen value.
|
48
|
+
authorization_required if request.params.key?("client_secret") && secret_matches?(oauth_application,
|
49
|
+
request.params["client_secret"])
|
50
|
+
|
51
|
+
oauth_application = transaction do
|
52
|
+
applications_ds = db[oauth_applications_table]
|
53
|
+
__update_and_return__(applications_ds, @oauth_application_params)
|
54
|
+
end
|
55
|
+
json_response_oauth_application(oauth_application)
|
56
|
+
end
|
57
|
+
|
58
|
+
request.on method: :delete do
|
59
|
+
applications_ds = db[oauth_applications_table]
|
60
|
+
applications_ds.where(oauth_applications_client_id_column => client_id).delete
|
61
|
+
response.status = 204
|
62
|
+
response["Cache-Control"] = "no-store"
|
63
|
+
response["Pragma"] = "no-cache"
|
64
|
+
response.finish
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
15
71
|
# /register
|
16
72
|
auth_server_route(:register) do |r|
|
17
73
|
before_register_route
|
18
74
|
|
19
|
-
validate_client_registration_params
|
20
|
-
|
21
75
|
r.post do
|
76
|
+
oauth_client_registration_required_params.each do |required_param|
|
77
|
+
unless request.params.key?(required_param)
|
78
|
+
register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
validate_client_registration_params
|
83
|
+
|
22
84
|
response_params = transaction do
|
23
85
|
before_register
|
24
86
|
do_register
|
@@ -57,12 +119,6 @@ module Rodauth
|
|
57
119
|
end
|
58
120
|
|
59
121
|
def validate_client_registration_params
|
60
|
-
oauth_client_registration_required_params.each do |required_param|
|
61
|
-
unless request.params.key?(required_param)
|
62
|
-
register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
122
|
@oauth_application_params = request.params.each_with_object({}) do |(key, value), params|
|
67
123
|
case key
|
68
124
|
when "redirect_uris"
|
@@ -96,7 +152,7 @@ module Rodauth
|
|
96
152
|
key = oauth_applications_grant_types_column
|
97
153
|
when "response_types"
|
98
154
|
if value.is_a?(Array)
|
99
|
-
grant_types = request.params["grant_types"] ||
|
155
|
+
grant_types = request.params["grant_types"] || %w[authorization_code]
|
100
156
|
value = value.each do |response_type|
|
101
157
|
unless oauth_response_types_supported.include?(response_type)
|
102
158
|
register_throw_json_response_error("invalid_client_metadata",
|
@@ -114,7 +170,7 @@ module Rodauth
|
|
114
170
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value)
|
115
171
|
case key
|
116
172
|
when "client_uri"
|
117
|
-
key =
|
173
|
+
key = oauth_applications_homepage_url_column
|
118
174
|
when "jwks_uri"
|
119
175
|
if request.params.key?("jwks")
|
120
176
|
register_throw_json_response_error("invalid_client_metadata",
|
@@ -132,6 +188,7 @@ module Rodauth
|
|
132
188
|
key = oauth_applications_jwks_column
|
133
189
|
value = JSON.dump(value)
|
134
190
|
when "scope"
|
191
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
|
135
192
|
scopes = value.split(" ") - oauth_application_scopes
|
136
193
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty?
|
137
194
|
key = oauth_applications_scopes_column
|
@@ -141,7 +198,37 @@ module Rodauth
|
|
141
198
|
value = value.join(" ")
|
142
199
|
key = oauth_applications_contacts_column
|
143
200
|
when "client_name"
|
201
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
|
144
202
|
key = oauth_applications_name_column
|
203
|
+
when "require_pushed_authorization_requests"
|
204
|
+
unless respond_to?(:oauth_applications_require_pushed_authorization_requests_column)
|
205
|
+
register_throw_json_response_error("invalid_client_metadata",
|
206
|
+
register_invalid_param_message(key))
|
207
|
+
end
|
208
|
+
request.params[key] = value = convert_to_boolean(key, value)
|
209
|
+
|
210
|
+
key = oauth_applications_require_pushed_authorization_requests_column
|
211
|
+
when "tls_client_certificate_bound_access_tokens"
|
212
|
+
property = :oauth_applications_tls_client_certificate_bound_access_tokens_column
|
213
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) unless respond_to?(property)
|
214
|
+
|
215
|
+
request.params[key] = value = convert_to_boolean(key, value)
|
216
|
+
|
217
|
+
key = oauth_applications_tls_client_certificate_bound_access_tokens_column
|
218
|
+
when /\Atls_client_auth_/
|
219
|
+
unless respond_to?(:"oauth_applications_#{key}_column")
|
220
|
+
register_throw_json_response_error("invalid_client_metadata",
|
221
|
+
register_invalid_param_message(key))
|
222
|
+
end
|
223
|
+
|
224
|
+
# client using the tls_client_auth authentication method MUST use exactly one of the below metadata
|
225
|
+
# parameters to indicate the certificate subject value that the authorization server is to expect when
|
226
|
+
# authenticating the respective client.
|
227
|
+
if params.any? { |k, _| k.to_s.start_with?("tls_client_auth_") }
|
228
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
|
229
|
+
end
|
230
|
+
|
231
|
+
key = __send__(:"oauth_applications_#{key}_column")
|
145
232
|
else
|
146
233
|
if respond_to?(:"oauth_applications_#{key}_column")
|
147
234
|
if PROTECTED_APPLICATION_ATTRIBUTES.include?(key)
|
@@ -183,42 +270,60 @@ module Rodauth
|
|
183
270
|
|
184
271
|
# set defaults
|
185
272
|
create_params = @oauth_application_params
|
273
|
+
|
274
|
+
# If omitted, an authorization server MAY register a client with a default set of scopes
|
186
275
|
create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_scopes.join(" ")
|
276
|
+
|
277
|
+
# https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
187
278
|
if create_params[oauth_applications_grant_types_column] ||= begin
|
279
|
+
# If omitted, the default behavior is that the client will use only the "authorization_code" Grant Type.
|
188
280
|
return_params["grant_types"] = %w[authorization_code] # rubocop:disable Lint/AssignmentInCondition
|
189
281
|
"authorization_code"
|
190
282
|
end
|
191
283
|
create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin
|
284
|
+
# If unspecified or omitted, the default is "client_secret_basic", denoting the HTTP Basic
|
285
|
+
# authentication scheme as specified in Section 2.3.1 of OAuth 2.0.
|
192
286
|
return_params["token_endpoint_auth_method"] = "client_secret_basic"
|
193
287
|
"client_secret_basic"
|
194
288
|
end
|
195
289
|
end
|
196
290
|
create_params[oauth_applications_response_types_column] ||= begin
|
291
|
+
# If omitted, the default is that the client will use only the "code" response type.
|
197
292
|
return_params["response_types"] = %w[code]
|
198
293
|
"code"
|
199
294
|
end
|
200
295
|
rescue_from_uniqueness_error do
|
201
|
-
|
202
|
-
create_params
|
203
|
-
return_params["client_id"] = client_id
|
204
|
-
return_params["client_id_issued_at"] = Time.now.utc.iso8601
|
205
|
-
if create_params.key?(oauth_applications_client_secret_column)
|
206
|
-
set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
|
207
|
-
return_params.delete("client_secret")
|
208
|
-
else
|
209
|
-
client_secret = oauth_unique_id_generator
|
210
|
-
set_client_secret(create_params, client_secret)
|
211
|
-
return_params["client_secret"] = client_secret
|
212
|
-
return_params["client_secret_expires_at"] = 0
|
213
|
-
|
214
|
-
create_params.delete_if { |k, _| !application_columns.include?(k) }
|
215
|
-
end
|
296
|
+
initialize_register_params(create_params, return_params)
|
297
|
+
create_params.delete_if { |k, _| !application_columns.include?(k) }
|
216
298
|
applications_ds.insert(create_params)
|
217
299
|
end
|
218
300
|
|
219
301
|
return_params
|
220
302
|
end
|
221
303
|
|
304
|
+
def initialize_register_params(create_params, return_params)
|
305
|
+
client_id = oauth_unique_id_generator
|
306
|
+
create_params[oauth_applications_client_id_column] = client_id
|
307
|
+
return_params["client_id"] = client_id
|
308
|
+
return_params["client_id_issued_at"] = Time.now.utc.iso8601
|
309
|
+
|
310
|
+
registration_access_token = oauth_unique_id_generator
|
311
|
+
create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
|
312
|
+
return_params["registration_access_token"] = registration_access_token
|
313
|
+
return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
|
314
|
+
|
315
|
+
if create_params.key?(oauth_applications_client_secret_column)
|
316
|
+
set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
|
317
|
+
return_params.delete("client_secret")
|
318
|
+
else
|
319
|
+
client_secret = oauth_unique_id_generator
|
320
|
+
set_client_secret(create_params, client_secret)
|
321
|
+
return_params["client_secret"] = client_secret
|
322
|
+
return_params["client_secret_expires_at"] = 0
|
323
|
+
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
222
327
|
def register_throw_json_response_error(code, message)
|
223
328
|
throw_json_response_error(oauth_invalid_response_status, code, message)
|
224
329
|
end
|
@@ -264,6 +369,54 @@ module Rodauth
|
|
264
369
|
"type '#{response_type}' to be allowed."
|
265
370
|
end
|
266
371
|
|
372
|
+
def convert_to_boolean(key, value)
|
373
|
+
case value
|
374
|
+
when "true" then true
|
375
|
+
when "false" then false
|
376
|
+
else
|
377
|
+
register_throw_json_response_error(
|
378
|
+
"invalid_client_metadata",
|
379
|
+
register_invalid_param_message(key)
|
380
|
+
)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def json_response_oauth_application(oauth_application)
|
385
|
+
params = methods.map { |k| k.to_s[/\Aoauth_applications_(\w+)_column\z/, 1] }.compact
|
386
|
+
|
387
|
+
body = params.each_with_object({}) do |k, hash|
|
388
|
+
next if %w[id account_id client_id client_secret cliennt_secret_hash].include?(k)
|
389
|
+
|
390
|
+
value = oauth_application[__send__(:"oauth_applications_#{k}_column")]
|
391
|
+
|
392
|
+
next unless value
|
393
|
+
|
394
|
+
case k
|
395
|
+
when "redirect_uri"
|
396
|
+
hash["redirect_uris"] = value.split(" ")
|
397
|
+
when "token_endpoint_auth_method", "grant_types", "response_types", "request_uris", "post_logout_redirect_uris"
|
398
|
+
hash[k] = value.split(" ")
|
399
|
+
when "scopes"
|
400
|
+
hash["scope"] = value
|
401
|
+
when "jwks"
|
402
|
+
hash[k] = value.is_a?(String) ? JSON.parse(value) : value
|
403
|
+
when "homepage_url"
|
404
|
+
hash["client_uri"] = value
|
405
|
+
when "name"
|
406
|
+
hash["client_name"] = value
|
407
|
+
else
|
408
|
+
hash[k] = value
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
response.status = 200
|
413
|
+
response["Content-Type"] ||= json_response_content_type
|
414
|
+
response["Cache-Control"] = "no-store"
|
415
|
+
response["Pragma"] = "no-cache"
|
416
|
+
json_payload = _json_response_body(body)
|
417
|
+
return_response(json_payload)
|
418
|
+
end
|
419
|
+
|
267
420
|
def oauth_server_metadata_body(*)
|
268
421
|
super.tap do |data|
|
269
422
|
data[:registration_endpoint] = register_url
|
@@ -42,7 +42,7 @@ module Rodauth
|
|
42
42
|
oauth_grants_type_column => "implicit",
|
43
43
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
44
44
|
oauth_grants_scopes_column => scopes,
|
45
|
-
|
45
|
+
**resource_owner_params
|
46
46
|
}.merge(grant_params)
|
47
47
|
|
48
48
|
generate_token(grant_params, false)
|
@@ -24,7 +24,8 @@ module Rodauth
|
|
24
24
|
:jwt_decode_no_key,
|
25
25
|
:generate_jti,
|
26
26
|
:oauth_jwt_issuer,
|
27
|
-
:oauth_jwt_audience
|
27
|
+
:oauth_jwt_audience,
|
28
|
+
:resource_owner_params_from_jwt_claims
|
28
29
|
)
|
29
30
|
|
30
31
|
private
|
@@ -70,6 +71,10 @@ module Rodauth
|
|
70
71
|
client_application[oauth_applications_client_id_column]
|
71
72
|
end
|
72
73
|
|
74
|
+
def resource_owner_params_from_jwt_claims(claims)
|
75
|
+
{ oauth_grants_account_id_column => claims["sub"] }
|
76
|
+
end
|
77
|
+
|
73
78
|
def oauth_server_metadata_body(path = nil)
|
74
79
|
metadata = super
|
75
80
|
metadata.merge! \
|
@@ -81,14 +86,6 @@ module Rodauth
|
|
81
86
|
@_jwt_key ||= (oauth_application_jwks(oauth_application) if oauth_application)
|
82
87
|
end
|
83
88
|
|
84
|
-
def _jwt_public_key
|
85
|
-
@_jwt_public_key ||= if oauth_application
|
86
|
-
oauth_application_jwks(oauth_application)
|
87
|
-
else
|
88
|
-
_jwt_key
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
89
|
# Resource Server only!
|
93
90
|
#
|
94
91
|
# returns the jwks set from the authorization server.
|
@@ -152,10 +149,28 @@ module Rodauth
|
|
152
149
|
A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
|
153
150
|
]
|
154
151
|
|
155
|
-
def
|
152
|
+
def key_to_jwk(key)
|
156
153
|
JSON::JWK.new(key)
|
157
154
|
end
|
158
155
|
|
156
|
+
def jwk_export(key)
|
157
|
+
key_to_jwk(key)
|
158
|
+
end
|
159
|
+
|
160
|
+
def jwk_import(jwk)
|
161
|
+
JSON::JWK.new(jwk)
|
162
|
+
end
|
163
|
+
|
164
|
+
def jwk_key(jwk)
|
165
|
+
jwk = jwk_import(jwk) unless jwk.is_a?(JSON::JWK)
|
166
|
+
jwk.to_key
|
167
|
+
end
|
168
|
+
|
169
|
+
def jwk_thumbprint(jwk)
|
170
|
+
jwk = jwk_import(jwk) if jwk.is_a?(Hash)
|
171
|
+
jwk.thumbprint
|
172
|
+
end
|
173
|
+
|
159
174
|
def jwt_encode(payload,
|
160
175
|
jwks: nil,
|
161
176
|
encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
|
@@ -287,8 +302,26 @@ module Rodauth
|
|
287
302
|
auth_value_method :oauth_jwt_jwe_encryption_methods_supported, []
|
288
303
|
end
|
289
304
|
|
305
|
+
def key_to_jwk(key)
|
306
|
+
JWT::JWK.new(key)
|
307
|
+
end
|
308
|
+
|
290
309
|
def jwk_export(key)
|
291
|
-
|
310
|
+
key_to_jwk(key).export
|
311
|
+
end
|
312
|
+
|
313
|
+
def jwk_import(jwk)
|
314
|
+
JWT::JWK.import(jwk)
|
315
|
+
end
|
316
|
+
|
317
|
+
def jwk_key(jwk)
|
318
|
+
jwk = jwk_import(jwk) unless jwk.is_a?(JWT::JWK)
|
319
|
+
jwk.keypair
|
320
|
+
end
|
321
|
+
|
322
|
+
def jwk_thumbprint(jwk)
|
323
|
+
jwk = jwk_import(jwk) if jwk.is_a?(Hash)
|
324
|
+
JWT::JWK::Thumbprint.new(jwk).generate
|
292
325
|
end
|
293
326
|
|
294
327
|
def jwt_encode(payload,
|
@@ -445,6 +478,14 @@ module Rodauth
|
|
445
478
|
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
446
479
|
end
|
447
480
|
|
481
|
+
def jwk_import(_jwk)
|
482
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
483
|
+
end
|
484
|
+
|
485
|
+
def jwk_thumbprint(_jwk)
|
486
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
487
|
+
end
|
488
|
+
|
448
489
|
def jwt_encode(_token)
|
449
490
|
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
450
491
|
end
|
@@ -46,28 +46,7 @@ module Rodauth
|
|
46
46
|
request_object = response.body
|
47
47
|
end
|
48
48
|
|
49
|
-
|
50
|
-
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
51
|
-
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
|
52
|
-
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
|
53
|
-
}.compact
|
54
|
-
|
55
|
-
request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
|
56
|
-
|
57
|
-
if request_sig_enc_opts[:jws_algorithm] == "none"
|
58
|
-
jwks = nil
|
59
|
-
elsif (jwks = oauth_application_jwks(oauth_application))
|
60
|
-
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
61
|
-
else
|
62
|
-
redirect_response_error("invalid_request_object")
|
63
|
-
end
|
64
|
-
|
65
|
-
claims = jwt_decode(request_object,
|
66
|
-
jwks: jwks,
|
67
|
-
verify_jti: false,
|
68
|
-
verify_iss: false,
|
69
|
-
verify_aud: false,
|
70
|
-
**request_sig_enc_opts)
|
49
|
+
claims = decode_request_object(request_object)
|
71
50
|
|
72
51
|
redirect_response_error("invalid_request_object") unless claims
|
73
52
|
|
@@ -105,6 +84,35 @@ module Rodauth
|
|
105
84
|
request_uris.nil? || request_uris.split(oauth_scope_separator).one? { |uri| request_uri.start_with?(uri) }
|
106
85
|
end
|
107
86
|
|
87
|
+
def decode_request_object(request_object)
|
88
|
+
request_sig_enc_opts = {
|
89
|
+
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
90
|
+
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
|
91
|
+
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
|
92
|
+
}.compact
|
93
|
+
|
94
|
+
request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
|
95
|
+
|
96
|
+
if request_sig_enc_opts[:jws_algorithm] == "none"
|
97
|
+
jwks = nil
|
98
|
+
elsif (jwks = oauth_application_jwks(oauth_application))
|
99
|
+
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
100
|
+
else
|
101
|
+
redirect_response_error("invalid_request_object")
|
102
|
+
end
|
103
|
+
|
104
|
+
claims = jwt_decode(request_object,
|
105
|
+
jwks: jwks,
|
106
|
+
verify_jti: false,
|
107
|
+
verify_iss: false,
|
108
|
+
verify_aud: false,
|
109
|
+
**request_sig_enc_opts)
|
110
|
+
|
111
|
+
redirect_response_error("invalid_request_object") unless claims
|
112
|
+
|
113
|
+
claims
|
114
|
+
end
|
115
|
+
|
108
116
|
def oauth_server_metadata_body(*)
|
109
117
|
super.tap do |data|
|
110
118
|
data[:request_parameter_supported] = true
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_pushed_authorization_request, :OauthJwtPushedAuthorizationRequest) do
|
7
|
+
depends :oauth_authorize_base
|
8
|
+
|
9
|
+
auth_value_method :oauth_require_pushed_authorization_requests, false
|
10
|
+
auth_value_method :oauth_applications_require_pushed_authorization_requests_column, :require_pushed_authorization_requests
|
11
|
+
auth_value_method :oauth_pushed_authorization_request_expires_in, 90 # 90 seconds
|
12
|
+
auth_value_method :oauth_require_pushed_authorization_request_iss_request_object, true
|
13
|
+
|
14
|
+
auth_value_method :oauth_pushed_authorization_requests_table, :oauth_pushed_requests
|
15
|
+
|
16
|
+
%i[
|
17
|
+
oauth_application_id params code expires_in
|
18
|
+
].each do |column|
|
19
|
+
auth_value_method :"oauth_pushed_authorization_requests_#{column}_column", column
|
20
|
+
end
|
21
|
+
|
22
|
+
# /par
|
23
|
+
auth_server_route(:par) do |r|
|
24
|
+
require_oauth_application
|
25
|
+
before_par_route
|
26
|
+
|
27
|
+
r.post do
|
28
|
+
validate_par_params
|
29
|
+
|
30
|
+
ds = db[oauth_pushed_authorization_requests_table]
|
31
|
+
|
32
|
+
code = oauth_unique_id_generator
|
33
|
+
push_request_params = {
|
34
|
+
oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
35
|
+
oauth_pushed_authorization_requests_code_column => code,
|
36
|
+
oauth_pushed_authorization_requests_params_column => URI.encode_www_form(request.params),
|
37
|
+
oauth_pushed_authorization_requests_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
|
38
|
+
seconds: oauth_pushed_authorization_request_expires_in)
|
39
|
+
}
|
40
|
+
|
41
|
+
rescue_from_uniqueness_error do
|
42
|
+
ds.insert(push_request_params)
|
43
|
+
end
|
44
|
+
|
45
|
+
json_response_success(
|
46
|
+
"request_uri" => "urn:ietf:params:oauth:request_uri:#{code}",
|
47
|
+
"expires_in" => oauth_pushed_authorization_request_expires_in
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_csrf?
|
53
|
+
case request.path
|
54
|
+
when par_path
|
55
|
+
false
|
56
|
+
else
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def validate_par_params
|
64
|
+
# https://datatracker.ietf.org/doc/html/rfc9126#section-2.1
|
65
|
+
# The request_uri authorization request parameter is one exception, and it MUST NOT be provided.
|
66
|
+
redirect_response_error("invalid_request") if param_or_nil("request_uri")
|
67
|
+
|
68
|
+
if (request_object = param_or_nil("request")) && features.include?(:oauth_jwt_secured_authorization_request)
|
69
|
+
claims = decode_request_object(request_object)
|
70
|
+
|
71
|
+
# https://datatracker.ietf.org/doc/html/rfc9126#section-3-5.3
|
72
|
+
# reject the request if the authenticated client_id does not match the client_id claim in the Request Object
|
73
|
+
if (client_id = claims["client_id"]) && (client_id != oauth_application[oauth_applications_client_id_column])
|
74
|
+
redirect_response_error("invalid_request_object")
|
75
|
+
end
|
76
|
+
|
77
|
+
# requiring the iss claim to match the client_id is at the discretion of the authorization server
|
78
|
+
if oauth_require_pushed_authorization_request_iss_request_object &&
|
79
|
+
(iss = claims.delete("iss")) &&
|
80
|
+
iss != oauth_application[oauth_applications_client_id_column]
|
81
|
+
redirect_response_error("invalid_request_object")
|
82
|
+
end
|
83
|
+
|
84
|
+
if (aud = claims.delete("aud")) && !verify_aud(aud, oauth_jwt_issuer)
|
85
|
+
redirect_response_error("invalid_request_object")
|
86
|
+
end
|
87
|
+
|
88
|
+
claims.delete("exp")
|
89
|
+
request.params.delete("request")
|
90
|
+
|
91
|
+
claims.each do |k, v|
|
92
|
+
request.params[k.to_s] = v
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
validate_authorize_params
|
97
|
+
end
|
98
|
+
|
99
|
+
def validate_authorize_params
|
100
|
+
return super unless request.get? && request.path == authorize_path
|
101
|
+
|
102
|
+
if (request_uri = param_or_nil("request_uri"))
|
103
|
+
code = request_uri.delete_prefix("urn:ietf:params:oauth:request_uri:")
|
104
|
+
|
105
|
+
table = oauth_pushed_authorization_requests_table
|
106
|
+
ds = db[table]
|
107
|
+
|
108
|
+
pushed_request = ds.where(
|
109
|
+
oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
110
|
+
oauth_pushed_authorization_requests_code_column => code
|
111
|
+
).where(
|
112
|
+
Sequel.expr(Sequel[table][oauth_pushed_authorization_requests_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP
|
113
|
+
).first
|
114
|
+
|
115
|
+
redirect_response_error("invalid_request") unless pushed_request
|
116
|
+
|
117
|
+
URI.decode_www_form(pushed_request[oauth_pushed_authorization_requests_params_column]).each do |k, v|
|
118
|
+
request.params[k.to_s] = v
|
119
|
+
end
|
120
|
+
|
121
|
+
elsif oauth_require_pushed_authorization_requests ||
|
122
|
+
(oauth_application && oauth_application[oauth_applications_require_pushed_authorization_requests_column])
|
123
|
+
redirect_authorize_error("request_uri")
|
124
|
+
end
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
def oauth_server_metadata_body(*)
|
129
|
+
super.tap do |data|
|
130
|
+
data[:require_pushed_authorization_requests] = oauth_require_pushed_authorization_requests
|
131
|
+
data[:pushed_authorization_request_endpoint] = par_url
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "ipaddr"
|
5
|
+
require "uri"
|
6
|
+
require "rodauth/oauth"
|
7
|
+
|
8
|
+
module Rodauth
|
9
|
+
Feature.define(:oauth_tls_client_auth, :OauthTlsClientAuth) do
|
10
|
+
depends :oauth_jwt_base
|
11
|
+
|
12
|
+
auth_value_method :oauth_tls_client_certificate_bound_access_tokens, false
|
13
|
+
|
14
|
+
%i[
|
15
|
+
tls_client_auth_subject_dn tls_client_auth_san_dns
|
16
|
+
tls_client_auth_san_uri tls_client_auth_san_ip
|
17
|
+
tls_client_auth_san_email tls_client_certificate_bound_access_tokens
|
18
|
+
].each do |column|
|
19
|
+
auth_value_method :"oauth_applications_#{column}_column", column
|
20
|
+
end
|
21
|
+
|
22
|
+
auth_value_method :oauth_grants_certificate_thumbprint_column, :certificate_thumbprint
|
23
|
+
|
24
|
+
def oauth_token_endpoint_auth_methods_supported
|
25
|
+
super | %w[tls_client_auth self_signed_tls_client_auth]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate_token_params
|
31
|
+
# For all requests to the authorization server utilizing mutual-TLS client authentication,
|
32
|
+
# the client MUST include the client_id parameter
|
33
|
+
redirect_response_error("invalid_request") if client_certificate && !param_or_nil("client_id")
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
def require_oauth_application
|
39
|
+
return super unless client_certificate
|
40
|
+
|
41
|
+
authorization_required unless oauth_application
|
42
|
+
|
43
|
+
if supports_auth_method?(oauth_application, "tls_client_auth")
|
44
|
+
# It relies on a validated certificate chain [RFC5280]
|
45
|
+
|
46
|
+
ssl_verify = request.env["SSL_CLIENT_VERIFY"] || request.env["HTTP_SSL_CLIENT_VERIFY"] || request.env["HTTP_X_SSL_CLIENT_VERIFY"]
|
47
|
+
|
48
|
+
authorization_required unless ssl_verify == "SUCCESS"
|
49
|
+
|
50
|
+
# and a single subject distinguished name (DN) or a single subject alternative name (SAN) to
|
51
|
+
# authenticate the client. Only one subject name value of any type is used for each client.
|
52
|
+
|
53
|
+
name_matches = if oauth_application[:tls_client_auth_subject_dn]
|
54
|
+
distinguished_name_match?(client_certificate.subject, oauth_application[:tls_client_auth_subject_dn])
|
55
|
+
elsif (dns = oauth_application[:tls_client_auth_san_dns])
|
56
|
+
client_certificate_sans.any? { |san| san.tag == 2 && OpenSSL::SSL.verify_hostname(dns, san.value) }
|
57
|
+
elsif (uri = oauth_application[:tls_client_auth_san_uri])
|
58
|
+
uri = URI(uri)
|
59
|
+
client_certificate_sans.any? { |san| san.tag == 6 && URI(san.value) == uri }
|
60
|
+
elsif (ip = oauth_application[:tls_client_auth_san_ip])
|
61
|
+
ip = IPAddr.new(ip).hton
|
62
|
+
client_certificate_sans.any? { |san| san.tag == 7 && san.value == ip }
|
63
|
+
elsif (email = oauth_application[:tls_client_auth_san_email])
|
64
|
+
client_certificate_sans.any? { |san| san.tag == 1 && san.value == email }
|
65
|
+
else
|
66
|
+
false
|
67
|
+
end
|
68
|
+
authorization_required unless name_matches
|
69
|
+
|
70
|
+
oauth_application
|
71
|
+
elsif supports_auth_method?(oauth_application, "self_signed_tls_client_auth")
|
72
|
+
jwks = oauth_application_jwks(oauth_application)
|
73
|
+
|
74
|
+
thumbprint = jwk_thumbprint(key_to_jwk(client_certificate.public_key))
|
75
|
+
|
76
|
+
# The client is successfully authenticated if the certificate that it presented during the handshake
|
77
|
+
# matches one of the certificates configured or registered for that particular client.
|
78
|
+
authorization_required unless jwks.any? { |jwk| Array(jwk[:x5c]).first == thumbprint }
|
79
|
+
|
80
|
+
oauth_application
|
81
|
+
else
|
82
|
+
super
|
83
|
+
end
|
84
|
+
rescue URI::InvalidURIError, IPAddr::InvalidAddressError
|
85
|
+
authorization_required
|
86
|
+
end
|
87
|
+
|
88
|
+
def store_token(grant_params, update_params = {})
|
89
|
+
return super unless client_certificate && (
|
90
|
+
oauth_tls_client_certificate_bound_access_tokens ||
|
91
|
+
oauth_application[oauth_applications_tls_client_certificate_bound_access_tokens_column]
|
92
|
+
)
|
93
|
+
|
94
|
+
update_params[oauth_grants_certificate_thumbprint_column] = jwk_thumbprint(key_to_jwk(client_certificate.public_key))
|
95
|
+
super
|
96
|
+
end
|
97
|
+
|
98
|
+
def jwt_claims(oauth_grant)
|
99
|
+
claims = super
|
100
|
+
|
101
|
+
return claims unless oauth_grant[oauth_grants_certificate_thumbprint_column]
|
102
|
+
|
103
|
+
claims[:cnf] = {
|
104
|
+
"x5t#S256" => oauth_grant[oauth_grants_certificate_thumbprint_column]
|
105
|
+
}
|
106
|
+
|
107
|
+
claims
|
108
|
+
end
|
109
|
+
|
110
|
+
def json_token_introspect_payload(grant_or_claims)
|
111
|
+
claims = super
|
112
|
+
|
113
|
+
return claims unless grant_or_claims && grant_or_claims[oauth_grants_certificate_thumbprint_column]
|
114
|
+
|
115
|
+
claims[:cnf] = {
|
116
|
+
"x5t#S256" => grant_or_claims[oauth_grants_certificate_thumbprint_column]
|
117
|
+
}
|
118
|
+
|
119
|
+
claims
|
120
|
+
end
|
121
|
+
|
122
|
+
def oauth_server_metadata_body(*)
|
123
|
+
super.tap do |data|
|
124
|
+
data[:tls_client_certificate_bound_access_tokens] = oauth_tls_client_certificate_bound_access_tokens
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def client_certificate
|
129
|
+
return @client_certificate if defined?(@client_certificate)
|
130
|
+
|
131
|
+
unless (pem_cert = request.env["SSL_CLIENT_CERT"] || request.env["HTTP_SSL_CLIENT_CERT"] || request.env["HTTP_X_SSL_CLIENT_CERT"])
|
132
|
+
return
|
133
|
+
end
|
134
|
+
|
135
|
+
return if pem_cert.empty?
|
136
|
+
|
137
|
+
@certificate = OpenSSL::X509::Certificate.new(pem_cert)
|
138
|
+
end
|
139
|
+
|
140
|
+
def client_certificate_sans
|
141
|
+
return @client_certificate_sans if defined?(@client_certificate_sans)
|
142
|
+
|
143
|
+
@client_certificate_sans = begin
|
144
|
+
return [] unless client_certificate
|
145
|
+
|
146
|
+
san = client_certificate.extensions.find { |ext| ext.oid == "subjectAltName" }
|
147
|
+
|
148
|
+
return [] unless san
|
149
|
+
|
150
|
+
ostr = OpenSSL::ASN1.decode(san.to_der).value.last
|
151
|
+
|
152
|
+
sans = OpenSSL::ASN1.decode(ostr.value)
|
153
|
+
|
154
|
+
return [] unless sans
|
155
|
+
|
156
|
+
sans.value
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def distinguished_name_match?(sub1, sub2)
|
161
|
+
sub1 = OpenSSL::X509::Name.parse(sub1) if sub1.is_a?(String)
|
162
|
+
sub2 = OpenSSL::X509::Name.parse(sub2) if sub2.is_a?(String)
|
163
|
+
# OpenSSL::X509::Name#cp calls X509_NAME_cmp via openssl.
|
164
|
+
# https://www.openssl.org/docs/manmaster/man3/X509_NAME_cmp.html
|
165
|
+
# This procedure adheres to the matching rules for Distinguished Names (DN) given in
|
166
|
+
# RFC 4517 section 4.2.15 and RFC 5280 section 7.1.
|
167
|
+
sub1.cmp(sub2).zero?
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -68,7 +68,7 @@ module Rodauth
|
|
68
68
|
auth_value_method :oauth_application_scopes, %w[openid]
|
69
69
|
|
70
70
|
%i[
|
71
|
-
subject_type application_type sector_identifier_uri
|
71
|
+
subject_type application_type sector_identifier_uri initiate_login_uri
|
72
72
|
id_token_signed_response_alg id_token_encrypted_response_alg id_token_encrypted_response_enc
|
73
73
|
userinfo_signed_response_alg userinfo_encrypted_response_alg userinfo_encrypted_response_enc
|
74
74
|
].each do |column|
|
@@ -112,7 +112,7 @@ module Rodauth
|
|
112
112
|
|
113
113
|
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
|
114
114
|
|
115
|
-
account =
|
115
|
+
account = account_ds(claims["sub"]).first
|
116
116
|
|
117
117
|
throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless account
|
118
118
|
|
@@ -126,7 +126,7 @@ module Rodauth
|
|
126
126
|
|
127
127
|
oauth_grant = valid_oauth_grant_ds(
|
128
128
|
oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
|
129
|
-
|
129
|
+
**resource_owner_params_from_jwt_claims(claims)
|
130
130
|
).first
|
131
131
|
|
132
132
|
claims_locales = oauth_grant[oauth_grants_claims_locales_column] if oauth_grant
|
@@ -333,8 +333,9 @@ module Rodauth
|
|
333
333
|
|
334
334
|
identifier_uri = URI(identifier_uri).host
|
335
335
|
|
336
|
-
|
337
|
-
|
336
|
+
account_ids = oauth_grant.values_at(oauth_grants_resource_owner_columns)
|
337
|
+
values = [identifier_uri, *account_ids, oauth_jwt_subject_secret]
|
338
|
+
Digest::SHA256.hexdigest(values.join)
|
338
339
|
else
|
339
340
|
raise StandardError, "unexpected subject (#{subject_type})"
|
340
341
|
end
|
@@ -434,8 +435,8 @@ module Rodauth
|
|
434
435
|
end
|
435
436
|
|
436
437
|
def create_oauth_grant_with_token(create_params = {})
|
438
|
+
create_params.merge!(resource_owner_params)
|
437
439
|
create_params[oauth_grants_type_column] = "hybrid"
|
438
|
-
create_params[oauth_grants_account_id_column] = account_id
|
439
440
|
create_params[oauth_grants_expires_in_column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in)
|
440
441
|
authorization_code = create_oauth_grant(create_params)
|
441
442
|
access_token = if oauth_jwt_access_tokens
|
@@ -587,14 +588,14 @@ module Rodauth
|
|
587
588
|
additional_info = additional_claims_info[param] || EMPTY_HASH
|
588
589
|
value = additional_info["value"] || meth[account, param]
|
589
590
|
value = nil if additional_info["values"] && additional_info["values"].include?(value)
|
590
|
-
cl[param] = value
|
591
|
+
cl[param] = value unless value.nil?
|
591
592
|
end
|
592
593
|
elsif claims_locales.nil?
|
593
594
|
lambda do |account, param, cl = claims|
|
594
595
|
additional_info = additional_claims_info[param] || EMPTY_HASH
|
595
596
|
value = additional_info["value"] || meth[account, param, nil]
|
596
597
|
value = nil if additional_info["values"] && additional_info["values"].include?(value)
|
597
|
-
cl[param] = value
|
598
|
+
cl[param] = value unless value.nil?
|
598
599
|
end
|
599
600
|
else
|
600
601
|
lambda do |account, param, cl = claims|
|
@@ -691,7 +692,7 @@ module Rodauth
|
|
691
692
|
|
692
693
|
def oidc_grant_params
|
693
694
|
grant_params = {
|
694
|
-
|
695
|
+
**resource_owner_params,
|
695
696
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
696
697
|
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
697
698
|
}
|
@@ -43,7 +43,11 @@ module Rodauth
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
if (value = @oauth_application_params[oauth_applications_sector_identifier_uri_column])
|
46
|
+
if (value = @oauth_application_params[oauth_applications_sector_identifier_uri_column]) && !check_valid_uri?(value)
|
47
|
+
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
|
48
|
+
end
|
49
|
+
|
50
|
+
if (value = @oauth_application_params[oauth_applications_initiate_login_uri_column])
|
47
51
|
uri = URI(value)
|
48
52
|
|
49
53
|
unless uri.scheme == "https" || uri.host == "localhost"
|
@@ -219,5 +223,13 @@ module Rodauth
|
|
219
223
|
def register_invalid_application_type_message(application_type)
|
220
224
|
"The application type '#{application_type}' is not allowed."
|
221
225
|
end
|
226
|
+
|
227
|
+
def initialize_register_params(create_params, return_params)
|
228
|
+
super
|
229
|
+
registration_access_token = oauth_unique_id_generator
|
230
|
+
create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
|
231
|
+
return_params["registration_access_token"] = registration_access_token
|
232
|
+
return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
|
233
|
+
end
|
222
234
|
end
|
223
235
|
end
|
@@ -36,10 +36,9 @@ module Rodauth
|
|
36
36
|
|
37
37
|
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["aud"]).first
|
38
38
|
oauth_grant = db[oauth_grants_table]
|
39
|
-
.where(
|
40
|
-
|
41
|
-
|
42
|
-
).first
|
39
|
+
.where(resource_owner_params)
|
40
|
+
.where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
|
41
|
+
.first
|
43
42
|
|
44
43
|
# check whether ID token belongs to currently logged-in user
|
45
44
|
redirect_logout_with_error(oauth_invalid_client_message) unless oauth_grant && claims["sub"] == jwt_subject(oauth_grant,
|
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.2.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: 2023-
|
11
|
+
date: 2023-02-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rodauth
|
@@ -68,6 +68,7 @@ extra_rdoc_files:
|
|
68
68
|
- doc/release_notes/0_9_3.md
|
69
69
|
- doc/release_notes/1_0_0.md
|
70
70
|
- doc/release_notes/1_1_0.md
|
71
|
+
- doc/release_notes/1_2_0.md
|
71
72
|
files:
|
72
73
|
- CHANGELOG.md
|
73
74
|
- LICENSE.txt
|
@@ -107,6 +108,7 @@ files:
|
|
107
108
|
- doc/release_notes/0_9_3.md
|
108
109
|
- doc/release_notes/1_0_0.md
|
109
110
|
- doc/release_notes/1_1_0.md
|
111
|
+
- doc/release_notes/1_2_0.md
|
110
112
|
- lib/generators/rodauth/oauth/install_generator.rb
|
111
113
|
- lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
|
112
114
|
- lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
|
@@ -138,9 +140,11 @@ files:
|
|
138
140
|
- lib/rodauth/features/oauth_jwt_secured_authorization_request.rb
|
139
141
|
- lib/rodauth/features/oauth_management_base.rb
|
140
142
|
- lib/rodauth/features/oauth_pkce.rb
|
143
|
+
- lib/rodauth/features/oauth_pushed_authorization_request.rb
|
141
144
|
- lib/rodauth/features/oauth_resource_indicators.rb
|
142
145
|
- lib/rodauth/features/oauth_resource_server.rb
|
143
146
|
- lib/rodauth/features/oauth_saml_bearer_grant.rb
|
147
|
+
- lib/rodauth/features/oauth_tls_client_auth.rb
|
144
148
|
- lib/rodauth/features/oauth_token_introspection.rb
|
145
149
|
- lib/rodauth/features/oauth_token_revocation.rb
|
146
150
|
- lib/rodauth/features/oidc.rb
|