rodauth-oauth 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|