rodauth-oauth 0.7.4 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +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
|