rodauth-oauth 0.7.4 → 0.8.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/CHANGELOG.md +1 -424
- data/README.md +26 -389
- data/doc/release_notes/0_0_1.md +3 -0
- data/doc/release_notes/0_0_2.md +15 -0
- data/doc/release_notes/0_0_3.md +31 -0
- data/doc/release_notes/0_0_4.md +36 -0
- data/doc/release_notes/0_0_5.md +36 -0
- data/doc/release_notes/0_0_6.md +21 -0
- data/doc/release_notes/0_1_0.md +44 -0
- data/doc/release_notes/0_2_0.md +43 -0
- data/doc/release_notes/0_3_0.md +28 -0
- data/doc/release_notes/0_4_0.md +18 -0
- data/doc/release_notes/0_4_1.md +9 -0
- data/doc/release_notes/0_4_2.md +5 -0
- data/doc/release_notes/0_4_3.md +3 -0
- data/doc/release_notes/0_5_0.md +11 -0
- data/doc/release_notes/0_5_1.md +13 -0
- data/doc/release_notes/0_6_0.md +9 -0
- data/doc/release_notes/0_6_1.md +6 -0
- data/doc/release_notes/0_7_0.md +20 -0
- data/doc/release_notes/0_7_1.md +10 -0
- data/doc/release_notes/0_7_2.md +21 -0
- data/doc/release_notes/0_7_3.md +10 -0
- data/doc/release_notes/0_7_4.md +5 -0
- data/doc/release_notes/0_8_0.md +37 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +3 -3
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +22 -10
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +11 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +38 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +5 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +11 -15
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +9 -1
- data/lib/rodauth/features/oauth.rb +3 -1418
- data/lib/rodauth/features/oauth_application_management.rb +209 -0
- data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +249 -0
- data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
- data/lib/rodauth/features/oauth_base.rb +735 -0
- data/lib/rodauth/features/oauth_device_grant.rb +221 -0
- data/lib/rodauth/features/oauth_http_mac.rb +3 -21
- data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
- data/lib/rodauth/features/oauth_jwt.rb +37 -60
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
- data/lib/rodauth/features/oauth_pkce.rb +98 -0
- data/lib/rodauth/features/oauth_resource_server.rb +21 -0
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
- data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
- data/lib/rodauth/features/oauth_token_management.rb +77 -0
- data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
- data/lib/rodauth/features/oidc.rb +4 -3
- data/lib/rodauth/oauth/database_extensions.rb +15 -2
- data/lib/rodauth/oauth/refinements.rb +48 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +28 -12
- data/templates/authorize.str +7 -7
- data/templates/client_secret_field.str +2 -2
- data/templates/description_field.str +1 -1
- data/templates/device_search.str +11 -0
- data/templates/device_verification.str +24 -0
- data/templates/homepage_url_field.str +2 -2
- data/templates/jws_jwk_field.str +4 -0
- data/templates/jwt_public_key_field.str +4 -0
- data/templates/name_field.str +1 -1
- data/templates/new_oauth_application.str +9 -0
- data/templates/oauth_application.str +7 -3
- data/templates/oauth_application_oauth_tokens.str +51 -0
- data/templates/oauth_applications.str +2 -2
- data/templates/oauth_tokens.str +9 -11
- data/templates/redirect_uri_field.str +2 -2
- metadata +71 -3
- data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_device_grant, :OauthDeviceGrant) do
|
5
|
+
depends :oauth_base
|
6
|
+
|
7
|
+
auth_value_method :use_oauth_device_code_grant_type?, false
|
8
|
+
|
9
|
+
before "device_authorization"
|
10
|
+
before "device_verification"
|
11
|
+
|
12
|
+
notice_flash "The device is verified", "device_verification"
|
13
|
+
error_flash "No device to authorize with the given user code", "user_code_not_found"
|
14
|
+
|
15
|
+
view "device_verification", "Device Verification", "device_verification"
|
16
|
+
view "device_search", "Device Search", "device_search"
|
17
|
+
|
18
|
+
button "Verify", "oauth_device_verification"
|
19
|
+
button "Search", "oauth_device_search"
|
20
|
+
|
21
|
+
auth_value_method :oauth_grants_user_code_column, :user_code
|
22
|
+
auth_value_method :oauth_grants_last_polled_at_column, :last_polled_at
|
23
|
+
|
24
|
+
translatable_method :expired_token_message, "the device code has expired"
|
25
|
+
translatable_method :access_denied_message, "the authorization request has been denied"
|
26
|
+
translatable_method :authorization_pending_message, "the authorization request is still pending"
|
27
|
+
translatable_method :slow_down_message, "authorization request is still pending but poll interval should be increased"
|
28
|
+
|
29
|
+
auth_value_method :oauth_device_code_grant_polling_interval, 5 # seconds
|
30
|
+
auth_value_method :oauth_device_code_grant_user_code_size, 8 # characters
|
31
|
+
%w[user_code].each do |param|
|
32
|
+
auth_value_method :"oauth_grant_#{param}_param", param
|
33
|
+
end
|
34
|
+
translatable_method :oauth_grant_user_code_label, "User code"
|
35
|
+
|
36
|
+
auth_value_methods(
|
37
|
+
:generate_user_code
|
38
|
+
)
|
39
|
+
|
40
|
+
# /device-authorization
|
41
|
+
route(:device_authorization) do |r|
|
42
|
+
next unless is_authorization_server? && use_oauth_device_code_grant_type?
|
43
|
+
|
44
|
+
before_device_authorization_route
|
45
|
+
|
46
|
+
r.post do
|
47
|
+
require_oauth_application
|
48
|
+
|
49
|
+
user_code = generate_user_code
|
50
|
+
device_code = transaction do
|
51
|
+
before_device_authorization
|
52
|
+
create_oauth_grant(oauth_grants_user_code_column => user_code)
|
53
|
+
end
|
54
|
+
|
55
|
+
json_response_success \
|
56
|
+
"device_code" => device_code,
|
57
|
+
"user_code" => user_code,
|
58
|
+
"verification_uri" => device_url,
|
59
|
+
"verification_uri_complete" => device_url(user_code: user_code),
|
60
|
+
"expires_in" => oauth_grant_expires_in,
|
61
|
+
"interval" => oauth_device_code_grant_polling_interval
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# /device
|
66
|
+
route(:device) do |r|
|
67
|
+
next unless is_authorization_server? && use_oauth_device_code_grant_type?
|
68
|
+
|
69
|
+
before_device_route
|
70
|
+
require_authorizable_account
|
71
|
+
|
72
|
+
r.get do
|
73
|
+
if (user_code = param_or_nil("user_code"))
|
74
|
+
oauth_grant = db[oauth_grants_table].where(
|
75
|
+
oauth_grants_user_code_column => user_code,
|
76
|
+
oauth_grants_revoked_at_column => nil
|
77
|
+
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP).first
|
78
|
+
|
79
|
+
unless oauth_grant
|
80
|
+
set_redirect_error_flash user_code_not_found_error_flash
|
81
|
+
redirect device_path
|
82
|
+
end
|
83
|
+
|
84
|
+
scope.instance_variable_set(:@oauth_grant, oauth_grant)
|
85
|
+
device_verification_view
|
86
|
+
else
|
87
|
+
device_search_view
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
r.post do
|
92
|
+
catch_error do
|
93
|
+
unless param_or_nil("user_code")
|
94
|
+
set_redirect_error_flash invalid_grant_message
|
95
|
+
redirect device_path
|
96
|
+
end
|
97
|
+
|
98
|
+
transaction do
|
99
|
+
before_device_verification
|
100
|
+
create_oauth_token("device_code")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
set_notice_flash device_verification_notice_flash
|
104
|
+
redirect device_path
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def check_csrf?
|
109
|
+
case request.path
|
110
|
+
when device_authorization_path
|
111
|
+
false
|
112
|
+
else
|
113
|
+
super
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def generate_user_code
|
120
|
+
user_code_size = oauth_device_code_grant_user_code_size
|
121
|
+
SecureRandom.random_number(36**user_code_size)
|
122
|
+
.to_s(36) # 0 to 9, a to z
|
123
|
+
.upcase
|
124
|
+
.rjust(user_code_size, "0")
|
125
|
+
end
|
126
|
+
|
127
|
+
def authorized_oauth_application?(oauth_application, client_secret)
|
128
|
+
# skip if using device grant
|
129
|
+
#
|
130
|
+
# requests may be performed by devices with no knowledge of client secret.
|
131
|
+
return true if !client_secret && oauth_application && use_oauth_device_code_grant_type?
|
132
|
+
|
133
|
+
super
|
134
|
+
end
|
135
|
+
|
136
|
+
def create_oauth_token(grant_type)
|
137
|
+
case grant_type
|
138
|
+
when "urn:ietf:params:oauth:grant-type:device_code"
|
139
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_grant_type") unless use_oauth_device_code_grant_type?
|
140
|
+
|
141
|
+
oauth_grant = db[oauth_grants_table].where(
|
142
|
+
oauth_grants_code_column => param("device_code"),
|
143
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
|
144
|
+
).for_update.first
|
145
|
+
|
146
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_grant") unless oauth_grant
|
147
|
+
|
148
|
+
now = Time.now
|
149
|
+
|
150
|
+
if oauth_grant[oauth_grants_revoked_at_column]
|
151
|
+
oauth_token = db[oauth_tokens_table]
|
152
|
+
.where(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
153
|
+
.where(Sequel[oauth_tokens_table][oauth_tokens_revoked_at_column] => nil)
|
154
|
+
.where(oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column])
|
155
|
+
.first
|
156
|
+
|
157
|
+
throw_json_response_error(invalid_oauth_response_status, "access_denied") unless oauth_token
|
158
|
+
elsif oauth_grant[oauth_grants_expires_in_column] < now
|
159
|
+
throw_json_response_error(invalid_oauth_response_status, "expired_token")
|
160
|
+
else
|
161
|
+
last_polled_at = oauth_grant[oauth_grants_last_polled_at_column]
|
162
|
+
if last_polled_at && convert_timestamp(last_polled_at) + oauth_device_code_grant_polling_interval > now
|
163
|
+
throw_json_response_error(invalid_oauth_response_status, "slow_down")
|
164
|
+
else
|
165
|
+
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
166
|
+
.update(oauth_grants_last_polled_at_column => Sequel::CURRENT_TIMESTAMP)
|
167
|
+
throw_json_response_error(invalid_oauth_response_status, "authorization_pending")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
oauth_token
|
171
|
+
when "device_code"
|
172
|
+
redirect_response_error("invalid_grant_type") unless use_oauth_device_code_grant_type?
|
173
|
+
|
174
|
+
# fetch oauth grant
|
175
|
+
oauth_grant = db[oauth_grants_table].where(
|
176
|
+
oauth_grants_user_code_column => param("user_code"),
|
177
|
+
oauth_grants_revoked_at_column => nil
|
178
|
+
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
179
|
+
.for_update
|
180
|
+
.first
|
181
|
+
|
182
|
+
return unless oauth_grant
|
183
|
+
|
184
|
+
# update ownership
|
185
|
+
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
186
|
+
.update(
|
187
|
+
oauth_grants_user_code_column => nil,
|
188
|
+
oauth_grants_account_id_column => account_id
|
189
|
+
)
|
190
|
+
|
191
|
+
create_params = {
|
192
|
+
oauth_tokens_account_id_column => account_id,
|
193
|
+
oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
|
194
|
+
oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
|
195
|
+
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
196
|
+
}
|
197
|
+
create_oauth_token_from_authorization_code(oauth_grant, create_params)
|
198
|
+
else
|
199
|
+
super
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def validate_oauth_token_params
|
204
|
+
grant_type = param_or_nil("grant_type")
|
205
|
+
|
206
|
+
if grant_type == "urn:ietf:params:oauth:grant-type:device_code" && !param_or_nil("device_code")
|
207
|
+
redirect_response_error("invalid_request")
|
208
|
+
end
|
209
|
+
super
|
210
|
+
end
|
211
|
+
|
212
|
+
def oauth_server_metadata_body(*)
|
213
|
+
super.tap do |data|
|
214
|
+
if use_oauth_device_code_grant_type?
|
215
|
+
data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:device_code"
|
216
|
+
data[:device_authorization_endpoint] = device_authorization_url
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -1,28 +1,10 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
+
require "rodauth/oauth/refinements"
|
4
|
+
|
3
5
|
module Rodauth
|
4
6
|
Feature.define(:oauth_http_mac, :OauthHttpMac) do
|
5
|
-
|
6
|
-
module PrefixExtensions
|
7
|
-
refine(String) do
|
8
|
-
def delete_suffix(suffix)
|
9
|
-
suffix = suffix.to_s
|
10
|
-
len = suffix.length
|
11
|
-
return dup unless len.positive? && index(suffix, -len)
|
12
|
-
|
13
|
-
self[0...-len]
|
14
|
-
end
|
15
|
-
|
16
|
-
def delete_prefix(prefix)
|
17
|
-
prefix = prefix.to_s
|
18
|
-
return dup unless rindex(prefix, 0)
|
19
|
-
|
20
|
-
self[prefix.length..-1]
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
using(PrefixExtensions)
|
25
|
-
end
|
7
|
+
using PrefixExtensions
|
26
8
|
|
27
9
|
depends :oauth
|
28
10
|
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_implicit_grant, :OauthImplicitGrant) do
|
5
|
+
depends :oauth_base
|
6
|
+
|
7
|
+
auth_value_method :use_oauth_implicit_grant_type?, false
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
|
12
|
+
return super unless param("response_type") == "token" && use_oauth_implicit_grant_type?
|
13
|
+
|
14
|
+
response_mode ||= "fragment"
|
15
|
+
response_params.replace(_do_authorize_token)
|
16
|
+
|
17
|
+
response_params["state"] = param("state") if param_or_nil("state")
|
18
|
+
|
19
|
+
[response_params, response_mode]
|
20
|
+
end
|
21
|
+
|
22
|
+
def _do_authorize_token
|
23
|
+
create_params = {
|
24
|
+
oauth_tokens_account_id_column => account_id,
|
25
|
+
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
26
|
+
oauth_tokens_scopes_column => scopes
|
27
|
+
}
|
28
|
+
oauth_token = generate_oauth_token(create_params, false)
|
29
|
+
|
30
|
+
json_access_token_payload(oauth_token)
|
31
|
+
end
|
32
|
+
|
33
|
+
def authorize_response(params, mode)
|
34
|
+
return super unless mode == "fragment"
|
35
|
+
|
36
|
+
redirect_url = URI.parse(redirect_uri)
|
37
|
+
params = params.map { |k, v| "#{k}=#{v}" }
|
38
|
+
params << redirect_url.query if redirect_url.query
|
39
|
+
redirect_url.fragment = params.join("&")
|
40
|
+
redirect(redirect_url.to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
def oauth_server_metadata_body(*)
|
44
|
+
super.tap do |data|
|
45
|
+
if use_oauth_implicit_grant_type?
|
46
|
+
data[:response_types_supported] << "token"
|
47
|
+
data[:response_modes_supported] << "fragment"
|
48
|
+
data[:grant_types_supported] << "implicit"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_valid_response_type?
|
54
|
+
return true if use_oauth_implicit_grant_type? && param_or_nil("response_type") == "token"
|
55
|
+
|
56
|
+
super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -15,7 +15,13 @@ module Rodauth
|
|
15
15
|
|
16
16
|
auth_value_method :oauth_jwt_token_issuer, nil
|
17
17
|
|
18
|
-
auth_value_method :
|
18
|
+
auth_value_method :oauth_applications_jws_jwk_column, :jws_jwk
|
19
|
+
auth_value_method :oauth_applications_jwt_public_key_column, :jwt_public_key
|
20
|
+
|
21
|
+
translatable_method :oauth_applications_jws_jwk_label, "JSON Web Keys"
|
22
|
+
translatable_method :oauth_applications_jwt_public_key_label, "Public key"
|
23
|
+
auth_value_method :oauth_application_jws_jwk_param, :jws_jwk
|
24
|
+
auth_value_method :oauth_application_jwt_public_key_param, :jwt_public_key
|
19
25
|
|
20
26
|
auth_value_method :oauth_jwt_key, nil
|
21
27
|
auth_value_method :oauth_jwt_public_key, nil
|
@@ -113,9 +119,7 @@ module Rodauth
|
|
113
119
|
|
114
120
|
return super unless request_object && oauth_application
|
115
121
|
|
116
|
-
jws_jwk = if oauth_application[
|
117
|
-
jwk = oauth_application[oauth_application_jws_jwk_column]
|
118
|
-
|
122
|
+
jws_jwk = if (jwk = oauth_application[oauth_applications_jws_jwk_column])
|
119
123
|
jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
|
120
124
|
else
|
121
125
|
redirect_response_error("invalid_request_object")
|
@@ -145,51 +149,6 @@ module Rodauth
|
|
145
149
|
|
146
150
|
# /token
|
147
151
|
|
148
|
-
def require_oauth_application
|
149
|
-
# requset authentication optional for assertions
|
150
|
-
return super unless param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
151
|
-
|
152
|
-
claims = jwt_decode(param("assertion"))
|
153
|
-
|
154
|
-
redirect_response_error("invalid_grant") unless claims
|
155
|
-
|
156
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
157
|
-
|
158
|
-
authorization_required unless @oauth_application
|
159
|
-
end
|
160
|
-
|
161
|
-
def validate_oauth_token_params
|
162
|
-
if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
163
|
-
redirect_response_error("invalid_client") unless param_or_nil("assertion")
|
164
|
-
else
|
165
|
-
super
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def create_oauth_token
|
170
|
-
if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
171
|
-
create_oauth_token_from_assertion
|
172
|
-
else
|
173
|
-
super
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def create_oauth_token_from_assertion
|
178
|
-
claims = jwt_decode(param("assertion"))
|
179
|
-
|
180
|
-
account = account_ds(claims["sub"]).first
|
181
|
-
|
182
|
-
redirect_response_error("invalid_client") unless oauth_application && account
|
183
|
-
|
184
|
-
create_params = {
|
185
|
-
oauth_tokens_account_id_column => claims["sub"],
|
186
|
-
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
187
|
-
oauth_tokens_scopes_column => claims["scope"]
|
188
|
-
}
|
189
|
-
|
190
|
-
generate_oauth_token(create_params, false)
|
191
|
-
end
|
192
|
-
|
193
152
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
194
153
|
create_params = {
|
195
154
|
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
@@ -295,7 +254,17 @@ module Rodauth
|
|
295
254
|
end
|
296
255
|
|
297
256
|
def _jwt_key
|
298
|
-
@_jwt_key ||= oauth_jwt_key ||
|
257
|
+
@_jwt_key ||= oauth_jwt_key || begin
|
258
|
+
if oauth_application
|
259
|
+
|
260
|
+
if (jwk = oauth_application[oauth_applications_jws_jwk_column])
|
261
|
+
jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
|
262
|
+
jwk
|
263
|
+
else
|
264
|
+
oauth_application[oauth_applications_jwt_public_key_column]
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
299
268
|
end
|
300
269
|
|
301
270
|
# Resource Server only!
|
@@ -346,8 +315,8 @@ module Rodauth
|
|
346
315
|
generate_jti(claims) == jti
|
347
316
|
end
|
348
317
|
|
349
|
-
def verify_aud(
|
350
|
-
|
318
|
+
def verify_aud(expected_aud, aud)
|
319
|
+
expected_aud == aud
|
351
320
|
end
|
352
321
|
|
353
322
|
if defined?(JSON::JWT)
|
@@ -379,6 +348,8 @@ module Rodauth
|
|
379
348
|
jws_key: oauth_jwt_public_key || _jwt_key,
|
380
349
|
verify_claims: true,
|
381
350
|
verify_jti: true,
|
351
|
+
verify_iss: true,
|
352
|
+
verify_aud: false,
|
382
353
|
**
|
383
354
|
)
|
384
355
|
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
|
@@ -393,11 +364,15 @@ module Rodauth
|
|
393
364
|
JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
|
394
365
|
end
|
395
366
|
|
396
|
-
|
397
|
-
|
398
|
-
(!claims[:
|
399
|
-
(
|
400
|
-
(
|
367
|
+
now = Time.now
|
368
|
+
if verify_claims && (
|
369
|
+
(!claims[:exp] || Time.at(claims[:exp]) < now) &&
|
370
|
+
(claims[:nbf] && Time.at(claims[:nbf]) < now) &&
|
371
|
+
(claims[:iat] && Time.at(claims[:iat]) < now) &&
|
372
|
+
(verify_iss && claims[:iss] != issuer) &&
|
373
|
+
(verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
|
374
|
+
(verify_jti && !verify_jti(claims[:jti], claims))
|
375
|
+
)
|
401
376
|
return
|
402
377
|
end
|
403
378
|
|
@@ -456,7 +431,9 @@ module Rodauth
|
|
456
431
|
jws_key: oauth_jwt_public_key || _jwt_key,
|
457
432
|
jws_algorithm: oauth_jwt_algorithm,
|
458
433
|
verify_claims: true,
|
459
|
-
verify_jti: true
|
434
|
+
verify_jti: true,
|
435
|
+
verify_iss: true,
|
436
|
+
verify_aud: false
|
460
437
|
)
|
461
438
|
# decrypt jwe
|
462
439
|
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
@@ -472,7 +449,7 @@ module Rodauth
|
|
472
449
|
#
|
473
450
|
verify_claims_params = if verify_claims
|
474
451
|
{
|
475
|
-
verify_iss:
|
452
|
+
verify_iss: verify_iss,
|
476
453
|
iss: issuer,
|
477
454
|
# can't use stock aud verification, as it's dependent on the client application id
|
478
455
|
verify_aud: false,
|
@@ -496,7 +473,7 @@ module Rodauth
|
|
496
473
|
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
|
497
474
|
end
|
498
475
|
|
499
|
-
return if verify_claims && !verify_aud(claims["aud"], claims)
|
476
|
+
return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
|
500
477
|
|
501
478
|
claims
|
502
479
|
rescue JWT::DecodeError, JWT::JWKError
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth/ttl_store"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_jwt_bearer_grant, :OauthJwtBearerGrant) do
|
7
|
+
depends :oauth_assertion_base, :oauth_jwt
|
8
|
+
|
9
|
+
auth_value_methods(
|
10
|
+
:require_oauth_application_from_jwt_bearer_assertion_issuer,
|
11
|
+
:require_oauth_application_from_jwt_bearer_assertion_subject,
|
12
|
+
:account_from_jwt_bearer_assertion
|
13
|
+
)
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
|
18
|
+
claims = jwt_assertion(assertion)
|
19
|
+
|
20
|
+
return unless claims
|
21
|
+
|
22
|
+
db[oauth_applications_table].where(
|
23
|
+
oauth_applications_client_id_column => claims["iss"]
|
24
|
+
).first
|
25
|
+
end
|
26
|
+
|
27
|
+
def require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
|
28
|
+
claims = jwt_assertion(assertion)
|
29
|
+
|
30
|
+
return unless claims
|
31
|
+
|
32
|
+
db[oauth_applications_table].where(
|
33
|
+
oauth_applications_client_id_column => claims["sub"]
|
34
|
+
).first
|
35
|
+
end
|
36
|
+
|
37
|
+
def account_from_jwt_bearer_assertion(assertion)
|
38
|
+
claims = jwt_assertion(assertion)
|
39
|
+
|
40
|
+
return unless claims
|
41
|
+
|
42
|
+
account_from_bearer_assertion_subject(claims["sub"])
|
43
|
+
end
|
44
|
+
|
45
|
+
def jwt_assertion(assertion)
|
46
|
+
claims = jwt_decode(assertion, verify_iss: false, verify_aud: false)
|
47
|
+
return unless verify_aud(token_url, claims["aud"])
|
48
|
+
|
49
|
+
claims
|
50
|
+
end
|
51
|
+
|
52
|
+
def oauth_server_metadata_body(*)
|
53
|
+
super.tap do |data|
|
54
|
+
data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
55
|
+
data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth/refinements"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_pkce, :OauthPkce) do
|
7
|
+
using PrefixExtensions
|
8
|
+
|
9
|
+
depends :oauth_authorization_code_grant
|
10
|
+
|
11
|
+
auth_value_method :use_oauth_pkce?, true
|
12
|
+
|
13
|
+
auth_value_method :oauth_require_pkce, false
|
14
|
+
auth_value_method :oauth_pkce_challenge_method, "S256"
|
15
|
+
|
16
|
+
auth_value_method :oauth_grants_code_challenge_column, :code_challenge
|
17
|
+
auth_value_method :oauth_grants_code_challenge_method_column, :code_challenge_method
|
18
|
+
|
19
|
+
auth_value_method :code_challenge_required_error_code, "invalid_request"
|
20
|
+
translatable_method :code_challenge_required_message, "code challenge required"
|
21
|
+
auth_value_method :unsupported_transform_algorithm_error_code, "invalid_request"
|
22
|
+
translatable_method :unsupported_transform_algorithm_message, "transform algorithm not supported"
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def authorized_oauth_application?(oauth_application, client_secret)
|
27
|
+
return true if use_oauth_pkce? && param_or_nil("code_verifier")
|
28
|
+
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_oauth_grant_params
|
33
|
+
validate_pkce_challenge_params if use_oauth_pkce?
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_oauth_grant(create_params = {})
|
39
|
+
# PKCE flow
|
40
|
+
if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
|
41
|
+
code_challenge_method = param_or_nil("code_challenge_method")
|
42
|
+
|
43
|
+
create_params[oauth_grants_code_challenge_column] = code_challenge
|
44
|
+
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
45
|
+
end
|
46
|
+
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_oauth_token_from_authorization_code(oauth_grant, create_params)
|
51
|
+
if use_oauth_pkce?
|
52
|
+
if oauth_grant[oauth_grants_code_challenge_column]
|
53
|
+
code_verifier = param_or_nil("code_verifier")
|
54
|
+
|
55
|
+
redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
|
56
|
+
elsif oauth_require_pkce
|
57
|
+
redirect_response_error("code_challenge_required")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_pkce_challenge_params
|
65
|
+
if param_or_nil("code_challenge")
|
66
|
+
|
67
|
+
challenge_method = param_or_nil("code_challenge_method")
|
68
|
+
redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
|
69
|
+
else
|
70
|
+
return unless oauth_require_pkce
|
71
|
+
|
72
|
+
redirect_response_error("code_challenge_required")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def check_valid_grant_challenge?(grant, verifier)
|
77
|
+
challenge = grant[oauth_grants_code_challenge_column]
|
78
|
+
|
79
|
+
case grant[oauth_grants_code_challenge_method_column]
|
80
|
+
when "plain"
|
81
|
+
challenge == verifier
|
82
|
+
when "S256"
|
83
|
+
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
|
84
|
+
generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
|
85
|
+
|
86
|
+
challenge == generated_challenge
|
87
|
+
else
|
88
|
+
redirect_response_error("unsupported_transform_algorithm")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def oauth_server_metadata_body(*)
|
93
|
+
super.tap do |data|
|
94
|
+
data[:code_challenge_methods_supported] = oauth_pkce_challenge_method if use_oauth_pkce?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_resource_server, :OauthResourceServer) do
|
5
|
+
def authorization_token
|
6
|
+
return @authorization_token if defined?(@authorization_token)
|
7
|
+
|
8
|
+
# check if there is a token
|
9
|
+
bearer_token = fetch_access_token
|
10
|
+
|
11
|
+
return unless bearer_token
|
12
|
+
|
13
|
+
# where in resource server, NOT the authorization server.
|
14
|
+
payload = introspection_request("access_token", bearer_token)
|
15
|
+
|
16
|
+
return unless payload["active"]
|
17
|
+
|
18
|
+
@authorization_token = payload
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|