rodauth-oauth 0.1.0 → 0.4.2
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 +108 -0
- data/README.md +2 -1
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +4 -4
- data/lib/rodauth/features/oauth.rb +461 -355
- data/lib/rodauth/features/oauth_http_mac.rb +6 -12
- data/lib/rodauth/features/oauth_jwt.rb +70 -52
- data/lib/rodauth/features/oauth_saml.rb +104 -0
- data/lib/rodauth/features/oidc.rb +217 -85
- data/lib/rodauth/oauth/database_extensions.rb +73 -0
- data/lib/rodauth/oauth/ttl_store.rb +1 -1
- data/lib/rodauth/oauth/version.rb +1 -1
- data/templates/authorize.str +34 -0
- data/templates/client_secret_field.str +4 -0
- data/templates/description_field.str +4 -0
- data/templates/homepage_url_field.str +4 -0
- data/templates/name_field.str +4 -0
- data/templates/new_oauth_application.str +10 -0
- data/templates/oauth_application.str +11 -0
- data/templates/oauth_applications.str +14 -0
- data/templates/oauth_tokens.str +49 -0
- data/templates/redirect_uri_field.str +4 -0
- data/templates/scope_field.str +10 -0
- metadata +20 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38ad6d8c4d03dac86f78e9c883f11426d805f77f82248098192ed4696a9f20e8
|
4
|
+
data.tar.gz: 07d4a10bae7e031033f0660347022e42139adb7be8834daaba307f1530838ce0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6331ba7a98a83f27d5ab3accb21dcf0676d43b21b76b98bf2349b688eeec5eaa1842e3c7e340cbc8a4109f40186ddabfc9d6daf679860b6438f1bb0520c6b383
|
7
|
+
data.tar.gz: 3b3b5f1c512a55fbcd6308cebf1e638bc9fe0a1e720f2acbde3b2cf52ca76431e668f74ae7070bb1f031c9e5b63fa307deab4b525c033caa00aa898d8505bb34
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,114 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
### 0.4.2
|
6
|
+
|
7
|
+
### Bugfixes
|
8
|
+
|
9
|
+
* database entensions were being run in resource server mode, when it's not expected that the oauth db tables are around.
|
10
|
+
|
11
|
+
### 0.4.1
|
12
|
+
|
13
|
+
### Improvements
|
14
|
+
|
15
|
+
When in "Resource Server" mode, calling `rodauth.authorization_token` will now return an hash of the JSON payload that the Authorization Server responds, and which was already previously used to authorize access to protected resources.
|
16
|
+
|
17
|
+
### Bugfixes
|
18
|
+
|
19
|
+
* An error ocurred if the client passed an empty authorization header (`Authorization: ` or `Authorization: Bearer `), causing an unexpected error; It now responds with the proper `401 Unauthorized` status code.
|
20
|
+
|
21
|
+
### 0.4.0
|
22
|
+
|
23
|
+
### Features
|
24
|
+
|
25
|
+
* A new method, `get_additional_param(account, claim)`, is now exposed; this method will be called whenever non-OIDC scopes are requested in the emission of the ID token.
|
26
|
+
|
27
|
+
* The `form_post` response is now supported, either by passing the `response_mode=form_post` request param in the authorization URL, or by setting `oauth_response_mode "form_post"` option. This improves the overall security of an Authorization server even more, as authorization codes are sent to client applications via a POST request to the redirect URI.
|
28
|
+
|
29
|
+
|
30
|
+
### Improvements
|
31
|
+
|
32
|
+
* For the OIDC `address` scope, proper claims are now emitted as per the standard, i.e. the "formatted", "street_address", "locality", "region", "postal_code", "country". These will be the ones referenced in the `get_oidc_param` method.
|
33
|
+
|
34
|
+
### Bugfixes
|
35
|
+
|
36
|
+
* The rails templates were missing declarations from a few params, which made some of the flows (the PKCE for example) not work out-of-the box;
|
37
|
+
* rails tests were silently not running in CI;
|
38
|
+
* The CI suite was revamped, so that all Oauth tests would be run under rails as well. All versions from rails equal or above 5.0 are now targeted;
|
39
|
+
|
40
|
+
### 0.3.0
|
41
|
+
|
42
|
+
#### Features
|
43
|
+
|
44
|
+
* `oauth_refresh_token_protection_policy` is a new option, which can be used to set a protection policy around usage of refresh tokens. By default it's `none`, for backwards-compatibility. However, when set to `rotation`, refresh tokens will be "use-once", i.e. a token refresh request will generate a new refresh token. Also, refresh token requests performed with already-used refresh tokens will be interpreted as a security breach, i.e. all tokens linked to the compromised refresh token will be revoked.
|
45
|
+
|
46
|
+
#### Improvements
|
47
|
+
|
48
|
+
|
49
|
+
* Support for the OIDC authorize [`prompt` parameter](https://openid.net/specs/openid-connect-core-1_0.html) (sectionn 3.1.2.1). It supports the `none`, `login` and `consent` out-of-the-box, while providing support for `select-account` when paired with [rodauth-select-account, a rodauth feature to handle multiple accounts in the same session](https://gitlab.com/honeyryderchuck/rodauth-select-account).
|
50
|
+
|
51
|
+
* Refresh Tokens are now expirable. The refresh token expiration period is governed by the `oauth_refresh_token_expires_in` option (default: 1 year), and is the period for which a refresh token can be used after its respective access token expired.
|
52
|
+
|
53
|
+
#### Bugfixes
|
54
|
+
|
55
|
+
* Default Templates now being packaged, as a way to provide a default experience to the OAuth journeys.
|
56
|
+
|
57
|
+
* fixing metadata urls when plugin loaded with a prefix path (@ianks)
|
58
|
+
|
59
|
+
* All date/time-based calculations, such as determining an expiration date, or checking if a token has expired, are now performed using database arithmetic operations, using sequel's `date_arithmetic` plugin. This will eliminate subtle bugs, such as when the database timezone is different than the application OS timezone.
|
60
|
+
|
61
|
+
* OIDC configuration endpoint is now stricter, eliminating JSON metadata inherited from the Oauth metadata endpoint. (@ianks)
|
62
|
+
|
63
|
+
#### Chore
|
64
|
+
|
65
|
+
Use `rodauth.convert_timestamp` in the templates, whenever dates are displayed.
|
66
|
+
|
67
|
+
Set HTTP Cache headers for metadata responses, such as `/.well-known/oauth-authorization-server` and `/.well-known/openid-configuration`, so they can be stored at the edge. The cache will be valid for 1 day (this value isn't set by an option yet).
|
68
|
+
|
69
|
+
### 0.2.0
|
70
|
+
|
71
|
+
#### Features
|
72
|
+
|
73
|
+
##### SAML Assertion Grant Type
|
74
|
+
|
75
|
+
`rodauth-auth` now supports using a SAML Assertion to request for an Access token.In order to enable, you have to:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
plugin :rodauth do
|
79
|
+
enable :oauth_saml
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
For more info about integrating it, [check the wiki](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/SAML-Assertion-Access-Tokens).
|
84
|
+
|
85
|
+
##### Supporting rotating keys
|
86
|
+
|
87
|
+
At some point, you'll want to replace the pkeys and algorithm used to generate and verify the JWT access tokens, but you want to keep validating previously-distributed JWT tokens, at least until they expire. Now you can, via two new options, `oauth_jwt_legacy_public_key` and `oauth_jwt_legacy_algorithm`, which will be declared in the JWKs URI and used to verify access tokens.
|
88
|
+
|
89
|
+
|
90
|
+
##### Reuse access tokens
|
91
|
+
|
92
|
+
If the `oauth_reuse_access_token` is set, if there's already an existing valid access token, any new grant for the same application / account / scope will keep the same access token. This can be helpful in scenarios where one wants the same access token distributed across devices.
|
93
|
+
|
94
|
+
##### require_authorizable_account
|
95
|
+
|
96
|
+
The method used to verify access to the authorize flow is called `require_authorizable_account`. By default, it checks if a user is logged in by using rodauth's own `require_account`. This is the method you'd want to redefine in order to augment these requirements, i.e. request 2fa authentication.
|
97
|
+
|
98
|
+
#### Improvements
|
99
|
+
|
100
|
+
Expired and revoked access tokens end up generating a lot of garbage, which will have to be periodically cleaned up. You can mitigate this now by setting a uniqueness index for a group of columns, i.e. if you set a uniqueness index for the `oauth_application_id/account_id/scopes` column, `rodauth-oauth` will transparently reuse the same db entry to store the new access token. If setting some other type of uniqueness index, make sure to update the option `oauth_tokens_unique_columns` (the array of columns from the uniqueness index).
|
101
|
+
|
102
|
+
#### Bugfixes
|
103
|
+
|
104
|
+
Calling `before_*_route` callbacks appropriately.
|
105
|
+
|
106
|
+
Fixed some mishandling of HTTP headers when in in resource-server mode.
|
107
|
+
|
108
|
+
#### Chore
|
109
|
+
|
110
|
+
* 97.7% test coverage;
|
111
|
+
* `rodauth-oauth` CI tests run against sqlite, postgresql and mysql.
|
112
|
+
|
5
113
|
### 0.1.0
|
6
114
|
|
7
115
|
(31/7/2020)
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Rodauth::Oauth
|
2
2
|
|
3
3
|
[](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/pipelines?page=1&ref=master)
|
4
|
-
[](https://honeyryderchuck.gitlab.io/rodauth-oauth/coverage/#_AllFiles)
|
4
|
+
[](https://honeyryderchuck.gitlab.io/rodauth-oauth/coverage/#_AllFiles)
|
5
5
|
|
6
6
|
This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framework](https://tools.ietf.org/html/rfc6749) for an authorization server.
|
7
7
|
|
@@ -21,6 +21,7 @@ This gem implements the following RFCs and features of OAuth:
|
|
21
21
|
* Access Type (Token refresh online and offline);
|
22
22
|
* [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
|
23
23
|
* [JWT Acess Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
|
24
|
+
* [SAML 2.0 Assertion Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-saml2-bearer-03);
|
24
25
|
* [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
|
25
26
|
* OAuth application and token management dashboards;
|
26
27
|
|
@@ -43,14 +43,14 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
|
|
43
43
|
t.foreign_key :oauth_tokens, column: :oauth_token_id
|
44
44
|
t.integer :oauth_application_id
|
45
45
|
t.foreign_key :oauth_applications, column: :oauth_application_id
|
46
|
-
t.string :token, null: false, token: true
|
46
|
+
t.string :token, null: false, token: true, unique: true
|
47
47
|
# uncomment if setting oauth_tokens_token_hash_column
|
48
48
|
# and delete the token column
|
49
|
-
# t.string :token_hash, token: true
|
50
|
-
t.string :refresh_token
|
49
|
+
# t.string :token_hash, token: true, unique: true
|
50
|
+
t.string :refresh_token, unique: true
|
51
51
|
# uncomment if setting oauth_tokens_refresh_token_hash_column
|
52
52
|
# and delete the refresh_token column
|
53
|
-
# t.string :refresh_token_hash, token: true
|
53
|
+
# t.string :refresh_token_hash, token: true, unique: true
|
54
54
|
t.datetime :expires_in, null: false
|
55
55
|
t.datetime :revoked_at
|
56
56
|
t.string :scopes, null: false
|
@@ -1,16 +1,21 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
+
require "time"
|
3
4
|
require "base64"
|
4
5
|
require "securerandom"
|
5
6
|
require "net/http"
|
6
7
|
|
7
8
|
require "rodauth/oauth/ttl_store"
|
9
|
+
require "rodauth/oauth/database_extensions"
|
8
10
|
|
9
11
|
module Rodauth
|
10
12
|
Feature.define(:oauth) do
|
11
13
|
# RUBY EXTENSIONS
|
12
|
-
# :nocov:
|
13
14
|
unless Regexp.method_defined?(:match?)
|
15
|
+
# If you wonder why this is there: the oauth feature uses a refinement to enhance the
|
16
|
+
# Regexp class locally with #match? , but this is never tested, because ActiveSupport
|
17
|
+
# monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
|
18
|
+
# :nocov:
|
14
19
|
module RegexpExtensions
|
15
20
|
refine(Regexp) do
|
16
21
|
def match?(*args)
|
@@ -19,6 +24,7 @@ module Rodauth
|
|
19
24
|
end
|
20
25
|
end
|
21
26
|
using(RegexpExtensions)
|
27
|
+
# :nocov:
|
22
28
|
end
|
23
29
|
|
24
30
|
unless String.method_defined?(:delete_suffix!)
|
@@ -37,10 +43,11 @@ module Rodauth
|
|
37
43
|
end
|
38
44
|
using(SuffixExtensions)
|
39
45
|
end
|
40
|
-
# :nocov:
|
41
46
|
|
42
47
|
SCOPES = %w[profile.read].freeze
|
43
48
|
|
49
|
+
SERVER_METADATA = OAuth::TtlStore.new
|
50
|
+
|
44
51
|
before "authorize"
|
45
52
|
after "authorize"
|
46
53
|
|
@@ -70,12 +77,14 @@ module Rodauth
|
|
70
77
|
|
71
78
|
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
|
72
79
|
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
|
80
|
+
auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
|
73
81
|
auth_value_method :use_oauth_implicit_grant_type?, false
|
74
82
|
auth_value_method :use_oauth_pkce?, true
|
75
83
|
auth_value_method :use_oauth_access_type?, true
|
76
84
|
|
77
85
|
auth_value_method :oauth_require_pkce, false
|
78
86
|
auth_value_method :oauth_pkce_challenge_method, "S256"
|
87
|
+
auth_value_method :oauth_response_mode, "query"
|
79
88
|
|
80
89
|
auth_value_method :oauth_valid_uri_schemes, %w[https]
|
81
90
|
|
@@ -92,6 +101,7 @@ module Rodauth
|
|
92
101
|
button "Register", "oauth_application"
|
93
102
|
button "Authorize", "oauth_authorize"
|
94
103
|
button "Revoke", "oauth_token_revoke"
|
104
|
+
button "Back to Client Application", "oauth_authorize_post"
|
95
105
|
|
96
106
|
# OAuth Token
|
97
107
|
auth_value_method :oauth_tokens_path, "oauth-tokens"
|
@@ -110,6 +120,8 @@ module Rodauth
|
|
110
120
|
auth_value_method :oauth_tokens_token_hash_column, nil
|
111
121
|
auth_value_method :oauth_tokens_refresh_token_hash_column, nil
|
112
122
|
|
123
|
+
# Access Token reuse
|
124
|
+
auth_value_method :oauth_reuse_access_token, false
|
113
125
|
# OAuth Grants
|
114
126
|
auth_value_method :oauth_grants_table, :oauth_grants
|
115
127
|
auth_value_method :oauth_grants_id_column, :id
|
@@ -124,6 +136,7 @@ module Rodauth
|
|
124
136
|
|
125
137
|
auth_value_method :authorization_required_error_status, 401
|
126
138
|
auth_value_method :invalid_oauth_response_status, 400
|
139
|
+
auth_value_method :already_in_use_response_status, 409
|
127
140
|
|
128
141
|
# OAuth Applications
|
129
142
|
auth_value_method :oauth_applications_path, "oauth-applications"
|
@@ -141,9 +154,11 @@ module Rodauth
|
|
141
154
|
auth_value_method :"oauth_applications_#{column}_column", column
|
142
155
|
end
|
143
156
|
|
157
|
+
# Feature options
|
144
158
|
auth_value_method :oauth_application_default_scope, SCOPES.first
|
145
159
|
auth_value_method :oauth_application_scopes, SCOPES
|
146
160
|
auth_value_method :oauth_token_type, "bearer"
|
161
|
+
auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
|
147
162
|
|
148
163
|
auth_value_method :invalid_client_message, "Invalid client"
|
149
164
|
auth_value_method :invalid_grant_type_message, "Invalid grant type"
|
@@ -155,6 +170,8 @@ module Rodauth
|
|
155
170
|
|
156
171
|
auth_value_method :unique_error_message, "is already in use"
|
157
172
|
auth_value_method :null_error_message, "is not filled"
|
173
|
+
auth_value_method :already_in_use_message, "error generating unique token"
|
174
|
+
auth_value_method :already_in_use_error_code, "invalid_request"
|
158
175
|
|
159
176
|
# PKCE
|
160
177
|
auth_value_method :code_challenge_required_error_code, "invalid_request"
|
@@ -172,6 +189,8 @@ module Rodauth
|
|
172
189
|
# Only required to use if the plugin is to be used in a resource server
|
173
190
|
auth_value_method :is_authorization_server?, true
|
174
191
|
|
192
|
+
auth_value_method :oauth_unique_id_generation_retries, 3
|
193
|
+
|
175
194
|
auth_value_methods(
|
176
195
|
:fetch_access_token,
|
177
196
|
:oauth_unique_id_generator,
|
@@ -179,14 +198,223 @@ module Rodauth
|
|
179
198
|
:secret_hash,
|
180
199
|
:generate_token_hash,
|
181
200
|
:authorization_server_url,
|
182
|
-
:before_introspection_request
|
201
|
+
:before_introspection_request,
|
202
|
+
:require_authorizable_account,
|
203
|
+
:oauth_tokens_unique_columns
|
183
204
|
)
|
184
205
|
|
185
206
|
auth_value_methods(:only_json?)
|
186
207
|
|
187
208
|
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
188
209
|
|
189
|
-
|
210
|
+
# /token
|
211
|
+
route(:token) do |r|
|
212
|
+
next unless is_authorization_server?
|
213
|
+
|
214
|
+
before_token_route
|
215
|
+
require_oauth_application
|
216
|
+
|
217
|
+
r.post do
|
218
|
+
catch_error do
|
219
|
+
validate_oauth_token_params
|
220
|
+
|
221
|
+
oauth_token = nil
|
222
|
+
transaction do
|
223
|
+
before_token
|
224
|
+
oauth_token = create_oauth_token
|
225
|
+
end
|
226
|
+
|
227
|
+
json_response_success(json_access_token_payload(oauth_token))
|
228
|
+
end
|
229
|
+
|
230
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# /introspect
|
235
|
+
route(:introspect) do |r|
|
236
|
+
next unless is_authorization_server?
|
237
|
+
|
238
|
+
before_introspect_route
|
239
|
+
|
240
|
+
r.post do
|
241
|
+
catch_error do
|
242
|
+
validate_oauth_introspect_params
|
243
|
+
|
244
|
+
before_introspect
|
245
|
+
oauth_token = case param("token_type_hint")
|
246
|
+
when "access_token"
|
247
|
+
oauth_token_by_token(param("token"))
|
248
|
+
when "refresh_token"
|
249
|
+
oauth_token_by_refresh_token(param("token"))
|
250
|
+
else
|
251
|
+
oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
|
252
|
+
end
|
253
|
+
|
254
|
+
if oauth_application
|
255
|
+
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
256
|
+
elsif oauth_token
|
257
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
258
|
+
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
259
|
+
end
|
260
|
+
|
261
|
+
json_response_success(json_token_introspect_payload(oauth_token))
|
262
|
+
end
|
263
|
+
|
264
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# /revoke
|
269
|
+
route(:revoke) do |r|
|
270
|
+
next unless is_authorization_server?
|
271
|
+
|
272
|
+
before_revoke_route
|
273
|
+
require_oauth_application
|
274
|
+
|
275
|
+
r.post do
|
276
|
+
catch_error do
|
277
|
+
validate_oauth_revoke_params
|
278
|
+
|
279
|
+
oauth_token = nil
|
280
|
+
transaction do
|
281
|
+
before_revoke
|
282
|
+
oauth_token = revoke_oauth_token
|
283
|
+
after_revoke
|
284
|
+
end
|
285
|
+
|
286
|
+
if accepts_json?
|
287
|
+
json_response_success \
|
288
|
+
"token" => oauth_token[oauth_tokens_token_column],
|
289
|
+
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
290
|
+
"revoked_at" => convert_timestamp(oauth_token[oauth_tokens_revoked_at_column])
|
291
|
+
else
|
292
|
+
set_notice_flash revoke_oauth_token_notice_flash
|
293
|
+
redirect request.referer || "/"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
redirect_response_error("invalid_request", request.referer || "/")
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# /authorize
|
302
|
+
route(:authorize) do |r|
|
303
|
+
next unless is_authorization_server?
|
304
|
+
|
305
|
+
before_authorize_route
|
306
|
+
require_authorizable_account
|
307
|
+
|
308
|
+
validate_oauth_grant_params
|
309
|
+
try_approval_prompt if use_oauth_access_type? && request.get?
|
310
|
+
|
311
|
+
r.get do
|
312
|
+
authorize_view
|
313
|
+
end
|
314
|
+
|
315
|
+
r.post do
|
316
|
+
redirect_url = URI.parse(redirect_uri)
|
317
|
+
|
318
|
+
params, mode = transaction do
|
319
|
+
before_authorize
|
320
|
+
do_authorize
|
321
|
+
end
|
322
|
+
|
323
|
+
case mode
|
324
|
+
when "query"
|
325
|
+
params = params.map { |k, v| "#{k}=#{v}" }
|
326
|
+
params << redirect_url.query if redirect_url.query
|
327
|
+
redirect_url.query = params.join("&")
|
328
|
+
redirect(redirect_url.to_s)
|
329
|
+
when "fragment"
|
330
|
+
params = params.map { |k, v| "#{k}=#{v}" }
|
331
|
+
params << redirect_url.query if redirect_url.query
|
332
|
+
redirect_url.fragment = params.join("&")
|
333
|
+
redirect(redirect_url.to_s)
|
334
|
+
when "form_post"
|
335
|
+
scope.view layout: false, inline: <<-FORM
|
336
|
+
<html>
|
337
|
+
<head><title>Authorized</title></head>
|
338
|
+
<body onload="javascript:document.forms[0].submit()">
|
339
|
+
<form method="post" action="#{redirect_uri}">
|
340
|
+
#{
|
341
|
+
params.map do |name, value|
|
342
|
+
"<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
|
343
|
+
end.join
|
344
|
+
}
|
345
|
+
<input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
|
346
|
+
</form>
|
347
|
+
</body>
|
348
|
+
</html>
|
349
|
+
FORM
|
350
|
+
when "none"
|
351
|
+
redirect(redirect_url.to_s)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def oauth_server_metadata(issuer = nil)
|
357
|
+
request.on(".well-known") do
|
358
|
+
request.on("oauth-authorization-server") do
|
359
|
+
request.get do
|
360
|
+
json_response_success(oauth_server_metadata_body(issuer), true)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
# /oauth-applications routes
|
367
|
+
def oauth_applications
|
368
|
+
request.on(oauth_applications_path) do
|
369
|
+
require_account
|
370
|
+
|
371
|
+
request.get "new" do
|
372
|
+
new_oauth_application_view
|
373
|
+
end
|
374
|
+
|
375
|
+
request.on(oauth_applications_id_pattern) do |id|
|
376
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
|
377
|
+
next unless oauth_application
|
378
|
+
|
379
|
+
scope.instance_variable_set(:@oauth_application, oauth_application)
|
380
|
+
|
381
|
+
request.is do
|
382
|
+
request.get do
|
383
|
+
oauth_application_view
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
request.on(oauth_tokens_path) do
|
388
|
+
oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
|
389
|
+
scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
|
390
|
+
request.get do
|
391
|
+
oauth_tokens_view
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
request.get do
|
397
|
+
scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table])
|
398
|
+
oauth_applications_view
|
399
|
+
end
|
400
|
+
|
401
|
+
request.post do
|
402
|
+
catch_error do
|
403
|
+
validate_oauth_application_params
|
404
|
+
|
405
|
+
transaction do
|
406
|
+
before_create_oauth_application
|
407
|
+
id = create_oauth_application
|
408
|
+
after_create_oauth_application
|
409
|
+
set_notice_flash create_oauth_application_notice_flash
|
410
|
+
redirect "#{request.path}/#{id}"
|
411
|
+
end
|
412
|
+
end
|
413
|
+
set_error_flash create_oauth_application_error_flash
|
414
|
+
new_oauth_application_view
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
190
418
|
|
191
419
|
def check_csrf?
|
192
420
|
case request.path
|
@@ -213,14 +441,12 @@ module Rodauth
|
|
213
441
|
end
|
214
442
|
|
215
443
|
unless method_defined?(:json_request?)
|
216
|
-
# :nocov:
|
217
444
|
# copied from the jwt feature
|
218
445
|
def json_request?
|
219
446
|
return @json_request if defined?(@json_request)
|
220
447
|
|
221
448
|
@json_request = request.content_type =~ json_request_regexp
|
222
449
|
end
|
223
|
-
# :nocov:
|
224
450
|
end
|
225
451
|
|
226
452
|
def initialize(scope)
|
@@ -263,13 +489,13 @@ module Rodauth
|
|
263
489
|
def fetch_access_token
|
264
490
|
value = request.env["HTTP_AUTHORIZATION"]
|
265
491
|
|
266
|
-
return unless value
|
492
|
+
return unless value && !value.empty?
|
267
493
|
|
268
494
|
scheme, token = value.split(" ", 2)
|
269
495
|
|
270
496
|
return unless scheme.downcase == oauth_token_type
|
271
497
|
|
272
|
-
return if token.empty?
|
498
|
+
return if token.nil? || token.empty?
|
273
499
|
|
274
500
|
token
|
275
501
|
end
|
@@ -282,101 +508,84 @@ module Rodauth
|
|
282
508
|
|
283
509
|
return unless bearer_token
|
284
510
|
|
285
|
-
|
286
|
-
|
287
|
-
|
511
|
+
@authorization_token = if is_authorization_server?
|
512
|
+
# check if token has not expired
|
513
|
+
# check if token has been revoked
|
514
|
+
oauth_token_by_token(bearer_token)
|
515
|
+
else
|
516
|
+
# where in resource server, NOT the authorization server.
|
517
|
+
payload = introspection_request("access_token", bearer_token)
|
518
|
+
|
519
|
+
return unless payload["active"]
|
520
|
+
|
521
|
+
payload
|
522
|
+
end
|
288
523
|
end
|
289
524
|
|
290
525
|
def require_oauth_authorization(*scopes)
|
291
|
-
|
292
|
-
authorization_required unless authorization_token
|
526
|
+
authorization_required unless authorization_token
|
293
527
|
|
294
|
-
|
528
|
+
scopes << oauth_application_default_scope if scopes.empty?
|
295
529
|
|
530
|
+
token_scopes = if is_authorization_server?
|
296
531
|
authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
|
297
532
|
else
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
# where in resource server, NOT the authorization server.
|
305
|
-
payload = introspection_request("access_token", bearer_token)
|
306
|
-
|
307
|
-
authorization_required unless payload["active"]
|
308
|
-
|
309
|
-
payload["scope"].split(oauth_scope_separator)
|
533
|
+
aux_scopes = authorization_token["scope"]
|
534
|
+
if aux_scopes
|
535
|
+
aux_scopes.split(oauth_scope_separator)
|
536
|
+
else
|
537
|
+
[]
|
538
|
+
end
|
310
539
|
end
|
311
540
|
|
312
541
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
313
542
|
end
|
314
543
|
|
315
|
-
|
316
|
-
|
317
|
-
request.on(oauth_applications_path) do
|
318
|
-
require_account
|
319
|
-
|
320
|
-
request.get "new" do
|
321
|
-
new_oauth_application_view
|
322
|
-
end
|
544
|
+
def post_configure
|
545
|
+
super
|
323
546
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
scope.instance_variable_set(:@oauth_application, oauth_application)
|
547
|
+
# all of the extensions below involve DB changes. Resource server mode doesn't use
|
548
|
+
# database functions for OAuth though.
|
549
|
+
return unless is_authorization_server?
|
329
550
|
|
330
|
-
|
331
|
-
request.get do
|
332
|
-
oauth_application_view
|
333
|
-
end
|
334
|
-
end
|
551
|
+
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
|
335
552
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
end
|
342
|
-
end
|
553
|
+
# Check whether we can reutilize db entries for the same account / application pair
|
554
|
+
one_oauth_token_per_account = begin
|
555
|
+
db.indexes(oauth_tokens_table).values.any? do |definition|
|
556
|
+
definition[:unique] &&
|
557
|
+
definition[:columns] == oauth_tokens_unique_columns
|
343
558
|
end
|
559
|
+
end
|
560
|
+
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
|
561
|
+
end
|
344
562
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
end
|
563
|
+
def use_date_arithmetic?
|
564
|
+
true
|
565
|
+
end
|
349
566
|
|
350
|
-
|
351
|
-
catch_error do
|
352
|
-
validate_oauth_application_params
|
567
|
+
private
|
353
568
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
set_error_flash create_oauth_application_error_flash
|
363
|
-
new_oauth_application_view
|
364
|
-
end
|
569
|
+
def rescue_from_uniqueness_error(&block)
|
570
|
+
retries = oauth_unique_id_generation_retries
|
571
|
+
begin
|
572
|
+
transaction(savepoint: :only, &block)
|
573
|
+
rescue Sequel::UniqueConstraintViolation
|
574
|
+
redirect_response_error("already_in_use") if retries.zero?
|
575
|
+
retries -= 1
|
576
|
+
retry
|
365
577
|
end
|
366
578
|
end
|
367
579
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
end
|
580
|
+
# OAuth Token Unique/Reuse
|
581
|
+
def oauth_tokens_unique_columns
|
582
|
+
[
|
583
|
+
oauth_tokens_oauth_application_id_column,
|
584
|
+
oauth_tokens_account_id_column,
|
585
|
+
oauth_tokens_scopes_column
|
586
|
+
]
|
376
587
|
end
|
377
588
|
|
378
|
-
private
|
379
|
-
|
380
589
|
def authorization_server_url
|
381
590
|
base_url
|
382
591
|
end
|
@@ -399,10 +608,10 @@ module Rodauth
|
|
399
608
|
|
400
609
|
# time-to-live
|
401
610
|
ttl = if response.key?("cache-control")
|
402
|
-
cache_control = response["
|
403
|
-
cache_control[/max-age=(\d+)/, 1]
|
611
|
+
cache_control = response["cache-control"]
|
612
|
+
cache_control[/max-age=(\d+)/, 1].to_i
|
404
613
|
elsif response.key?("expires")
|
405
|
-
Time.
|
614
|
+
Time.parse(response["expires"]).to_i - Time.now.to_i
|
406
615
|
end
|
407
616
|
|
408
617
|
[JSON.parse(response.body, symbolize_names: true), ttl]
|
@@ -470,7 +679,7 @@ module Rodauth
|
|
470
679
|
end
|
471
680
|
|
472
681
|
def oauth_unique_id_generator
|
473
|
-
SecureRandom.
|
682
|
+
SecureRandom.urlsafe_base64(32)
|
474
683
|
end
|
475
684
|
|
476
685
|
def generate_token_hash(token)
|
@@ -482,89 +691,106 @@ module Rodauth
|
|
482
691
|
end
|
483
692
|
|
484
693
|
unless method_defined?(:password_hash)
|
485
|
-
# :nocov:
|
486
694
|
# From login_requirements_base feature
|
487
|
-
if ENV["RACK_ENV"] == "test"
|
488
|
-
def password_hash_cost
|
489
|
-
BCrypt::Engine::MIN_COST
|
490
|
-
end
|
491
|
-
else
|
492
|
-
def password_hash_cost
|
493
|
-
BCrypt::Engine::DEFAULT_COST
|
494
|
-
end
|
495
|
-
end
|
496
695
|
|
497
696
|
def password_hash(password)
|
498
|
-
BCrypt::Password.create(password, cost:
|
697
|
+
BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
|
499
698
|
end
|
500
|
-
# :nocov:
|
501
699
|
end
|
502
700
|
|
503
701
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
504
702
|
create_params = {
|
505
|
-
oauth_grants_expires_in_column =>
|
703
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
506
704
|
}.merge(params)
|
507
705
|
|
508
|
-
|
509
|
-
|
706
|
+
rescue_from_uniqueness_error do
|
707
|
+
token = oauth_unique_id_generator
|
510
708
|
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
709
|
+
if oauth_tokens_token_hash_column
|
710
|
+
create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
711
|
+
else
|
712
|
+
create_params[oauth_tokens_token_column] = token
|
713
|
+
end
|
516
714
|
|
517
|
-
|
518
|
-
|
715
|
+
refresh_token = nil
|
716
|
+
if should_generate_refresh_token
|
717
|
+
refresh_token = oauth_unique_id_generator
|
519
718
|
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
719
|
+
if oauth_tokens_refresh_token_hash_column
|
720
|
+
create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
721
|
+
else
|
722
|
+
create_params[oauth_tokens_refresh_token_column] = refresh_token
|
723
|
+
end
|
524
724
|
end
|
725
|
+
oauth_token = _generate_oauth_token(create_params)
|
726
|
+
oauth_token[oauth_tokens_token_column] = token
|
727
|
+
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
728
|
+
oauth_token
|
525
729
|
end
|
526
|
-
oauth_token = _generate_oauth_token(create_params)
|
527
|
-
|
528
|
-
oauth_token[oauth_tokens_token_column] = token
|
529
|
-
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
530
|
-
oauth_token
|
531
730
|
end
|
532
731
|
|
533
732
|
def _generate_oauth_token(params = {})
|
534
733
|
ds = db[oauth_tokens_table]
|
535
734
|
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
735
|
+
if __one_oauth_token_per_account
|
736
|
+
|
737
|
+
token = __insert_or_update_and_return__(
|
738
|
+
ds,
|
739
|
+
oauth_tokens_id_column,
|
740
|
+
oauth_tokens_unique_columns,
|
741
|
+
params,
|
742
|
+
Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
|
743
|
+
([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
|
744
|
+
)
|
745
|
+
|
746
|
+
# if the previous operation didn't return a row, it means that the conditions
|
747
|
+
# invalidated the update, and the existing token is still valid.
|
748
|
+
token || ds.where(
|
749
|
+
oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
|
750
|
+
oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
|
751
|
+
).first
|
752
|
+
else
|
753
|
+
if oauth_reuse_access_token
|
754
|
+
unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
|
755
|
+
valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
|
756
|
+
.where(unique_conds).first
|
757
|
+
return valid_token if valid_token
|
542
758
|
end
|
543
|
-
|
544
|
-
retry
|
759
|
+
__insert_and_return__(ds, oauth_tokens_id_column, params)
|
545
760
|
end
|
546
761
|
end
|
547
762
|
|
548
|
-
def oauth_token_by_token(token
|
763
|
+
def oauth_token_by_token(token)
|
764
|
+
ds = db[oauth_tokens_table]
|
765
|
+
|
549
766
|
ds = if oauth_tokens_token_hash_column
|
550
|
-
|
767
|
+
ds.where(oauth_tokens_token_hash_column => generate_token_hash(token))
|
551
768
|
else
|
552
|
-
|
769
|
+
ds.where(oauth_tokens_token_column => token)
|
553
770
|
end
|
554
771
|
|
555
772
|
ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
556
773
|
.where(oauth_tokens_revoked_at_column => nil).first
|
557
774
|
end
|
558
775
|
|
559
|
-
def oauth_token_by_refresh_token(token,
|
776
|
+
def oauth_token_by_refresh_token(token, revoked: false)
|
777
|
+
ds = db[oauth_tokens_table]
|
778
|
+
#
|
779
|
+
# filter expired refresh tokens out.
|
780
|
+
# an expired refresh token is a token whose access token expired for a period longer than the
|
781
|
+
# refresh token expiration period.
|
782
|
+
#
|
783
|
+
ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
|
784
|
+
|
560
785
|
ds = if oauth_tokens_refresh_token_hash_column
|
561
|
-
|
786
|
+
ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
|
562
787
|
else
|
563
|
-
|
788
|
+
ds.where(oauth_tokens_refresh_token_column => token)
|
564
789
|
end
|
565
790
|
|
566
|
-
ds.where(
|
567
|
-
|
791
|
+
ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
|
792
|
+
|
793
|
+
ds.first
|
568
794
|
end
|
569
795
|
|
570
796
|
def json_access_token_payload(oauth_token)
|
@@ -633,7 +859,6 @@ module Rodauth
|
|
633
859
|
# set client ID/secret pairs
|
634
860
|
|
635
861
|
create_params.merge! \
|
636
|
-
oauth_applications_client_id_column => oauth_unique_id_generator,
|
637
862
|
oauth_applications_client_secret_column => \
|
638
863
|
secret_hash(oauth_application_params[oauth_application_client_secret_param])
|
639
864
|
|
@@ -643,29 +868,14 @@ module Rodauth
|
|
643
868
|
oauth_application_default_scope
|
644
869
|
end
|
645
870
|
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
false
|
650
|
-
rescue Sequel::ConstraintViolation => e
|
651
|
-
e
|
652
|
-
end
|
653
|
-
|
654
|
-
if raised
|
655
|
-
field = raised.message[/\.(.*)$/, 1]
|
656
|
-
case raised
|
657
|
-
when Sequel::UniqueConstraintViolation
|
658
|
-
throw_error(field, unique_error_message)
|
659
|
-
when Sequel::NotNullConstraintViolation
|
660
|
-
throw_error(field, null_error_message)
|
661
|
-
end
|
871
|
+
rescue_from_uniqueness_error do
|
872
|
+
create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
|
873
|
+
db[oauth_applications_table].insert(create_params)
|
662
874
|
end
|
663
|
-
|
664
|
-
!raised && id
|
665
875
|
end
|
666
876
|
|
667
877
|
# Authorize
|
668
|
-
def
|
878
|
+
def require_authorizable_account
|
669
879
|
require_account
|
670
880
|
end
|
671
881
|
|
@@ -678,6 +888,9 @@ module Rodauth
|
|
678
888
|
end
|
679
889
|
redirect_response_error("invalid_scope") unless check_valid_scopes?
|
680
890
|
|
891
|
+
if (response_mode = param_or_nil("response_mode")) && response_mode != "form_post"
|
892
|
+
redirect_response_error("invalid_request")
|
893
|
+
end
|
681
894
|
validate_pkce_challenge_params if use_oauth_pkce?
|
682
895
|
end
|
683
896
|
|
@@ -695,7 +908,6 @@ module Rodauth
|
|
695
908
|
).count.zero?
|
696
909
|
|
697
910
|
# if there's a previous oauth grant for the params combo, it means that this user has approved before.
|
698
|
-
|
699
911
|
request.env["REQUEST_METHOD"] = "POST"
|
700
912
|
end
|
701
913
|
|
@@ -704,64 +916,52 @@ module Rodauth
|
|
704
916
|
oauth_grants_account_id_column => account_id,
|
705
917
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
706
918
|
oauth_grants_redirect_uri_column => redirect_uri,
|
707
|
-
oauth_grants_expires_in_column =>
|
919
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
|
708
920
|
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
709
921
|
)
|
710
922
|
|
711
923
|
# Access Type flow
|
712
|
-
if use_oauth_access_type?
|
713
|
-
|
714
|
-
create_params[oauth_grants_access_type_column] = access_type
|
715
|
-
end
|
924
|
+
if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
|
925
|
+
create_params[oauth_grants_access_type_column] = access_type
|
716
926
|
end
|
717
927
|
|
718
928
|
# PKCE flow
|
719
|
-
if use_oauth_pkce?
|
929
|
+
if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
|
930
|
+
code_challenge_method = param_or_nil("code_challenge_method")
|
720
931
|
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
create_params[oauth_grants_code_challenge_column] = code_challenge
|
725
|
-
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
726
|
-
elsif oauth_require_pkce
|
727
|
-
redirect_response_error("code_challenge_required")
|
728
|
-
end
|
932
|
+
create_params[oauth_grants_code_challenge_column] = code_challenge
|
933
|
+
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
729
934
|
end
|
730
935
|
|
731
936
|
ds = db[oauth_grants_table]
|
732
937
|
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
ds.insert(create_params)
|
737
|
-
authorization_code
|
738
|
-
rescue Sequel::UniqueConstraintViolation
|
739
|
-
retry
|
938
|
+
rescue_from_uniqueness_error do
|
939
|
+
create_params[oauth_grants_code_column] = oauth_unique_id_generator
|
940
|
+
__insert_and_return__(ds, oauth_grants_id_column, create_params)
|
740
941
|
end
|
942
|
+
create_params[oauth_grants_code_column]
|
741
943
|
end
|
742
944
|
|
743
|
-
def do_authorize(
|
945
|
+
def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
|
744
946
|
case param("response_type")
|
745
947
|
when "token"
|
746
948
|
redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
|
747
949
|
|
748
|
-
|
749
|
-
|
750
|
-
|
950
|
+
response_mode ||= "fragment"
|
951
|
+
response_params.replace(_do_authorize_token)
|
952
|
+
when "code"
|
953
|
+
response_mode ||= "query"
|
954
|
+
response_params.replace(_do_authorize_code)
|
955
|
+
when "none"
|
956
|
+
response_mode ||= "none"
|
957
|
+
when "", nil
|
958
|
+
response_mode ||= oauth_response_mode
|
959
|
+
response_params.replace(_do_authorize_code)
|
751
960
|
end
|
752
961
|
|
753
|
-
if param_or_nil("state")
|
754
|
-
if !fragment_params.empty?
|
755
|
-
fragment_params << "state=#{param('state')}"
|
756
|
-
else
|
757
|
-
query_params << "state=#{param('state')}"
|
758
|
-
end
|
759
|
-
end
|
962
|
+
response_params["state"] = param("state") if param_or_nil("state")
|
760
963
|
|
761
|
-
|
762
|
-
|
763
|
-
redirect_url.query = query_params.join("&") unless query_params.empty?
|
764
|
-
redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
|
964
|
+
[response_params, response_mode]
|
765
965
|
end
|
766
966
|
|
767
967
|
def _do_authorize_code
|
@@ -781,10 +981,6 @@ module Rodauth
|
|
781
981
|
|
782
982
|
# Access Tokens
|
783
983
|
|
784
|
-
def before_token
|
785
|
-
require_oauth_application
|
786
|
-
end
|
787
|
-
|
788
984
|
def validate_oauth_token_params
|
789
985
|
unless (grant_type = param_or_nil("grant_type"))
|
790
986
|
redirect_response_error("invalid_request")
|
@@ -824,18 +1020,32 @@ module Rodauth
|
|
824
1020
|
}
|
825
1021
|
create_oauth_token_from_authorization_code(oauth_grant, create_params)
|
826
1022
|
when "refresh_token"
|
827
|
-
# fetch oauth token
|
828
|
-
oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
|
829
|
-
|
830
|
-
|
1023
|
+
# fetch potentially revoked oauth token
|
1024
|
+
oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
|
1025
|
+
|
1026
|
+
if !oauth_token
|
1027
|
+
redirect_response_error("invalid_grant")
|
1028
|
+
elsif oauth_token[oauth_tokens_revoked_at_column]
|
1029
|
+
if oauth_refresh_token_protection_policy == "rotation"
|
1030
|
+
# https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
|
1031
|
+
#
|
1032
|
+
# If a refresh token is compromised and subsequently used by both the attacker and the legitimate
|
1033
|
+
# client, one of them will present an invalidated refresh token, which will inform the authorization
|
1034
|
+
# server of the breach. The authorization server cannot determine which party submitted the invalid
|
1035
|
+
# refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
|
1036
|
+
# forcing the legitimate client to obtain a fresh authorization grant.
|
1037
|
+
|
1038
|
+
db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
|
1039
|
+
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
1040
|
+
end
|
1041
|
+
redirect_response_error("invalid_grant")
|
1042
|
+
end
|
831
1043
|
|
832
1044
|
update_params = {
|
833
1045
|
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
834
|
-
oauth_tokens_expires_in_column =>
|
1046
|
+
oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
835
1047
|
}
|
836
1048
|
create_oauth_token_from_token(oauth_token, update_params)
|
837
|
-
else
|
838
|
-
redirect_response_error("invalid_grant")
|
839
1049
|
end
|
840
1050
|
end
|
841
1051
|
|
@@ -864,29 +1074,38 @@ module Rodauth
|
|
864
1074
|
def create_oauth_token_from_token(oauth_token, update_params)
|
865
1075
|
redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
|
866
1076
|
|
867
|
-
|
1077
|
+
rescue_from_uniqueness_error do
|
1078
|
+
oauth_tokens_ds = db[oauth_tokens_table]
|
1079
|
+
token = oauth_unique_id_generator
|
868
1080
|
|
869
|
-
|
870
|
-
|
871
|
-
else
|
872
|
-
update_params[oauth_tokens_token_column] = token
|
873
|
-
end
|
874
|
-
|
875
|
-
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
876
|
-
|
877
|
-
oauth_token = begin
|
878
|
-
if ds.supports_returning?(:update)
|
879
|
-
ds.returning.update(update_params).first
|
1081
|
+
if oauth_tokens_token_hash_column
|
1082
|
+
update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
880
1083
|
else
|
881
|
-
|
882
|
-
ds.first
|
1084
|
+
update_params[oauth_tokens_token_column] = token
|
883
1085
|
end
|
884
|
-
rescue Sequel::UniqueConstraintViolation
|
885
|
-
retry
|
886
|
-
end
|
887
1086
|
|
888
|
-
|
889
|
-
|
1087
|
+
oauth_token = if oauth_refresh_token_protection_policy == "rotation"
|
1088
|
+
insert_params = {
|
1089
|
+
**update_params,
|
1090
|
+
oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
|
1091
|
+
oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
|
1092
|
+
}
|
1093
|
+
|
1094
|
+
# revoke the refresh token
|
1095
|
+
oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
1096
|
+
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
1097
|
+
|
1098
|
+
insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
|
1099
|
+
__insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
|
1100
|
+
else
|
1101
|
+
# includes none
|
1102
|
+
ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
1103
|
+
__update_and_return__(ds, update_params)
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
oauth_token[oauth_tokens_token_column] = token
|
1107
|
+
oauth_token
|
1108
|
+
end
|
890
1109
|
end
|
891
1110
|
|
892
1111
|
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
|
@@ -895,8 +1114,8 @@ module Rodauth
|
|
895
1114
|
|
896
1115
|
def validate_oauth_introspect_params
|
897
1116
|
# check if valid token hint type
|
898
|
-
if param_or_nil("token_type_hint")
|
899
|
-
redirect_response_error("unsupported_token_type")
|
1117
|
+
if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
1118
|
+
redirect_response_error("unsupported_token_type")
|
900
1119
|
end
|
901
1120
|
|
902
1121
|
redirect_response_error("invalid_request") unless param_or_nil("token")
|
@@ -914,18 +1133,12 @@ module Rodauth
|
|
914
1133
|
}
|
915
1134
|
end
|
916
1135
|
|
917
|
-
def before_introspect; end
|
918
|
-
|
919
1136
|
# Token revocation
|
920
1137
|
|
921
|
-
def before_revoke
|
922
|
-
require_oauth_application
|
923
|
-
end
|
924
|
-
|
925
1138
|
def validate_oauth_revoke_params
|
926
1139
|
# check if valid token hint type
|
927
|
-
if param_or_nil("token_type_hint")
|
928
|
-
redirect_response_error("unsupported_token_type")
|
1140
|
+
if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
1141
|
+
redirect_response_error("unsupported_token_type")
|
929
1142
|
end
|
930
1143
|
|
931
1144
|
redirect_response_error("invalid_request") unless param_or_nil("token")
|
@@ -942,23 +1155,13 @@ module Rodauth
|
|
942
1155
|
|
943
1156
|
redirect_response_error("invalid_request") unless oauth_token
|
944
1157
|
|
945
|
-
|
946
|
-
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
947
|
-
else
|
948
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
949
|
-
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
950
|
-
end
|
1158
|
+
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
951
1159
|
|
952
1160
|
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
953
1161
|
|
954
1162
|
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
955
1163
|
|
956
|
-
oauth_token =
|
957
|
-
ds.returning.update(update_params).first
|
958
|
-
else
|
959
|
-
ds.update(update_params)
|
960
|
-
ds.first
|
961
|
-
end
|
1164
|
+
oauth_token = __update_and_return__(ds, update_params)
|
962
1165
|
|
963
1166
|
oauth_token[oauth_tokens_token_column] = token
|
964
1167
|
oauth_token
|
@@ -976,7 +1179,13 @@ module Rodauth
|
|
976
1179
|
|
977
1180
|
def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
|
978
1181
|
if accepts_json?
|
979
|
-
|
1182
|
+
status_code = if respond_to?(:"#{error_code}_response_status")
|
1183
|
+
send(:"#{error_code}_response_status")
|
1184
|
+
else
|
1185
|
+
invalid_oauth_response_status
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
throw_json_response_error(status_code, error_code)
|
980
1189
|
else
|
981
1190
|
redirect_url = URI.parse(redirect_url)
|
982
1191
|
query_params = []
|
@@ -998,9 +1207,17 @@ module Rodauth
|
|
998
1207
|
end
|
999
1208
|
end
|
1000
1209
|
|
1001
|
-
def json_response_success(body)
|
1210
|
+
def json_response_success(body, cache = false)
|
1002
1211
|
response.status = 200
|
1003
1212
|
response["Content-Type"] ||= json_response_content_type
|
1213
|
+
if cache
|
1214
|
+
# defaulting to 1-day for everyone, for now at least
|
1215
|
+
max_age = 60 * 60 * 24
|
1216
|
+
response["Cache-Control"] = "private, max-age=#{max_age}"
|
1217
|
+
else
|
1218
|
+
response["Cache-Control"] = "no-store"
|
1219
|
+
response["Pragma"] = "no-cache"
|
1220
|
+
end
|
1004
1221
|
json_payload = _json_response_body(body)
|
1005
1222
|
response.write(json_payload)
|
1006
1223
|
request.halt
|
@@ -1023,7 +1240,6 @@ module Rodauth
|
|
1023
1240
|
end
|
1024
1241
|
|
1025
1242
|
unless method_defined?(:_json_response_body)
|
1026
|
-
# :nocov:
|
1027
1243
|
def _json_response_body(hash)
|
1028
1244
|
if request.respond_to?(:convert_to_json)
|
1029
1245
|
request.send(:convert_to_json, hash)
|
@@ -1031,7 +1247,6 @@ module Rodauth
|
|
1031
1247
|
JSON.dump(hash)
|
1032
1248
|
end
|
1033
1249
|
end
|
1034
|
-
# :nocov:
|
1035
1250
|
end
|
1036
1251
|
|
1037
1252
|
def authorization_required
|
@@ -1122,7 +1337,7 @@ module Rodauth
|
|
1122
1337
|
issuer += "/#{path}" if path
|
1123
1338
|
|
1124
1339
|
responses_supported = %w[code]
|
1125
|
-
response_modes_supported = %w[query]
|
1340
|
+
response_modes_supported = %w[query form_post]
|
1126
1341
|
grant_types_supported = %w[authorization_code]
|
1127
1342
|
|
1128
1343
|
if use_oauth_implicit_grant_type?
|
@@ -1130,11 +1345,12 @@ module Rodauth
|
|
1130
1345
|
response_modes_supported << "fragment"
|
1131
1346
|
grant_types_supported << "implicit"
|
1132
1347
|
end
|
1348
|
+
|
1133
1349
|
{
|
1134
1350
|
issuer: issuer,
|
1135
1351
|
authorization_endpoint: authorize_url,
|
1136
1352
|
token_endpoint: token_url,
|
1137
|
-
registration_endpoint:
|
1353
|
+
registration_endpoint: route_url(oauth_applications_path),
|
1138
1354
|
scopes_supported: oauth_application_scopes,
|
1139
1355
|
response_types_supported: responses_supported,
|
1140
1356
|
response_modes_supported: response_modes_supported,
|
@@ -1151,115 +1367,5 @@ module Rodauth
|
|
1151
1367
|
code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
|
1152
1368
|
}
|
1153
1369
|
end
|
1154
|
-
|
1155
|
-
# /token
|
1156
|
-
route(:token) do |r|
|
1157
|
-
next unless is_authorization_server?
|
1158
|
-
|
1159
|
-
before_token
|
1160
|
-
|
1161
|
-
r.post do
|
1162
|
-
catch_error do
|
1163
|
-
validate_oauth_token_params
|
1164
|
-
|
1165
|
-
oauth_token = nil
|
1166
|
-
transaction do
|
1167
|
-
oauth_token = create_oauth_token
|
1168
|
-
end
|
1169
|
-
|
1170
|
-
json_response_success(json_access_token_payload(oauth_token))
|
1171
|
-
end
|
1172
|
-
|
1173
|
-
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
1174
|
-
end
|
1175
|
-
end
|
1176
|
-
|
1177
|
-
# /introspect
|
1178
|
-
route(:introspect) do |r|
|
1179
|
-
next unless is_authorization_server?
|
1180
|
-
|
1181
|
-
before_introspect
|
1182
|
-
|
1183
|
-
r.post do
|
1184
|
-
catch_error do
|
1185
|
-
validate_oauth_introspect_params
|
1186
|
-
|
1187
|
-
oauth_token = case param("token_type_hint")
|
1188
|
-
when "access_token"
|
1189
|
-
oauth_token_by_token(param("token"))
|
1190
|
-
when "refresh_token"
|
1191
|
-
oauth_token_by_refresh_token(param("token"))
|
1192
|
-
else
|
1193
|
-
oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
|
1194
|
-
end
|
1195
|
-
|
1196
|
-
if oauth_application
|
1197
|
-
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
1198
|
-
elsif oauth_token
|
1199
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
1200
|
-
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
1201
|
-
end
|
1202
|
-
|
1203
|
-
json_response_success(json_token_introspect_payload(oauth_token))
|
1204
|
-
end
|
1205
|
-
|
1206
|
-
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
1207
|
-
end
|
1208
|
-
end
|
1209
|
-
|
1210
|
-
# /revoke
|
1211
|
-
route(:revoke) do |r|
|
1212
|
-
next unless is_authorization_server?
|
1213
|
-
|
1214
|
-
before_revoke
|
1215
|
-
|
1216
|
-
r.post do
|
1217
|
-
catch_error do
|
1218
|
-
validate_oauth_revoke_params
|
1219
|
-
|
1220
|
-
oauth_token = nil
|
1221
|
-
transaction do
|
1222
|
-
oauth_token = revoke_oauth_token
|
1223
|
-
after_revoke
|
1224
|
-
end
|
1225
|
-
|
1226
|
-
if accepts_json?
|
1227
|
-
json_response_success \
|
1228
|
-
"token" => oauth_token[oauth_tokens_token_column],
|
1229
|
-
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
1230
|
-
"revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
|
1231
|
-
else
|
1232
|
-
set_notice_flash revoke_oauth_token_notice_flash
|
1233
|
-
redirect request.referer || "/"
|
1234
|
-
end
|
1235
|
-
end
|
1236
|
-
|
1237
|
-
redirect_response_error("invalid_request", request.referer || "/")
|
1238
|
-
end
|
1239
|
-
end
|
1240
|
-
|
1241
|
-
# /authorize
|
1242
|
-
route(:authorize) do |r|
|
1243
|
-
next unless is_authorization_server?
|
1244
|
-
|
1245
|
-
require_account
|
1246
|
-
validate_oauth_grant_params
|
1247
|
-
try_approval_prompt if use_oauth_access_type? && request.get?
|
1248
|
-
|
1249
|
-
before_authorize
|
1250
|
-
|
1251
|
-
r.get do
|
1252
|
-
authorize_view
|
1253
|
-
end
|
1254
|
-
|
1255
|
-
r.post do
|
1256
|
-
redirect_url = URI.parse(redirect_uri)
|
1257
|
-
|
1258
|
-
transaction do
|
1259
|
-
do_authorize(redirect_url)
|
1260
|
-
end
|
1261
|
-
redirect(redirect_url.to_s)
|
1262
|
-
end
|
1263
|
-
end
|
1264
1370
|
end
|
1265
1371
|
end
|