rodauth-oauth 0.0.6 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73f73e4143c1c646b3a6f2a5e87fb925f38f18e157366d7522328b5cbcd2fbb7
4
- data.tar.gz: bce8a4532e365328bb46197e72b05b78beca19b00587630f234cff0c8d51bc35
3
+ metadata.gz: c7c9cc026f547d781b05599d177237498390c8347791aaf5960e7447d2640b0b
4
+ data.tar.gz: 290ec103b22d394fbae7f153430605fa032b8baf6b6083e31ad8af8cd3d422b8
5
5
  SHA512:
6
- metadata.gz: 46053f71e35baad7b3c217bfbca0a259d07fd6909713805db23ff92c0503c0907903483e659fe988af774b26a87b274f8b72006983b1acc191bccb7d00919a86
7
- data.tar.gz: 35305a71ea2b4035933d93e3fa5a3e9b388f32b2301ff05b87a86af45defa494cefc4d6d9adb823822c82acb995e57794fb710b1935aaab10430c1898d1a55b0
6
+ metadata.gz: 64c22bd200ff9dcb5e8406ace5f4eb34625bcee5a381e52b6b7e960b614ec3941c0460997542d843ed4eaa843a85a7f2592a027c741d5380a7572edb974ca3a9
7
+ data.tar.gz: 0a6c93bc131d2fcb45e400173ced20096caa191e3ef73f4e67ab0fc12d5ead9b9f9e6867d163612299141b96b7691429cb5e5b263036887134396f244c3dd4f7
@@ -2,8 +2,160 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ### 0.4.1
6
+
7
+ ### Improvements
8
+
9
+ 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.
10
+
11
+ ### Bugfixes
12
+
13
+ * 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.
14
+
15
+ ### 0.4.0
16
+
17
+ ### Features
18
+
19
+ * 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.
20
+
21
+ * 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.
22
+
23
+
24
+ ### Improvements
25
+
26
+ * 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.
27
+
28
+ ### Bugfixes
29
+
30
+ * 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;
31
+ * rails tests were silently not running in CI;
32
+ * 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;
33
+
34
+ ### 0.3.0
35
+
36
+ #### Features
37
+
38
+ * `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.
39
+
40
+ #### Improvements
41
+
42
+
43
+ * 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).
44
+
45
+ * 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.
46
+
47
+ #### Bugfixes
48
+
49
+ * Default Templates now being packaged, as a way to provide a default experience to the OAuth journeys.
50
+
51
+ * fixing metadata urls when plugin loaded with a prefix path (@ianks)
52
+
53
+ * 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.
54
+
55
+ * OIDC configuration endpoint is now stricter, eliminating JSON metadata inherited from the Oauth metadata endpoint. (@ianks)
56
+
57
+ #### Chore
58
+
59
+ Use `rodauth.convert_timestamp` in the templates, whenever dates are displayed.
60
+
61
+ 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).
62
+
63
+ ### 0.2.0
64
+
65
+ #### Features
66
+
67
+ ##### SAML Assertion Grant Type
68
+
69
+ `rodauth-auth` now supports using a SAML Assertion to request for an Access token.In order to enable, you have to:
70
+
71
+ ```ruby
72
+ plugin :rodauth do
73
+ enable :oauth_saml
74
+ end
75
+ ```
76
+
77
+ For more info about integrating it, [check the wiki](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/SAML-Assertion-Access-Tokens).
78
+
79
+ ##### Supporting rotating keys
80
+
81
+ 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.
82
+
83
+
84
+ ##### Reuse access tokens
85
+
86
+ 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.
87
+
88
+ ##### require_authorizable_account
89
+
90
+ 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.
91
+
92
+ #### Improvements
93
+
94
+ 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).
95
+
96
+ #### Bugfixes
97
+
98
+ Calling `before_*_route` callbacks appropriately.
99
+
100
+ Fixed some mishandling of HTTP headers when in in resource-server mode.
101
+
102
+ #### Chore
103
+
104
+ * 97.7% test coverage;
105
+ * `rodauth-oauth` CI tests run against sqlite, postgresql and mysql.
106
+
107
+ ### 0.1.0
108
+
109
+ (31/7/2020)
110
+
111
+ #### Features
112
+
113
+ ##### OpenID
114
+
115
+ `rodauth-oauth` now ships with support for [OpenID Connect](https://openid.net/connect/). In order to enable, you have to:
116
+
117
+ ```ruby
118
+ plugin :rodauth do
119
+ enable :oidc
120
+ end
121
+ ```
122
+
123
+ For more info about integrating it, [check the wiki](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/home#openid-connect-since-v01).
124
+
125
+ It supports omniauth openID integrations out-of-the-box, [check the OpenID example, which integrates with omniauth_openid_connect](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/tree/master/examples).
126
+
127
+ #### Improvements
128
+
129
+ * JWT: `sub` claim now also handles "pairwise" subjects. For that, you have to set the `oauth_jwt_subject_type` option (`"public"` or `"pairwise"`) and `oauth_jwt_subject_secret` (will be used for salting the `sub` when the type is `"pairwise"`).
130
+ * JWT: `auth_time` claim is now supported; if your application uses the `rodauth` feature `:account_expiration`, it'll use the `last_account_login_at` method, otherwise you can set the `last_account_login_at` option:
131
+
132
+ ```ruby
133
+ last_account_login_at do
134
+ convert_timestamp(db[accounts_table].where(account_id_column => account_id).get(:that_column_where_you_keep_the_data))
135
+ end
136
+ ```
137
+ * JWT: `iss` claim now defaults to `authorization_server_url` when not defined;
138
+ * JWT: `aud` claim now defaults to the token application's client ID (`client_id` claim was removed as a result);
139
+
140
+
141
+
142
+ #### Breaking Changes
143
+
144
+ `rodauth-oauth` URLs no longer have the `oauth-` prefix, so make sure you update your integrations accordingly, i.e. where you used to rely on `/oauth-authorize`, you'll have to use `/authorize`.
145
+
146
+ URI schemes for client applications redirect URIs have to be `https`. In order to override this, set the `oauth_valid_uri_schemes` to an array of your expected URI schemes.
147
+
148
+
149
+ #### Bugfixes
150
+
151
+ * Authorization request submission can receive the `scope` as an array of values now, instead of only dealing with receiving a white-space separated list.
152
+ * fixed trailing "/" in the "issuer" value in server metadata (`https://server.com/` -> `https://server.com`).
153
+
154
+
5
155
  ### 0.0.6
6
156
 
157
+ (6/7/2020)
158
+
7
159
  #### Features
8
160
 
9
161
  The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (see https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20). This means that client applications can send the authorization parameters inside a signed JWT. The client applications keeps the private key, while the authorization server **must** store a public key for the client application. For encrypted JWTs, the client application should use one of the public encryption keys exposed in the JWKs URI, to encrypt the JWT. Remember, **tokens must be signed then encrypted** (or just signed).
@@ -25,7 +177,9 @@ The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (se
25
177
  Removed React Javascript from example applications.
26
178
 
27
179
 
28
- ### 0.0.5 (26/6/2020)
180
+ ### 0.0.5
181
+
182
+ (26/6/2020)
29
183
 
30
184
  #### Features
31
185
 
@@ -62,7 +216,9 @@ It **requires** the authorization to implement the server metadata endpoint (`/.
62
216
  * option `scopes_param` renamed to `scope_param`;
63
217
  *
64
218
 
65
- ## 0.0.4 (13/6/2020)
219
+ ## 0.0.4
220
+
221
+ (13/6/2020)
66
222
 
67
223
  ### Features
68
224
 
@@ -99,7 +255,9 @@ The `oauth_jwt` feature now allows the usage of access tokens to authorize the g
99
255
 
100
256
  * Fixed scope claim of JWT ("scopes" -> "scope");
101
257
 
102
- ## 0.0.3 (5/6/2020)
258
+ ## 0.0.3
259
+
260
+ (5/6/2020)
103
261
 
104
262
  ### Features
105
263
 
@@ -131,7 +289,9 @@ end
131
289
  * renamed the existing `use_oauth_implicit_grant_type` to `use_oauth_implicit_grant_type?`;
132
290
  * It's now usable as JSON API (small caveat: POST authorize will still redirect on success...);
133
291
 
134
- ## 0.0.2 (29/5/2020)
292
+ ## 0.0.2
293
+
294
+ (29/5/2020)
135
295
 
136
296
  ### Features
137
297
 
@@ -147,6 +307,8 @@ end
147
307
 
148
308
  * usage of client secret for authorizing the generation of tokens, as the spec mandates (and refraining from them when doing PKCE).
149
309
 
150
- ## 0.0.1 (14/5/2020)
310
+ ## 0.0.1
311
+
312
+ (14/5/2020)
151
313
 
152
314
  Initial implementation of the Oauth 2.0 framework, with an example app done using roda.
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Rodauth::Oauth
2
2
 
3
- [![pipeline status](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/commits/master)
4
- [![coverage report](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/coverage.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/commits/master)
3
+ [![pipeline status](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/pipelines?page=1&ref=master)
4
+ [![coverage report](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/coverage.svg?job=coverage)](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
 
8
8
  ## Features
9
9
 
10
- This gem implements:
10
+ This gem implements the following RFCs and features of OAuth:
11
11
 
12
12
  * [The OAuth 2.0 protocol framework](https://tools.ietf.org/html/rfc6749):
13
13
  * [Authorization grant flow](https://tools.ietf.org/html/rfc6749#section-1.3);
@@ -21,9 +21,12 @@ This gem implements:
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
 
28
+ It also implements the [OpenID Connect layer](https://openid.net/connect/) on top of the OAuth features it provides.
29
+
27
30
  This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
28
31
 
29
32
 
@@ -43,6 +46,15 @@ Or install it yourself as:
43
46
 
44
47
  $ gem install rodauth-oauth
45
48
 
49
+
50
+ ## Resources
51
+ | | |
52
+ | ------------- | ----------------------------------------------------------- |
53
+ | Website | https://honeyryderchuck.gitlab.io/rodauth-oauth/ |
54
+ | Documentation | https://honeyryderchuck.gitlab.io/rodauth-oauth/rdoc/ |
55
+ | Wiki | https://gitlab.com/honeyryderchuck/rodauth-oauth/wikis/home |
56
+ | CI | https://gitlab.com/honeyryderchuck/rodauth-oauth/pipelines |
57
+
46
58
  ## Usage
47
59
 
48
60
  This tutorial assumes you already read the documentation and know how to set up `rodauth`. After that, integrating `roda-auth` will look like:
@@ -86,7 +98,18 @@ route do |r|
86
98
  end
87
99
  ```
88
100
 
89
- You'll have to do a bit more boilerplate, so here's the instructions.
101
+
102
+ For OpenID, it's very similar to the example above:
103
+
104
+ ```ruby
105
+ plugin :rodauth do
106
+ # enable it in the plugin
107
+ enable :login, :openid
108
+ oauth_application_default_scope %w[openid]
109
+ oauth_application_scopes %w[openid email profile]
110
+ end
111
+ ```
112
+
90
113
 
91
114
  ### Example (TL;DR)
92
115
 
@@ -101,7 +124,7 @@ Generating tokens happens mostly server-to-server, so here's an example using:
101
124
 
102
125
  ```ruby
103
126
  require "httpx"
104
- response = HTTPX.post("https://auth_server/oauth-token",json: {
127
+ response = HTTPX.post("https://auth_server/token",json: {
105
128
  client_id: ENV["OAUTH_CLIENT_ID"],
106
129
  client_secret: ENV["OAUTH_CLIENT_SECRET"],
107
130
  grant_type: "authorization_code",
@@ -115,7 +138,7 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "refresh_token" => "2
115
138
  ##### cURL
116
139
 
117
140
  ```
118
- > curl --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"authorization_code","code":"oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"}' https://auth_server/oauth-token
141
+ > curl --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"authorization_code","code":"oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"}' https://auth_server/token
119
142
  ```
120
143
 
121
144
  #### Refresh Token
@@ -126,7 +149,7 @@ Refreshing expired tokens also happens mostly server-to-server, here's an exampl
126
149
 
127
150
  ```ruby
128
151
  require "httpx"
129
- response = HTTPX.post("https://auth_server/oauth-token",json: {
152
+ response = HTTPX.post("https://auth_server/token",json: {
130
153
  client_id: ENV["OAUTH_CLIENT_ID"],
131
154
  client_secret: ENV["OAUTH_CLIENT_SECRET"],
132
155
  grant_type: "refresh_token",
@@ -140,7 +163,7 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "token_type" => "Bear
140
163
  ##### cURL
141
164
 
142
165
  ```
143
- > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-token
166
+ > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/token
144
167
  ```
145
168
 
146
169
  #### Revoking tokens
@@ -151,7 +174,7 @@ Token revocation can be done both by the idenntity owner or the application owne
151
174
  require "httpx"
152
175
  httpx = HTTPX.plugin(:basic_authorization)
153
176
  response = httpx.basic_authentication(ENV["CLIENT_ID"], ENV["CLIENT_SECRET"])
154
- .post("https://auth_server/oauth-revoke",json: {
177
+ .post("https://auth_server/revoke",json: {
155
178
  token_type_hint: "access_token", # can also be "refresh:tokn"
156
179
  token: "2r89hfef4j9f90d2j2390jf390g"
157
180
  })
@@ -163,7 +186,7 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "token_type" => "Bear
163
186
  ##### cURL
164
187
 
165
188
  ```
166
- > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-revoke
189
+ > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/revoke
167
190
  ```
168
191
 
169
192
  #### Token introspection
@@ -174,7 +197,7 @@ Token revocation can be used to determine the state of a token (whether active,
174
197
  require "httpx"
175
198
  httpx = HTTPX.plugin(:basic_authorization)
176
199
  response = httpx.basic_authentication(ENV["CLIENT_ID"], ENV["CLIENT_SECRET"])
177
- .post("https://auth_server/oauth-introspect",json: {
200
+ .post("https://auth_server/introspect",json: {
178
201
  token_type_hint: "access_token", # can also be "refresh:tokn"
179
202
  token: "2r89hfef4j9f90d2j2390jf390g"
180
203
  })
@@ -186,7 +209,7 @@ puts payload #=> {"active" => true, "scope" => "read write" ....
186
209
  ##### cURL
187
210
 
188
211
  ```
189
- > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-revoke
212
+ > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/revoke
190
213
  ```
191
214
 
192
215
  ### Authorization Server Metadata
@@ -243,10 +266,10 @@ The rodauth default setup expects the roda `render` plugin to be activated; by d
243
266
 
244
267
  Once you set it up, by default, the following endpoints will be available:
245
268
 
246
- * `GET /oauth-authorize`: Loads the OAuth authorization HTML form;
247
- * `POST /oauth-authorize`: Responds to an OAuth authorization request, as [per the spec](https://tools.ietf.org/html/rfc6749#section-4);
248
- * `POST /oauth-token`: Generates OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc6749#section-4.4.2);
249
- * `POST /oauth-revoke`: Revokes OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc7009);
269
+ * `GET /authorize`: Loads the OAuth authorization HTML form;
270
+ * `POST /authorize`: Responds to an OAuth authorization request, as [per the spec](https://tools.ietf.org/html/rfc6749#section-4);
271
+ * `POST /token`: Generates OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc6749#section-4.4.2);
272
+ * `POST /revoke`: Revokes OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc7009);
250
273
 
251
274
  ### OAuth applications
252
275
 
@@ -426,7 +449,7 @@ The "Proof Key for Code Exchange by OAuth Public Clients" (aka PKCE) flow, which
426
449
  ```ruby
427
450
  # with httpx
428
451
  require "httpx"
429
- response = HTTPX.post("https://auth_server/oauth-token",json: {
452
+ response = HTTPX.post("https://auth_server/token",json: {
430
453
  client_id: ENV["OAUTH_CLIENT_ID"],
431
454
  grant_type: "authorization_code",
432
455
  code: "oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as",
@@ -477,7 +500,7 @@ Generating an access token will deliver the following fields:
477
500
  ```ruby
478
501
  # with httpx
479
502
  require "httpx"
480
- response = httpx.post("https://auth_server/oauth-token",json: {
503
+ response = httpx.post("https://auth_server/token",json: {
481
504
  client_id: env["oauth_client_id"],
482
505
  client_secret: env["oauth_client_secret"],
483
506
  grant_type: "authorization_code",
@@ -576,7 +599,7 @@ which adds an extra layer of protection.
576
599
 
577
600
  #### JWKS URI
578
601
 
579
- A route is defined for getting the JWK Set in a JSON format; this is typically used by client applications, who need the JWK set to decode the JWT token. This URL is typically `https://oauth-server/oauth-jwks`.
602
+ A route is defined for getting the JWK Set in a JSON format; this is typically used by client applications, who need the JWK set to decode the JWT token. This URL is typically `https://oauth-server/jwks`.
580
603
 
581
604
  #### JWT Bearer as authorization grant
582
605
 
@@ -585,7 +608,7 @@ One can emit a new access token by using the bearer access token as grant. This
585
608
  ```ruby
586
609
  # with httpx
587
610
  require "httpx"
588
- response = httpx.post("https://auth_server/oauth-token",json: {
611
+ response = httpx.post("https://auth_server/token",json: {
589
612
  grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
590
613
  assertion: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6IkV4YW1wbGUiLCJpYXQiOjE1OTIwMDk1MDEsImNsaWVudF9pZCI6IkNMSUVOVF9JRCIsImV4cCI6MTU5MjAxMzEwMSwiYXVkIjpudWxsLCJzY29wZSI6InVzZXIucmVhZCB1c2VyLndyaXRlIiwianRpIjoiOGM1NTVjMjdiOWRjNDdmOTcyNWRkYzBhMjk0NzA1ZTA4NzFkY2JlN2Q5ZTNlMmVkNGE1ZTBiOGZlNTZlYzcxMSJ9.AlxKRtE3ec0mtyBSDx4VseND4eC6cH5ubtv8gfYxxsc"
591
614
  })
@@ -29,7 +29,8 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
29
29
  # uncomment to enable PKCE
30
30
  # t.string :code_challenge
31
31
  # t.string :code_challenge_method
32
-
32
+ # uncomment to use OIDC nonce
33
+ # t.string :nonce
33
34
  t.index(%i[oauth_application_id code], unique: true)
34
35
  end
35
36
 
@@ -42,18 +43,20 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
42
43
  t.foreign_key :oauth_tokens, column: :oauth_token_id
43
44
  t.integer :oauth_application_id
44
45
  t.foreign_key :oauth_applications, column: :oauth_application_id
45
- t.string :token, null: false, token: true
46
+ t.string :token, null: false, token: true, unique: true
46
47
  # uncomment if setting oauth_tokens_token_hash_column
47
48
  # and delete the token column
48
- # t.string :token_hash, token: true
49
- t.string :refresh_token
49
+ # t.string :token_hash, token: true, unique: true
50
+ t.string :refresh_token, unique: true
50
51
  # uncomment if setting oauth_tokens_refresh_token_hash_column
51
52
  # and delete the refresh_token column
52
- # t.string :refresh_token_hash, token: true
53
+ # t.string :refresh_token_hash, token: true, unique: true
53
54
  t.datetime :expires_in, null: false
54
55
  t.datetime :revoked_at
55
56
  t.string :scopes, null: false
56
57
  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
58
+ # uncomment to use OIDC nonce
59
+ # t.string :nonce
57
60
  end
58
61
  end
59
62
  end
@@ -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,13 +43,13 @@ 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
- after "authorize_failure"
47
53
 
48
54
  before "token"
49
55
 
@@ -55,15 +61,13 @@ module Rodauth
55
61
  before "create_oauth_application"
56
62
  after "create_oauth_application"
57
63
 
58
- error_flash "OAuth Authorization invalid parameters", "oauth_grant_valid_parameters"
59
-
60
64
  error_flash "Please authorize to continue", "require_authorization"
61
65
  error_flash "There was an error registering your oauth application", "create_oauth_application"
62
66
  notice_flash "Your oauth application has been registered", "create_oauth_application"
63
67
 
64
68
  notice_flash "The oauth token has been revoked", "revoke_oauth_token"
65
69
 
66
- view "oauth_authorize", "Authorize", "authorize"
70
+ view "authorize", "Authorize", "authorize"
67
71
  view "oauth_applications", "Oauth Applications", "oauth_applications"
68
72
  view "oauth_application", "Oauth Application", "oauth_application"
69
73
  view "new_oauth_application", "New Oauth Application", "new_oauth_application"
@@ -73,14 +77,16 @@ module Rodauth
73
77
 
74
78
  auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
75
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
76
81
  auth_value_method :use_oauth_implicit_grant_type?, false
77
82
  auth_value_method :use_oauth_pkce?, true
78
83
  auth_value_method :use_oauth_access_type?, true
79
84
 
80
85
  auth_value_method :oauth_require_pkce, false
81
86
  auth_value_method :oauth_pkce_challenge_method, "S256"
87
+ auth_value_method :oauth_response_mode, "query"
82
88
 
83
- auth_value_method :oauth_valid_uri_schemes, %w[http https]
89
+ auth_value_method :oauth_valid_uri_schemes, %w[https]
84
90
 
85
91
  auth_value_method :oauth_scope_separator, " "
86
92
 
@@ -95,6 +101,7 @@ module Rodauth
95
101
  button "Register", "oauth_application"
96
102
  button "Authorize", "oauth_authorize"
97
103
  button "Revoke", "oauth_token_revoke"
104
+ button "Back to Client Application", "oauth_authorize_post"
98
105
 
99
106
  # OAuth Token
100
107
  auth_value_method :oauth_tokens_path, "oauth-tokens"
@@ -113,6 +120,8 @@ module Rodauth
113
120
  auth_value_method :oauth_tokens_token_hash_column, nil
114
121
  auth_value_method :oauth_tokens_refresh_token_hash_column, nil
115
122
 
123
+ # Access Token reuse
124
+ auth_value_method :oauth_reuse_access_token, false
116
125
  # OAuth Grants
117
126
  auth_value_method :oauth_grants_table, :oauth_grants
118
127
  auth_value_method :oauth_grants_id_column, :id
@@ -127,6 +136,7 @@ module Rodauth
127
136
 
128
137
  auth_value_method :authorization_required_error_status, 401
129
138
  auth_value_method :invalid_oauth_response_status, 400
139
+ auth_value_method :already_in_use_response_status, 409
130
140
 
131
141
  # OAuth Applications
132
142
  auth_value_method :oauth_applications_path, "oauth-applications"
@@ -144,13 +154,13 @@ module Rodauth
144
154
  auth_value_method :"oauth_applications_#{column}_column", column
145
155
  end
146
156
 
157
+ # Feature options
147
158
  auth_value_method :oauth_application_default_scope, SCOPES.first
148
159
  auth_value_method :oauth_application_scopes, SCOPES
149
160
  auth_value_method :oauth_token_type, "bearer"
161
+ auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
150
162
 
151
- auth_value_method :invalid_request, "Request is missing a required parameter"
152
- auth_value_method :invalid_client, "Invalid client"
153
- auth_value_method :unauthorized_client, "Unauthorized client"
163
+ auth_value_method :invalid_client_message, "Invalid client"
154
164
  auth_value_method :invalid_grant_type_message, "Invalid grant type"
155
165
  auth_value_method :invalid_grant_message, "Invalid grant"
156
166
  auth_value_method :invalid_scope_message, "Invalid scope"
@@ -160,6 +170,8 @@ module Rodauth
160
170
 
161
171
  auth_value_method :unique_error_message, "is already in use"
162
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"
163
175
 
164
176
  # PKCE
165
177
  auth_value_method :code_challenge_required_error_code, "invalid_request"
@@ -177,6 +189,8 @@ module Rodauth
177
189
  # Only required to use if the plugin is to be used in a resource server
178
190
  auth_value_method :is_authorization_server?, true
179
191
 
192
+ auth_value_method :oauth_unique_id_generation_retries, 3
193
+
180
194
  auth_value_methods(
181
195
  :fetch_access_token,
182
196
  :oauth_unique_id_generator,
@@ -184,22 +198,231 @@ module Rodauth
184
198
  :secret_hash,
185
199
  :generate_token_hash,
186
200
  :authorization_server_url,
187
- :before_introspection_request
201
+ :before_introspection_request,
202
+ :require_authorizable_account,
203
+ :oauth_tokens_unique_columns
188
204
  )
189
205
 
190
206
  auth_value_methods(:only_json?)
191
207
 
192
208
  auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
193
209
 
194
- SERVER_METADATA = OAuth::TtlStore.new
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
195
418
 
196
419
  def check_csrf?
197
420
  case request.path
198
- when oauth_token_path, oauth_introspect_path
421
+ when token_path, introspect_path
199
422
  false
200
- when oauth_revoke_path
423
+ when revoke_path
201
424
  !json_request?
202
- when oauth_authorize_path, %r{/#{oauth_applications_path}}
425
+ when authorize_path, %r{/#{oauth_applications_path}}
203
426
  only_json? ? false : super
204
427
  else
205
428
  super
@@ -218,14 +441,12 @@ module Rodauth
218
441
  end
219
442
 
220
443
  unless method_defined?(:json_request?)
221
- # :nocov:
222
444
  # copied from the jwt feature
223
445
  def json_request?
224
446
  return @json_request if defined?(@json_request)
225
447
 
226
448
  @json_request = request.content_type =~ json_request_regexp
227
449
  end
228
- # :nocov:
229
450
  end
230
451
 
231
452
  def initialize(scope)
@@ -233,7 +454,15 @@ module Rodauth
233
454
  end
234
455
 
235
456
  def scopes
236
- (param_or_nil("scope") || oauth_application_default_scope).split(" ")
457
+ scope = request.params["scope"]
458
+ case scope
459
+ when Array
460
+ scope
461
+ when String
462
+ scope.split(" ")
463
+ when nil
464
+ [oauth_application_default_scope]
465
+ end
237
466
  end
238
467
 
239
468
  def redirect_uri
@@ -260,12 +489,14 @@ module Rodauth
260
489
  def fetch_access_token
261
490
  value = request.env["HTTP_AUTHORIZATION"]
262
491
 
263
- return unless value
492
+ return unless value && !value.empty?
264
493
 
265
494
  scheme, token = value.split(" ", 2)
266
495
 
267
496
  return unless scheme.downcase == oauth_token_type
268
497
 
498
+ return if token.nil? || token.empty?
499
+
269
500
  token
270
501
  end
271
502
 
@@ -277,101 +508,79 @@ module Rodauth
277
508
 
278
509
  return unless bearer_token
279
510
 
280
- # check if token has not expired
281
- # check if token has been revoked
282
- @authorization_token = oauth_token_by_token(bearer_token)
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
283
523
  end
284
524
 
285
525
  def require_oauth_authorization(*scopes)
286
- token_scopes = if is_authorization_server?
287
- authorization_required unless authorization_token
526
+ authorization_required unless authorization_token
288
527
 
289
- scopes << oauth_application_default_scope if scopes.empty?
528
+ scopes << oauth_application_default_scope if scopes.empty?
290
529
 
530
+ token_scopes = if is_authorization_server?
291
531
  authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
292
532
  else
293
- bearer_token = fetch_access_token
294
-
295
- authorization_required unless bearer_token
296
-
297
- scopes << oauth_application_default_scope if scopes.empty?
298
-
299
- # where in resource server, NOT the authorization server.
300
- payload = introspection_request("access_token", bearer_token)
301
-
302
- authorization_required unless payload["active"]
303
-
304
- 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
305
539
  end
306
540
 
307
541
  authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
308
542
  end
309
543
 
310
- # /oauth-applications routes
311
- def oauth_applications
312
- request.on(oauth_applications_path) do
313
- require_account
314
-
315
- request.get "new" do
316
- new_oauth_application_view
317
- end
318
-
319
- request.on(oauth_applications_id_pattern) do |id|
320
- oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
321
- next unless oauth_application
322
-
323
- scope.instance_variable_set(:@oauth_application, oauth_application)
544
+ def post_configure
545
+ super
546
+ self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
324
547
 
325
- request.is do
326
- request.get do
327
- oauth_application_view
328
- end
329
- end
330
-
331
- request.on(oauth_tokens_path) do
332
- oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
333
- scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
334
- request.get do
335
- oauth_tokens_view
336
- end
337
- end
548
+ # Check whether we can reutilize db entries for the same account / application pair
549
+ one_oauth_token_per_account = begin
550
+ db.indexes(oauth_tokens_table).values.any? do |definition|
551
+ definition[:unique] &&
552
+ definition[:columns] == oauth_tokens_unique_columns
338
553
  end
554
+ end
555
+ self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
556
+ end
339
557
 
340
- request.get do
341
- scope.instance_variable_set(:@oauth_applications, db[:oauth_applications])
342
- oauth_applications_view
343
- end
558
+ def use_date_arithmetic?
559
+ true
560
+ end
344
561
 
345
- request.post do
346
- catch_error do
347
- validate_oauth_application_params
562
+ private
348
563
 
349
- transaction do
350
- before_create_oauth_application
351
- id = create_oauth_application
352
- after_create_oauth_application
353
- set_notice_flash create_oauth_application_notice_flash
354
- redirect "#{request.path}/#{id}"
355
- end
356
- end
357
- set_error_flash create_oauth_application_error_flash
358
- new_oauth_application_view
359
- end
564
+ def rescue_from_uniqueness_error(&block)
565
+ retries = oauth_unique_id_generation_retries
566
+ begin
567
+ transaction(savepoint: :only, &block)
568
+ rescue Sequel::UniqueConstraintViolation
569
+ redirect_response_error("already_in_use") if retries.zero?
570
+ retries -= 1
571
+ retry
360
572
  end
361
573
  end
362
574
 
363
- def oauth_server_metadata(issuer = nil)
364
- request.on(".well-known") do
365
- request.on("oauth-authorization-server") do
366
- request.get do
367
- json_response_success(oauth_server_metadata_body(issuer))
368
- end
369
- end
370
- end
575
+ # OAuth Token Unique/Reuse
576
+ def oauth_tokens_unique_columns
577
+ [
578
+ oauth_tokens_oauth_application_id_column,
579
+ oauth_tokens_account_id_column,
580
+ oauth_tokens_scopes_column
581
+ ]
371
582
  end
372
583
 
373
- private
374
-
375
584
  def authorization_server_url
376
585
  base_url
377
586
  end
@@ -394,10 +603,10 @@ module Rodauth
394
603
 
395
604
  # time-to-live
396
605
  ttl = if response.key?("cache-control")
397
- cache_control = response["cache_control"]
398
- cache_control[/max-age=(\d+)/, 1]
606
+ cache_control = response["cache-control"]
607
+ cache_control[/max-age=(\d+)/, 1].to_i
399
608
  elsif response.key?("expires")
400
- Time.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
609
+ Time.parse(response["expires"]).to_i - Time.now.to_i
401
610
  end
402
611
 
403
612
  [JSON.parse(response.body, symbolize_names: true), ttl]
@@ -409,7 +618,7 @@ module Rodauth
409
618
  http = Net::HTTP.new(auth_url.host, auth_url.port)
410
619
  http.use_ssl = auth_url.scheme == "https"
411
620
 
412
- request = Net::HTTP::Post.new(oauth_introspect_path)
621
+ request = Net::HTTP::Post.new(introspect_path)
413
622
  request["content-type"] = json_response_content_type
414
623
  request["accept"] = json_response_content_type
415
624
  request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
@@ -465,7 +674,7 @@ module Rodauth
465
674
  end
466
675
 
467
676
  def oauth_unique_id_generator
468
- SecureRandom.hex(32)
677
+ SecureRandom.urlsafe_base64(32)
469
678
  end
470
679
 
471
680
  def generate_token_hash(token)
@@ -477,89 +686,106 @@ module Rodauth
477
686
  end
478
687
 
479
688
  unless method_defined?(:password_hash)
480
- # :nocov:
481
689
  # From login_requirements_base feature
482
- if ENV["RACK_ENV"] == "test"
483
- def password_hash_cost
484
- BCrypt::Engine::MIN_COST
485
- end
486
- else
487
- def password_hash_cost
488
- BCrypt::Engine::DEFAULT_COST
489
- end
490
- end
491
690
 
492
691
  def password_hash(password)
493
- BCrypt::Password.create(password, cost: password_hash_cost)
692
+ BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
494
693
  end
495
- # :nocov:
496
694
  end
497
695
 
498
696
  def generate_oauth_token(params = {}, should_generate_refresh_token = true)
499
697
  create_params = {
500
- oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
698
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
501
699
  }.merge(params)
502
700
 
503
- token = oauth_unique_id_generator
504
- refresh_token = nil
701
+ rescue_from_uniqueness_error do
702
+ token = oauth_unique_id_generator
505
703
 
506
- if oauth_tokens_token_hash_column
507
- create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
508
- else
509
- create_params[oauth_tokens_token_column] = token
510
- end
704
+ if oauth_tokens_token_hash_column
705
+ create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
706
+ else
707
+ create_params[oauth_tokens_token_column] = token
708
+ end
511
709
 
512
- if should_generate_refresh_token
513
- refresh_token = oauth_unique_id_generator
710
+ refresh_token = nil
711
+ if should_generate_refresh_token
712
+ refresh_token = oauth_unique_id_generator
514
713
 
515
- if oauth_tokens_refresh_token_hash_column
516
- create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
517
- else
518
- create_params[oauth_tokens_refresh_token_column] = refresh_token
714
+ if oauth_tokens_refresh_token_hash_column
715
+ create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
716
+ else
717
+ create_params[oauth_tokens_refresh_token_column] = refresh_token
718
+ end
519
719
  end
720
+ oauth_token = _generate_oauth_token(create_params)
721
+ oauth_token[oauth_tokens_token_column] = token
722
+ oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
723
+ oauth_token
520
724
  end
521
- oauth_token = _generate_oauth_token(create_params)
522
-
523
- oauth_token[oauth_tokens_token_column] = token
524
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
525
- oauth_token
526
725
  end
527
726
 
528
727
  def _generate_oauth_token(params = {})
529
728
  ds = db[oauth_tokens_table]
530
729
 
531
- begin
532
- if ds.supports_returning?(:insert)
533
- ds.returning.insert(params).first
534
- else
535
- id = ds.insert(params)
536
- ds.where(oauth_tokens_id_column => id).first
730
+ if __one_oauth_token_per_account
731
+
732
+ token = __insert_or_update_and_return__(
733
+ ds,
734
+ oauth_tokens_id_column,
735
+ oauth_tokens_unique_columns,
736
+ params,
737
+ Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
738
+ ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
739
+ )
740
+
741
+ # if the previous operation didn't return a row, it means that the conditions
742
+ # invalidated the update, and the existing token is still valid.
743
+ token || ds.where(
744
+ oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
745
+ oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
746
+ ).first
747
+ else
748
+ if oauth_reuse_access_token
749
+ unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
750
+ valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
751
+ .where(unique_conds).first
752
+ return valid_token if valid_token
537
753
  end
538
- rescue Sequel::UniqueConstraintViolation
539
- retry
754
+ __insert_and_return__(ds, oauth_tokens_id_column, params)
540
755
  end
541
756
  end
542
757
 
543
- def oauth_token_by_token(token, dataset = db[oauth_tokens_table])
758
+ def oauth_token_by_token(token)
759
+ ds = db[oauth_tokens_table]
760
+
544
761
  ds = if oauth_tokens_token_hash_column
545
- dataset.where(oauth_tokens_token_hash_column => generate_token_hash(token))
762
+ ds.where(oauth_tokens_token_hash_column => generate_token_hash(token))
546
763
  else
547
- dataset.where(oauth_tokens_token_column => token)
764
+ ds.where(oauth_tokens_token_column => token)
548
765
  end
549
766
 
550
767
  ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
551
768
  .where(oauth_tokens_revoked_at_column => nil).first
552
769
  end
553
770
 
554
- def oauth_token_by_refresh_token(token, dataset = db[oauth_tokens_table])
771
+ def oauth_token_by_refresh_token(token, revoked: false)
772
+ ds = db[oauth_tokens_table]
773
+ #
774
+ # filter expired refresh tokens out.
775
+ # an expired refresh token is a token whose access token expired for a period longer than the
776
+ # refresh token expiration period.
777
+ #
778
+ ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
779
+
555
780
  ds = if oauth_tokens_refresh_token_hash_column
556
- dataset.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
781
+ ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
557
782
  else
558
- dataset.where(oauth_tokens_refresh_token_column => token)
783
+ ds.where(oauth_tokens_refresh_token_column => token)
559
784
  end
560
785
 
561
- ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
562
- .where(oauth_tokens_revoked_at_column => nil).first
786
+ ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
787
+
788
+ ds.first
563
789
  end
564
790
 
565
791
  def json_access_token_payload(oauth_token)
@@ -628,7 +854,6 @@ module Rodauth
628
854
  # set client ID/secret pairs
629
855
 
630
856
  create_params.merge! \
631
- oauth_applications_client_id_column => oauth_unique_id_generator,
632
857
  oauth_applications_client_secret_column => \
633
858
  secret_hash(oauth_application_params[oauth_application_client_secret_param])
634
859
 
@@ -638,29 +863,14 @@ module Rodauth
638
863
  oauth_application_default_scope
639
864
  end
640
865
 
641
- id = nil
642
- raised = begin
643
- id = db[oauth_applications_table].insert(create_params)
644
- false
645
- rescue Sequel::ConstraintViolation => e
646
- e
647
- end
648
-
649
- if raised
650
- field = raised.message[/\.(.*)$/, 1]
651
- case raised
652
- when Sequel::UniqueConstraintViolation
653
- throw_error(field, unique_error_message)
654
- when Sequel::NotNullConstraintViolation
655
- throw_error(field, null_error_message)
656
- end
866
+ rescue_from_uniqueness_error do
867
+ create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
868
+ db[oauth_applications_table].insert(create_params)
657
869
  end
658
-
659
- !raised && id
660
870
  end
661
871
 
662
872
  # Authorize
663
- def before_authorize
873
+ def require_authorizable_account
664
874
  require_account
665
875
  end
666
876
 
@@ -673,6 +883,9 @@ module Rodauth
673
883
  end
674
884
  redirect_response_error("invalid_scope") unless check_valid_scopes?
675
885
 
886
+ if (response_mode = param_or_nil("response_mode")) && response_mode != "form_post"
887
+ redirect_response_error("invalid_request")
888
+ end
676
889
  validate_pkce_challenge_params if use_oauth_pkce?
677
890
  end
678
891
 
@@ -690,57 +903,79 @@ module Rodauth
690
903
  ).count.zero?
691
904
 
692
905
  # if there's a previous oauth grant for the params combo, it means that this user has approved before.
693
-
694
906
  request.env["REQUEST_METHOD"] = "POST"
695
907
  end
696
908
 
697
- def create_oauth_grant
698
- create_params = {
909
+ def create_oauth_grant(create_params = {})
910
+ create_params.merge!(
699
911
  oauth_grants_account_id_column => account_id,
700
912
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
701
913
  oauth_grants_redirect_uri_column => redirect_uri,
702
- oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
914
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
703
915
  oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
704
- }
916
+ )
705
917
 
706
918
  # Access Type flow
707
- if use_oauth_access_type?
708
- if (access_type = param_or_nil("access_type"))
709
- create_params[oauth_grants_access_type_column] = access_type
710
- end
919
+ if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
920
+ create_params[oauth_grants_access_type_column] = access_type
711
921
  end
712
922
 
713
923
  # PKCE flow
714
- if use_oauth_pkce?
924
+ if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
925
+ code_challenge_method = param_or_nil("code_challenge_method")
715
926
 
716
- if (code_challenge = param_or_nil("code_challenge"))
717
- code_challenge_method = param_or_nil("code_challenge_method")
718
-
719
- create_params[oauth_grants_code_challenge_column] = code_challenge
720
- create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
721
- elsif oauth_require_pkce
722
- redirect_response_error("code_challenge_required")
723
- end
927
+ create_params[oauth_grants_code_challenge_column] = code_challenge
928
+ create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
724
929
  end
725
930
 
726
931
  ds = db[oauth_grants_table]
727
932
 
728
- begin
729
- authorization_code = oauth_unique_id_generator
730
- create_params[oauth_grants_code_column] = authorization_code
731
- ds.insert(create_params)
732
- authorization_code
733
- rescue Sequel::UniqueConstraintViolation
734
- retry
933
+ rescue_from_uniqueness_error do
934
+ create_params[oauth_grants_code_column] = oauth_unique_id_generator
935
+ __insert_and_return__(ds, oauth_grants_id_column, create_params)
735
936
  end
937
+ create_params[oauth_grants_code_column]
938
+ end
939
+
940
+ def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
941
+ case param("response_type")
942
+ when "token"
943
+ redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
944
+
945
+ response_mode ||= "fragment"
946
+ response_params.replace(_do_authorize_token)
947
+ when "code"
948
+ response_mode ||= "query"
949
+ response_params.replace(_do_authorize_code)
950
+ when "none"
951
+ response_mode ||= "none"
952
+ when "", nil
953
+ response_mode ||= oauth_response_mode
954
+ response_params.replace(_do_authorize_code)
955
+ end
956
+
957
+ response_params["state"] = param("state") if param_or_nil("state")
958
+
959
+ [response_params, response_mode]
736
960
  end
737
961
 
738
- # Access Tokens
962
+ def _do_authorize_code
963
+ { "code" => create_oauth_grant }
964
+ end
739
965
 
740
- def before_token
741
- require_oauth_application
966
+ def _do_authorize_token
967
+ create_params = {
968
+ oauth_tokens_account_id_column => account_id,
969
+ oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
970
+ oauth_tokens_scopes_column => scopes
971
+ }
972
+ oauth_token = generate_oauth_token(create_params, false)
973
+
974
+ json_access_token_payload(oauth_token)
742
975
  end
743
976
 
977
+ # Access Tokens
978
+
744
979
  def validate_oauth_token_params
745
980
  unless (grant_type = param_or_nil("grant_type"))
746
981
  redirect_response_error("invalid_request")
@@ -760,27 +995,56 @@ module Rodauth
760
995
  def create_oauth_token
761
996
  case param("grant_type")
762
997
  when "authorization_code"
763
- create_oauth_token_from_authorization_code(oauth_application)
998
+ # fetch oauth grant
999
+ oauth_grant = db[oauth_grants_table].where(
1000
+ oauth_grants_code_column => param("code"),
1001
+ oauth_grants_redirect_uri_column => param("redirect_uri"),
1002
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
1003
+ oauth_grants_revoked_at_column => nil
1004
+ ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
1005
+ .for_update
1006
+ .first
1007
+
1008
+ redirect_response_error("invalid_grant") unless oauth_grant
1009
+
1010
+ create_params = {
1011
+ oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
1012
+ oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
1013
+ oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
1014
+ oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
1015
+ }
1016
+ create_oauth_token_from_authorization_code(oauth_grant, create_params)
764
1017
  when "refresh_token"
765
- create_oauth_token_from_token(oauth_application)
766
- else
767
- redirect_response_error("invalid_grant")
1018
+ # fetch potentially revoked oauth token
1019
+ oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
1020
+
1021
+ if !oauth_token
1022
+ redirect_response_error("invalid_grant")
1023
+ elsif oauth_token[oauth_tokens_revoked_at_column]
1024
+ if oauth_refresh_token_protection_policy == "rotation"
1025
+ # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
1026
+ #
1027
+ # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
1028
+ # client, one of them will present an invalidated refresh token, which will inform the authorization
1029
+ # server of the breach. The authorization server cannot determine which party submitted the invalid
1030
+ # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
1031
+ # forcing the legitimate client to obtain a fresh authorization grant.
1032
+
1033
+ db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
1034
+ .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
1035
+ end
1036
+ redirect_response_error("invalid_grant")
1037
+ end
1038
+
1039
+ update_params = {
1040
+ oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
1041
+ oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
1042
+ }
1043
+ create_oauth_token_from_token(oauth_token, update_params)
768
1044
  end
769
1045
  end
770
1046
 
771
- def create_oauth_token_from_authorization_code(oauth_application)
772
- # fetch oauth grant
773
- oauth_grant = db[oauth_grants_table].where(
774
- oauth_grants_code_column => param("code"),
775
- oauth_grants_redirect_uri_column => param("redirect_uri"),
776
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
777
- oauth_grants_revoked_at_column => nil
778
- ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
779
- .for_update
780
- .first
781
-
782
- redirect_response_error("invalid_grant") unless oauth_grant
783
-
1047
+ def create_oauth_token_from_authorization_code(oauth_grant, create_params)
784
1048
  # PKCE
785
1049
  if use_oauth_pkce?
786
1050
  if oauth_grant[oauth_grants_code_challenge_column]
@@ -792,13 +1056,6 @@ module Rodauth
792
1056
  end
793
1057
  end
794
1058
 
795
- create_params = {
796
- oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
797
- oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
798
- oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
799
- oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
800
- }
801
-
802
1059
  # revoke oauth grant
803
1060
  db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
804
1061
  .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
@@ -809,40 +1066,41 @@ module Rodauth
809
1066
  generate_oauth_token(create_params, should_generate_refresh_token)
810
1067
  end
811
1068
 
812
- def create_oauth_token_from_token(oauth_application)
813
- # fetch oauth token
814
- oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
815
-
816
- redirect_response_error("invalid_grant") unless oauth_token && token_from_application?(oauth_token, oauth_application)
1069
+ def create_oauth_token_from_token(oauth_token, update_params)
1070
+ redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
817
1071
 
818
- token = oauth_unique_id_generator
1072
+ rescue_from_uniqueness_error do
1073
+ oauth_tokens_ds = db[oauth_tokens_table]
1074
+ token = oauth_unique_id_generator
819
1075
 
820
- update_params = {
821
- oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
822
- oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
823
- }
824
-
825
- if oauth_tokens_token_hash_column
826
- update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
827
- else
828
- update_params[oauth_tokens_token_column] = token
829
- end
830
-
831
- ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
832
-
833
- oauth_token = begin
834
- if ds.supports_returning?(:update)
835
- ds.returning.update(update_params).first
1076
+ if oauth_tokens_token_hash_column
1077
+ update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
836
1078
  else
837
- ds.update(update_params)
838
- ds.first
1079
+ update_params[oauth_tokens_token_column] = token
839
1080
  end
840
- rescue Sequel::UniqueConstraintViolation
841
- retry
842
- end
843
1081
 
844
- oauth_token[oauth_tokens_token_column] = token
845
- oauth_token
1082
+ oauth_token = if oauth_refresh_token_protection_policy == "rotation"
1083
+ insert_params = {
1084
+ **update_params,
1085
+ oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
1086
+ oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
1087
+ }
1088
+
1089
+ # revoke the refresh token
1090
+ oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
1091
+ .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
1092
+
1093
+ insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
1094
+ __insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
1095
+ else
1096
+ # includes none
1097
+ ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
1098
+ __update_and_return__(ds, update_params)
1099
+ end
1100
+
1101
+ oauth_token[oauth_tokens_token_column] = token
1102
+ oauth_token
1103
+ end
846
1104
  end
847
1105
 
848
1106
  TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
@@ -851,8 +1109,8 @@ module Rodauth
851
1109
 
852
1110
  def validate_oauth_introspect_params
853
1111
  # check if valid token hint type
854
- if param_or_nil("token_type_hint")
855
- redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1112
+ if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1113
+ redirect_response_error("unsupported_token_type")
856
1114
  end
857
1115
 
858
1116
  redirect_response_error("invalid_request") unless param_or_nil("token")
@@ -870,18 +1128,12 @@ module Rodauth
870
1128
  }
871
1129
  end
872
1130
 
873
- def before_introspect; end
874
-
875
1131
  # Token revocation
876
1132
 
877
- def before_revoke
878
- require_oauth_application
879
- end
880
-
881
1133
  def validate_oauth_revoke_params
882
1134
  # check if valid token hint type
883
- if param_or_nil("token_type_hint")
884
- redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1135
+ if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1136
+ redirect_response_error("unsupported_token_type")
885
1137
  end
886
1138
 
887
1139
  redirect_response_error("invalid_request") unless param_or_nil("token")
@@ -898,23 +1150,13 @@ module Rodauth
898
1150
 
899
1151
  redirect_response_error("invalid_request") unless oauth_token
900
1152
 
901
- if oauth_application
902
- redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
903
- else
904
- @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
905
- oauth_token[oauth_tokens_oauth_application_id_column]).first
906
- end
1153
+ redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
907
1154
 
908
1155
  update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
909
1156
 
910
1157
  ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
911
1158
 
912
- oauth_token = if ds.supports_returning?(:update)
913
- ds.returning.update(update_params).first
914
- else
915
- ds.update(update_params)
916
- ds.first
917
- end
1159
+ oauth_token = __update_and_return__(ds, update_params)
918
1160
 
919
1161
  oauth_token[oauth_tokens_token_column] = token
920
1162
  oauth_token
@@ -932,7 +1174,13 @@ module Rodauth
932
1174
 
933
1175
  def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
934
1176
  if accepts_json?
935
- throw_json_response_error(invalid_oauth_response_status, error_code)
1177
+ status_code = if respond_to?(:"#{error_code}_response_status")
1178
+ send(:"#{error_code}_response_status")
1179
+ else
1180
+ invalid_oauth_response_status
1181
+ end
1182
+
1183
+ throw_json_response_error(status_code, error_code)
936
1184
  else
937
1185
  redirect_url = URI.parse(redirect_url)
938
1186
  query_params = []
@@ -954,9 +1202,17 @@ module Rodauth
954
1202
  end
955
1203
  end
956
1204
 
957
- def json_response_success(body)
1205
+ def json_response_success(body, cache = false)
958
1206
  response.status = 200
959
1207
  response["Content-Type"] ||= json_response_content_type
1208
+ if cache
1209
+ # defaulting to 1-day for everyone, for now at least
1210
+ max_age = 60 * 60 * 24
1211
+ response["Cache-Control"] = "private, max-age=#{max_age}"
1212
+ else
1213
+ response["Cache-Control"] = "no-store"
1214
+ response["Pragma"] = "no-cache"
1215
+ end
960
1216
  json_payload = _json_response_body(body)
961
1217
  response.write(json_payload)
962
1218
  request.halt
@@ -979,7 +1235,6 @@ module Rodauth
979
1235
  end
980
1236
 
981
1237
  unless method_defined?(:_json_response_body)
982
- # :nocov:
983
1238
  def _json_response_body(hash)
984
1239
  if request.respond_to?(:convert_to_json)
985
1240
  request.send(:convert_to_json, hash)
@@ -987,7 +1242,6 @@ module Rodauth
987
1242
  JSON.dump(hash)
988
1243
  end
989
1244
  end
990
- # :nocov:
991
1245
  end
992
1246
 
993
1247
  def authorization_required
@@ -995,7 +1249,7 @@ module Rodauth
995
1249
  throw_json_response_error(authorization_required_error_status, "invalid_client")
996
1250
  else
997
1251
  set_redirect_error_flash(require_authorization_error_flash)
998
- redirect(oauth_authorize_path)
1252
+ redirect(authorize_path)
999
1253
  end
1000
1254
  end
1001
1255
 
@@ -1075,10 +1329,10 @@ module Rodauth
1075
1329
 
1076
1330
  def oauth_server_metadata_body(path)
1077
1331
  issuer = base_url
1078
- issuer += "/#{path}" if issuer
1332
+ issuer += "/#{path}" if path
1079
1333
 
1080
1334
  responses_supported = %w[code]
1081
- response_modes_supported = %w[query]
1335
+ response_modes_supported = %w[query form_post]
1082
1336
  grant_types_supported = %w[authorization_code]
1083
1337
 
1084
1338
  if use_oauth_implicit_grant_type?
@@ -1086,11 +1340,12 @@ module Rodauth
1086
1340
  response_modes_supported << "fragment"
1087
1341
  grant_types_supported << "implicit"
1088
1342
  end
1343
+
1089
1344
  {
1090
1345
  issuer: issuer,
1091
- authorization_endpoint: oauth_authorize_url,
1092
- token_endpoint: oauth_token_url,
1093
- registration_endpoint: "#{base_url}/#{oauth_applications_path}",
1346
+ authorization_endpoint: authorize_url,
1347
+ token_endpoint: token_url,
1348
+ registration_endpoint: route_url(oauth_applications_path),
1094
1349
  scopes_supported: oauth_application_scopes,
1095
1350
  response_types_supported: responses_supported,
1096
1351
  response_modes_supported: response_modes_supported,
@@ -1100,142 +1355,12 @@ module Rodauth
1100
1355
  ui_locales_supported: oauth_metadata_ui_locales_supported,
1101
1356
  op_policy_uri: oauth_metadata_op_policy_uri,
1102
1357
  op_tos_uri: oauth_metadata_op_tos_uri,
1103
- revocation_endpoint: oauth_revoke_url,
1358
+ revocation_endpoint: revoke_url,
1104
1359
  revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
1105
- introspection_endpoint: oauth_introspect_url,
1360
+ introspection_endpoint: introspect_url,
1106
1361
  introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
1107
1362
  code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
1108
1363
  }
1109
1364
  end
1110
-
1111
- # /oauth-token
1112
- route(:oauth_token) do |r|
1113
- before_token
1114
-
1115
- r.post do
1116
- catch_error do
1117
- validate_oauth_token_params
1118
-
1119
- oauth_token = nil
1120
- transaction do
1121
- oauth_token = create_oauth_token
1122
- end
1123
-
1124
- json_response_success(json_access_token_payload(oauth_token))
1125
- end
1126
-
1127
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
1128
- end
1129
- end
1130
-
1131
- # /oauth-introspect
1132
- route(:oauth_introspect) do |r|
1133
- before_introspect
1134
-
1135
- r.post do
1136
- catch_error do
1137
- validate_oauth_introspect_params
1138
-
1139
- oauth_token = case param("token_type_hint")
1140
- when "access_token"
1141
- oauth_token_by_token(param("token"))
1142
- when "refresh_token"
1143
- oauth_token_by_refresh_token(param("token"))
1144
- else
1145
- oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
1146
- end
1147
-
1148
- if oauth_application
1149
- redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
1150
- elsif oauth_token
1151
- @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
1152
- oauth_token[oauth_tokens_oauth_application_id_column]).first
1153
- end
1154
-
1155
- json_response_success(json_token_introspect_payload(oauth_token))
1156
- end
1157
-
1158
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
1159
- end
1160
- end
1161
-
1162
- # /oauth-revoke
1163
- route(:oauth_revoke) do |r|
1164
- before_revoke
1165
-
1166
- r.post do
1167
- catch_error do
1168
- validate_oauth_revoke_params
1169
-
1170
- oauth_token = nil
1171
- transaction do
1172
- oauth_token = revoke_oauth_token
1173
- after_revoke
1174
- end
1175
-
1176
- if accepts_json?
1177
- json_response_success \
1178
- "token" => oauth_token[oauth_tokens_token_column],
1179
- "refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
1180
- "revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
1181
- else
1182
- set_notice_flash revoke_oauth_token_notice_flash
1183
- redirect request.referer || "/"
1184
- end
1185
- end
1186
-
1187
- redirect_response_error("invalid_request", request.referer || "/")
1188
- end
1189
- end
1190
-
1191
- # /oauth-authorize
1192
- route(:oauth_authorize) do |r|
1193
- require_account
1194
- validate_oauth_grant_params
1195
- try_approval_prompt if use_oauth_access_type? && request.get?
1196
-
1197
- before_authorize
1198
-
1199
- r.get do
1200
- authorize_view
1201
- end
1202
-
1203
- r.post do
1204
- code = nil
1205
- query_params = []
1206
- fragment_params = []
1207
-
1208
- transaction do
1209
- case param("response_type")
1210
- when "token"
1211
- redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
1212
-
1213
- create_params = {
1214
- oauth_tokens_account_id_column => account_id,
1215
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
1216
- oauth_tokens_scopes_column => scopes
1217
- }
1218
- oauth_token = generate_oauth_token(create_params, false)
1219
-
1220
- token_payload = json_access_token_payload(oauth_token)
1221
- fragment_params.replace(token_payload.map { |k, v| "#{k}=#{v}" })
1222
- when "code", "", nil
1223
- code = create_oauth_grant
1224
- query_params << "code=#{code}"
1225
- else
1226
- redirect_response_error("invalid_request")
1227
- end
1228
- after_authorize
1229
- end
1230
-
1231
- redirect_url = URI.parse(redirect_uri)
1232
- query_params << "state=#{param('state')}" if param_or_nil("state")
1233
- query_params << redirect_url.query if redirect_url.query
1234
- redirect_url.query = query_params.join("&") unless query_params.empty?
1235
- redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
1236
-
1237
- redirect(redirect_url.to_s)
1238
- end
1239
- end
1240
1365
  end
1241
1366
  end