rodauth-oauth 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +92 -10
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +12 -0
- data/lib/rodauth/features/oauth.rb +331 -140
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21325edd12e5fc3ded38294dccf9200497df7bf2ecf72e5e90b5faa0f44c76b6
|
4
|
+
data.tar.gz: 5bd3e791aa77e4702763207fd60640db7c42e3c05d764cff5e56fab17df982b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 65614673a89008e8a23fd2f3e14617ea6b66eccc5dfad930f6d0205f591077dfb9132ec9927d3a8dce5e313b9310d024aee9761c5a3e9cc0d9cffe4b3e089851
|
7
|
+
data.tar.gz: 97b00f5c89429b79afe5403e2a6ee792611d6f121f6c6ae72582c15c4654daeebeddb3e7348c539e22e7664e886e00feb67df257ed308544bbfe3a4cb53b194e
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## master
|
4
|
+
|
5
|
+
## 0.0.2
|
6
|
+
|
7
|
+
### Features
|
8
|
+
|
9
|
+
* Implementation of PKCE by OAuth Public Clients (https://tools.ietf.org/html/rfc7636);
|
10
|
+
* Implementation of grants using "access_type" and "approval_prompt" ([similar to what Google OAuth 2.0 API does](https://wiki.scn.sap.com/wiki/display/Security/Access+Google+APIs+using+the+OAuth+2.0+Client+API));
|
11
|
+
|
12
|
+
### Improvements
|
13
|
+
|
14
|
+
* Store token/refresh token hashes in the database, instead of the "plain" tokens;
|
15
|
+
* Client secret hashed by default, and provided by the application owner;
|
16
|
+
|
17
|
+
### Fix
|
18
|
+
|
19
|
+
* usage of client secret for authorizing the generation of tokens, as the spec mandates (and refraining from them when doing PKCE).
|
20
|
+
|
3
21
|
## 0.0.1
|
4
22
|
|
5
23
|
Initial implementation of the Oauth 2.0 framework, with an example app done using roda.
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[![pipeline status](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/commits/master)
|
4
4
|
[![coverage report](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/coverage.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/commits/master)
|
5
5
|
|
6
|
-
This is an extension to the `rodauth` gem which
|
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
|
|
@@ -15,6 +15,7 @@ This gem implements:
|
|
15
15
|
* [Access Token refresh](https://tools.ietf.org/html/rfc6749#section-1.5);
|
16
16
|
* [Token revocation](https://tools.ietf.org/html/rfc7009);
|
17
17
|
* [Implicit grant (off by default)[https://tools.ietf.org/html/rfc6749#section-4.2];
|
18
|
+
* [PKCE](https://tools.ietf.org/html/rfc7636);
|
18
19
|
* Access Type (Token refresh online and offline);
|
19
20
|
* OAuth application and token management dashboards;
|
20
21
|
|
@@ -96,10 +97,9 @@ Generating tokens happens mostly server-to-server, so here's an example using:
|
|
96
97
|
|
97
98
|
```ruby
|
98
99
|
require "httpx"
|
99
|
-
|
100
|
-
response = httpx.with(headers: { "X-your-auth-scheme" => ENV["SERVER_KEY"] })
|
101
|
-
.post("https://auth_server/oauth-token",json: {
|
100
|
+
response = HTTPX.post("https://auth_server/oauth-token",json: {
|
102
101
|
client_id: ENV["OAUTH_CLIENT_ID"],
|
102
|
+
client_secret: ENV["OAUTH_CLIENT_SECRET"],
|
103
103
|
grant_type: "authorization_code",
|
104
104
|
code: "oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"
|
105
105
|
})
|
@@ -111,7 +111,7 @@ puts payload #=> {"token" => "awr23f3h8f9d2h89...", "refresh_token" => "23fkop3k
|
|
111
111
|
##### cURL
|
112
112
|
|
113
113
|
```
|
114
|
-
> curl
|
114
|
+
> curl --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"authorization_code","code":"oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"}' https://auth_server/oauth-token
|
115
115
|
```
|
116
116
|
|
117
117
|
#### Refresh Token
|
@@ -122,10 +122,9 @@ Refreshing expired tokens also happens mostly server-to-server, here's an exampl
|
|
122
122
|
|
123
123
|
```ruby
|
124
124
|
require "httpx"
|
125
|
-
|
126
|
-
response = httpx.with(headers: { "X-your-auth-scheme" => ENV["SERVER_KEY"] })
|
127
|
-
.post("https://auth_server/oauth-token",json: {
|
125
|
+
response = HTTPX.post("https://auth_server/oauth-token",json: {
|
128
126
|
client_id: ENV["OAUTH_CLIENT_ID"],
|
127
|
+
client_secret: ENV["OAUTH_CLIENT_SECRET"],
|
129
128
|
grant_type: "refresh_token",
|
130
129
|
token: "2r89hfef4j9f90d2j2390jf390g"
|
131
130
|
})
|
@@ -137,7 +136,7 @@ puts payload #=> {"token" => "awr23f3h8f9d2h89...", "token_type" => "Bearer" ...
|
|
137
136
|
##### cURL
|
138
137
|
|
139
138
|
```
|
140
|
-
> curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","grant_type":"token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-token
|
139
|
+
> 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
|
141
140
|
```
|
142
141
|
|
143
142
|
#### Revoking tokens
|
@@ -291,14 +290,59 @@ end
|
|
291
290
|
|
292
291
|
In this section, the non-standard features are going to be described in more detail.
|
293
292
|
|
293
|
+
### Token / Secrets Hashing
|
294
|
+
|
295
|
+
Although not human-friendly as passwords, for security reasons, you might not want to store access (and refresh) tokens in the database. If that is the case, You'll have to add the respective hash columns in the table:
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
# in migration
|
299
|
+
String :token_hash, null: false, token: true
|
300
|
+
String :refresh_token_hash, token, true
|
301
|
+
# and you DO NOT NEED the token and refresh_token columns anymore!
|
302
|
+
```
|
303
|
+
|
304
|
+
And declare them in the plugin:
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
plugin :rodauth do
|
308
|
+
enable :oauth
|
309
|
+
oauth_tokens_token_hash_column :token_hash
|
310
|
+
oauth_tokens_token_hash_column :refresh_token_hash
|
311
|
+
```
|
312
|
+
|
313
|
+
#### Client Secret
|
314
|
+
|
315
|
+
By default, it's expected that the "client secret" property from an OAuth application is only known by the owner, and only the hash is stored in the database; this way, the authorization server doesn't know what the client secret is, only the application owner. The provided [OAuth Applications Extensions](#oauth-applications) application form contains a "Client Secret" input field for this reason.
|
316
|
+
|
317
|
+
However, this extension is optional, and you might want to generate the secrets and store them as is. In that case, you'll have to re-define some options:
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
plugin :rodauth do
|
321
|
+
enable :oauth
|
322
|
+
secret_matches? ->(application, secret){ application[:client_secret] == secret }
|
323
|
+
end
|
324
|
+
```
|
325
|
+
|
294
326
|
### Access Type (default: "offline")
|
295
327
|
|
296
328
|
The "access_type" feature allows the authorization server to emit access tokens with no associated refresh token. This means that users with expired access tokens will have to go through the OAuth flow everytime they need a new one.
|
297
329
|
|
298
330
|
In order to enable this option, add "access_type=online" to the query params section of the authorization url.
|
299
331
|
|
300
|
-
|
332
|
+
#### Approval Prompt
|
333
|
+
|
334
|
+
When using "online grants", one can use an extra query param in the URL, "approval_prompt", which when set to "auto", will skip the authorization form (on the other hand, if one wants to force the authorization form for all grants, then you can set it to "force", or don't set it at all, as it's the default).
|
335
|
+
|
336
|
+
This will only work **if there was a previous successful online grant** for the same application, scopes and redirect URI.
|
337
|
+
|
338
|
+
#### DB schema
|
339
|
+
|
340
|
+
the "oauth_grants" table will have to include the "access_type row":
|
301
341
|
|
342
|
+
```ruby
|
343
|
+
# in migration
|
344
|
+
String :access_type, null: false, default: "offline"
|
345
|
+
```
|
302
346
|
|
303
347
|
### Implicit Grant (default: disabled)
|
304
348
|
|
@@ -315,6 +359,44 @@ end
|
|
315
359
|
|
316
360
|
And add "response_type=token" to the query params section of the authorization url.
|
317
361
|
|
362
|
+
### PKCE
|
363
|
+
|
364
|
+
The "Proof Key for Code Exchange by OAuth Public Clients" (aka PKCE) flow, which is **particularly recommended for OAuth integration in mobile apps**, is transparently supported by `rodauth-oauth`, by adding the `code_challenge_method=S256&code_challenge=$YOUR_CODE_CHALLENGE` query params to the authorization url. Once you do that, you'll have to pass the `code_verifier` when generating a token:
|
365
|
+
|
366
|
+
```ruby
|
367
|
+
# with httpx
|
368
|
+
require "httpx"
|
369
|
+
httpx = HTTPX.plugin(:authorization)
|
370
|
+
response = httpx.with(headers: { "X-your-auth-scheme" => ENV["SERVER_KEY"] })
|
371
|
+
.post("https://auth_server/oauth-token",json: {
|
372
|
+
client_id: ENV["OAUTH_CLIENT_ID"],
|
373
|
+
grant_type: "authorization_code",
|
374
|
+
code: "oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as",
|
375
|
+
code_verifier: your_code_verifier_here
|
376
|
+
})
|
377
|
+
response.raise_for_status
|
378
|
+
payload = JSON.parse(response.to_s)
|
379
|
+
puts payload #=> {"token" =
|
380
|
+
```
|
381
|
+
|
382
|
+
By default, the pkce integration sets "S256" as the default challenge method. If you value security, you **should not use plain**. However, if you really need to, you can set it in the `rodauth` plugin:
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
plugin :rodauth do
|
386
|
+
enable :oauth
|
387
|
+
oauth_pkce_challenge_method "plain"
|
388
|
+
end
|
389
|
+
```
|
390
|
+
|
391
|
+
Although PKCE flow is supported out-of-the-box, it's not enforced by default. If you want to, you can force it, thereby forcing clients to generate a challenge:
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
plugin :rodauth do
|
395
|
+
enable :oauth
|
396
|
+
oauth_require_pkce true
|
397
|
+
end
|
398
|
+
```
|
399
|
+
|
318
400
|
## Ruby support policy
|
319
401
|
|
320
402
|
The minimum Ruby version required to run `rodauth-oauth` is 2.3 . Besides that, it should support all rubies that rodauth and roda support.
|
@@ -24,6 +24,12 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
|
|
24
24
|
t.datetime :revoked_at
|
25
25
|
t.string :scopes, null: false
|
26
26
|
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
27
|
+
# for using access_types
|
28
|
+
t.string :access_type, null: false, default: "offline"
|
29
|
+
# uncomment to enable PKCE
|
30
|
+
# t.string :code_challenge
|
31
|
+
# t.string :code_challenge_method
|
32
|
+
|
27
33
|
t.index(%i[oauth_application_id code], unique: true)
|
28
34
|
end
|
29
35
|
|
@@ -37,7 +43,13 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
|
|
37
43
|
t.integer :oauth_application_id
|
38
44
|
t.foreign_key :oauth_applications, column: :oauth_application_id
|
39
45
|
t.string :token, null: false, token: true
|
46
|
+
# uncomment if setting oauth_tokens_token_hash_column
|
47
|
+
# and delete the token column
|
48
|
+
# t.string :token_hash, token: true
|
40
49
|
t.string :refresh_token
|
50
|
+
# uncomment if setting oauth_tokens_refresh_token_hash_column
|
51
|
+
# and delete the refresh_token column
|
52
|
+
# t.string :refresh_token_hash, token: true
|
41
53
|
t.datetime :expires_in, null: false
|
42
54
|
t.datetime :revoked_at
|
43
55
|
t.string :scopes, null: false
|
@@ -14,6 +14,23 @@ module Rodauth
|
|
14
14
|
using(RegexpExtensions)
|
15
15
|
end
|
16
16
|
|
17
|
+
unless String.method_defined?(:delete_suffix!)
|
18
|
+
module SuffixExtensions
|
19
|
+
refine(String) do
|
20
|
+
def delete_suffix!(suffix)
|
21
|
+
suffix = suffix.to_s
|
22
|
+
chomp! if frozen?
|
23
|
+
len = suffix.length
|
24
|
+
return unless len.positive? && index(suffix, -len)
|
25
|
+
|
26
|
+
self[-len..-1] = ""
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
using(SuffixExtensions)
|
32
|
+
end
|
33
|
+
|
17
34
|
SCOPES = %w[profile.read].freeze
|
18
35
|
|
19
36
|
depends :login
|
@@ -51,22 +68,26 @@ module Rodauth
|
|
51
68
|
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
|
52
69
|
auth_value_method :use_oauth_implicit_grant_type, false
|
53
70
|
|
71
|
+
auth_value_method :oauth_require_pkce, false
|
72
|
+
auth_value_method :oauth_pkce_challenge_method, "S256"
|
73
|
+
|
54
74
|
# URL PARAMS
|
55
75
|
|
56
76
|
# Authorize / token
|
57
77
|
%w[
|
58
|
-
grant_type code refresh_token client_id scope
|
78
|
+
grant_type code refresh_token client_id client_secret scope
|
59
79
|
state redirect_uri scopes token_type_hint token
|
60
|
-
access_type response_type
|
80
|
+
access_type approval_prompt response_type
|
81
|
+
code_challenge code_challenge_method code_verifier
|
61
82
|
].each do |param|
|
62
83
|
auth_value_method :"#{param}_param", param
|
63
84
|
end
|
64
85
|
|
65
86
|
# Application
|
66
|
-
APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri].freeze
|
87
|
+
APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri client_secret].freeze
|
67
88
|
auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
|
68
89
|
|
69
|
-
(APPLICATION_REQUIRED_PARAMS + %w[client_id
|
90
|
+
(APPLICATION_REQUIRED_PARAMS + %w[client_id]).each do |param|
|
70
91
|
auth_value_method :"oauth_application_#{param}_param", param
|
71
92
|
end
|
72
93
|
|
@@ -83,6 +104,10 @@ module Rodauth
|
|
83
104
|
auth_value_method :"oauth_tokens_#{column}_column", column
|
84
105
|
end
|
85
106
|
|
107
|
+
# Oauth Token Hash
|
108
|
+
auth_value_method :oauth_tokens_token_hash_column, nil
|
109
|
+
auth_value_method :oauth_tokens_refresh_token_hash_column, nil
|
110
|
+
|
86
111
|
# OAuth Grants
|
87
112
|
auth_value_method :oauth_grants_table, :oauth_grants
|
88
113
|
auth_value_method :oauth_grants_id_column, :id
|
@@ -90,6 +115,7 @@ module Rodauth
|
|
90
115
|
account_id oauth_application_id
|
91
116
|
redirect_uri code scopes access_type
|
92
117
|
expires_in revoked_at
|
118
|
+
code_challenge code_challenge_method
|
93
119
|
].each do |column|
|
94
120
|
auth_value_method :"oauth_grants_#{column}_column", column
|
95
121
|
end
|
@@ -130,8 +156,16 @@ module Rodauth
|
|
130
156
|
auth_value_method :unique_error_message, "is already in use"
|
131
157
|
auth_value_method :null_error_message, "is not filled"
|
132
158
|
|
159
|
+
# PKCE
|
160
|
+
auth_value_method :code_challenge_required_error_code, "invalid_request"
|
161
|
+
auth_value_method :code_challenge_required_message, "code challenge required"
|
162
|
+
auth_value_method :unsupported_transform_algorithm_error_code, "invalid_request"
|
163
|
+
auth_value_method :unsupported_transform_algorithm_message, "transform algorithm not supported"
|
164
|
+
|
133
165
|
auth_value_methods(
|
134
|
-
:oauth_unique_id_generator
|
166
|
+
:oauth_unique_id_generator,
|
167
|
+
:secret_matches?,
|
168
|
+
:secret_hash
|
135
169
|
)
|
136
170
|
|
137
171
|
redirect(:oauth_application) do |id|
|
@@ -178,51 +212,31 @@ module Rodauth
|
|
178
212
|
end
|
179
213
|
|
180
214
|
def state
|
181
|
-
|
182
|
-
|
183
|
-
return unless state && !state.empty?
|
184
|
-
|
185
|
-
state
|
215
|
+
param_or_nil(state_param)
|
186
216
|
end
|
187
217
|
|
188
218
|
def scopes
|
189
|
-
|
190
|
-
|
191
|
-
return [oauth_application_default_scope] unless scopes && !scopes.empty?
|
192
|
-
|
193
|
-
scopes.split(" ")
|
219
|
+
(param_or_nil(scopes_param) || oauth_application_default_scope).split(" ")
|
194
220
|
end
|
195
221
|
|
196
222
|
def client_id
|
197
|
-
|
198
|
-
|
199
|
-
return unless client_id && !client_id.empty?
|
223
|
+
param_or_nil(client_id_param)
|
224
|
+
end
|
200
225
|
|
201
|
-
|
226
|
+
def client_secret
|
227
|
+
param_or_nil(client_secret_param)
|
202
228
|
end
|
203
229
|
|
204
230
|
def redirect_uri
|
205
|
-
|
206
|
-
|
207
|
-
return oauth_application[oauth_applications_redirect_uri_column] unless redirect_uri && !redirect_uri.empty?
|
208
|
-
|
209
|
-
redirect_uri
|
231
|
+
param_or_nil(redirect_uri_param) || oauth_application[oauth_applications_redirect_uri_column]
|
210
232
|
end
|
211
233
|
|
212
234
|
def token_type_hint
|
213
|
-
|
214
|
-
|
215
|
-
return "access_token" unless token_type_hint && !token_type_hint.empty?
|
216
|
-
|
217
|
-
token_type_hint
|
235
|
+
param_or_nil(token_type_hint_param) || "access_token"
|
218
236
|
end
|
219
237
|
|
220
238
|
def token
|
221
|
-
|
222
|
-
|
223
|
-
return unless token && !token.empty?
|
224
|
-
|
225
|
-
token
|
239
|
+
param_or_nil(token_param)
|
226
240
|
end
|
227
241
|
|
228
242
|
def oauth_application
|
@@ -250,10 +264,9 @@ module Rodauth
|
|
250
264
|
# check if there is a token
|
251
265
|
# check if token has not expired
|
252
266
|
# check if token has been revoked
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
.first
|
267
|
+
oauth_token_by_token(token).where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
268
|
+
.where(oauth_tokens_revoked_at_column => nil)
|
269
|
+
.first
|
257
270
|
end
|
258
271
|
end
|
259
272
|
|
@@ -317,8 +330,100 @@ module Rodauth
|
|
317
330
|
|
318
331
|
private
|
319
332
|
|
333
|
+
def secret_matches?(oauth_application, secret)
|
334
|
+
BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret
|
335
|
+
end
|
336
|
+
|
337
|
+
def secret_hash(secret)
|
338
|
+
password_hash(secret)
|
339
|
+
end
|
340
|
+
|
320
341
|
def oauth_unique_id_generator
|
321
|
-
SecureRandom.
|
342
|
+
SecureRandom.hex(32)
|
343
|
+
end
|
344
|
+
|
345
|
+
def generate_token_hash(token)
|
346
|
+
Base64.urlsafe_encode64(Digest::SHA256.digest(token))
|
347
|
+
end
|
348
|
+
|
349
|
+
unless method_defined?(:password_hash)
|
350
|
+
# From login_requirements_base feature
|
351
|
+
if ENV["RACK_ENV"] == "test"
|
352
|
+
def password_hash_cost
|
353
|
+
BCrypt::Engine::MIN_COST
|
354
|
+
end
|
355
|
+
else
|
356
|
+
# :nocov:
|
357
|
+
def password_hash_cost
|
358
|
+
BCrypt::Engine::DEFAULT_COST
|
359
|
+
end
|
360
|
+
# :nocov:
|
361
|
+
end
|
362
|
+
|
363
|
+
def password_hash(password)
|
364
|
+
BCrypt::Password.create(password, cost: password_hash_cost)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
369
|
+
create_params = {
|
370
|
+
oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
|
371
|
+
}.merge(params)
|
372
|
+
|
373
|
+
token = oauth_unique_id_generator
|
374
|
+
refresh_token = nil
|
375
|
+
|
376
|
+
if oauth_tokens_token_hash_column
|
377
|
+
create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
378
|
+
else
|
379
|
+
create_params[oauth_tokens_token_column] = token
|
380
|
+
end
|
381
|
+
|
382
|
+
if should_generate_refresh_token
|
383
|
+
refresh_token = oauth_unique_id_generator
|
384
|
+
|
385
|
+
if oauth_tokens_refresh_token_hash_column
|
386
|
+
create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
387
|
+
else
|
388
|
+
create_params[oauth_tokens_refresh_token_column] = refresh_token
|
389
|
+
end
|
390
|
+
end
|
391
|
+
oauth_token = _generate_oauth_token(create_params)
|
392
|
+
|
393
|
+
oauth_token[oauth_tokens_token_column] = token
|
394
|
+
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
395
|
+
oauth_token
|
396
|
+
end
|
397
|
+
|
398
|
+
def _generate_oauth_token(params = {})
|
399
|
+
ds = db[oauth_tokens_table]
|
400
|
+
|
401
|
+
begin
|
402
|
+
if ds.supports_returning?(:insert)
|
403
|
+
ds.returning.insert(params)
|
404
|
+
else
|
405
|
+
id = ds.insert(params)
|
406
|
+
ds.where(oauth_tokens_id_column => id).first
|
407
|
+
end
|
408
|
+
rescue Sequel::UniqueConstraintViolation
|
409
|
+
retry
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
def oauth_token_by_token(token)
|
414
|
+
if oauth_tokens_token_hash_column
|
415
|
+
db[oauth_tokens_table].where(oauth_tokens_token_hash_column => generate_token_hash(token))
|
416
|
+
else
|
417
|
+
db[oauth_tokens_table].where(oauth_tokens_token_column => token)
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def oauth_token_by_refresh_token(token)
|
422
|
+
if oauth_tokens_refresh_token_hash_column
|
423
|
+
db[oauth_tokens_table].where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
|
424
|
+
else
|
425
|
+
db[oauth_tokens_table].where(oauth_tokens_refresh_token_column => token)
|
426
|
+
end
|
322
427
|
end
|
323
428
|
|
324
429
|
# Oauth Application
|
@@ -363,9 +468,11 @@ module Rodauth
|
|
363
468
|
}
|
364
469
|
|
365
470
|
# set client ID/secret pairs
|
471
|
+
|
366
472
|
create_params.merge! \
|
367
473
|
oauth_applications_client_id_column => oauth_unique_id_generator,
|
368
|
-
oauth_applications_client_secret_column =>
|
474
|
+
oauth_applications_client_secret_column => \
|
475
|
+
secret_hash(oauth_application_params[oauth_application_client_secret_param])
|
369
476
|
|
370
477
|
create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
|
371
478
|
create_params[oauth_applications_scopes_column].join(",")
|
@@ -404,10 +511,31 @@ module Rodauth
|
|
404
511
|
# Authorize
|
405
512
|
|
406
513
|
def validate_oauth_grant_params
|
407
|
-
unless oauth_application && check_valid_redirect_uri? && check_valid_access_type?
|
514
|
+
unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
|
515
|
+
check_valid_approval_prompt? && check_valid_response_type?
|
408
516
|
redirect_response_error("invalid_request")
|
409
517
|
end
|
410
518
|
redirect_response_error("invalid_scope") unless check_valid_scopes?
|
519
|
+
|
520
|
+
validate_pkce_challenge_params
|
521
|
+
end
|
522
|
+
|
523
|
+
def try_approval_prompt
|
524
|
+
approval_prompt = param_or_nil(approval_prompt_param)
|
525
|
+
|
526
|
+
return unless approval_prompt && approval_prompt == "auto"
|
527
|
+
|
528
|
+
return if db[oauth_grants_table].where(
|
529
|
+
oauth_grants_account_id_column => account_id,
|
530
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
531
|
+
oauth_grants_redirect_uri_column => redirect_uri,
|
532
|
+
oauth_grants_scopes_column => scopes.join(","),
|
533
|
+
oauth_grants_access_type_column => "online"
|
534
|
+
).count.zero?
|
535
|
+
|
536
|
+
# if there's a previous oauth grant for the params combo, it means that this user has approved before.
|
537
|
+
|
538
|
+
request.env["REQUEST_METHOD"] = "POST"
|
411
539
|
end
|
412
540
|
|
413
541
|
def create_oauth_grant
|
@@ -420,10 +548,20 @@ module Rodauth
|
|
420
548
|
oauth_grants_scopes_column => scopes.join(",")
|
421
549
|
}
|
422
550
|
|
423
|
-
|
551
|
+
if (access_type = param_or_nil(access_type_param))
|
424
552
|
create_params[oauth_grants_access_type_column] = access_type
|
425
553
|
end
|
426
554
|
|
555
|
+
# PKCE flow
|
556
|
+
if (code_challenge = param_or_nil(code_challenge_param))
|
557
|
+
code_challenge_method = param_or_nil(code_challenge_method_param)
|
558
|
+
|
559
|
+
create_params[oauth_grants_code_challenge_column] = code_challenge
|
560
|
+
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
561
|
+
elsif oauth_require_pkce
|
562
|
+
redirect_response_error("code_challenge_required")
|
563
|
+
end
|
564
|
+
|
427
565
|
ds = db[oauth_grants_table]
|
428
566
|
|
429
567
|
begin
|
@@ -441,60 +579,63 @@ module Rodauth
|
|
441
579
|
# Access Tokens
|
442
580
|
|
443
581
|
def validate_oauth_token_params
|
444
|
-
redirect_response_error("invalid_request") unless
|
582
|
+
redirect_response_error("invalid_request") unless param_or_nil(client_id_param)
|
445
583
|
|
446
|
-
unless (
|
584
|
+
unless param_or_nil(client_secret_param)
|
585
|
+
redirect_response_error("invalid_request") unless param_or_nil(code_verifier_param)
|
586
|
+
end
|
587
|
+
|
588
|
+
unless (grant_type = param_or_nil(grant_type_param))
|
447
589
|
redirect_response_error("invalid_request")
|
448
590
|
end
|
449
591
|
|
450
592
|
case grant_type
|
451
593
|
when "authorization_code"
|
452
|
-
redirect_response_error("invalid_request") unless
|
594
|
+
redirect_response_error("invalid_request") unless param_or_nil(code_param)
|
453
595
|
|
454
596
|
when "refresh_token"
|
455
|
-
redirect_response_error("invalid_request") unless
|
597
|
+
redirect_response_error("invalid_request") unless param_or_nil(refresh_token_param)
|
456
598
|
else
|
457
599
|
redirect_response_error("invalid_request")
|
458
600
|
end
|
459
601
|
end
|
460
602
|
|
461
|
-
def
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
}.merge(params)
|
603
|
+
def create_oauth_token
|
604
|
+
oauth_application = db[oauth_applications_table].where(
|
605
|
+
oauth_applications_client_id_column => param(client_id_param)
|
606
|
+
).first
|
466
607
|
|
467
|
-
|
608
|
+
redirect_response_error("invalid_request") unless oauth_application
|
468
609
|
|
469
|
-
|
470
|
-
|
471
|
-
ds.returning.insert(create_params)
|
472
|
-
else
|
473
|
-
id = ds.insert(create_params)
|
474
|
-
ds.where(oauth_tokens_id_column => id).first
|
475
|
-
end
|
476
|
-
rescue Sequel::UniqueConstraintViolation
|
477
|
-
retry
|
610
|
+
if (client_secret = param_or_nil(client_secret_param))
|
611
|
+
redirect_response_error("invalid_request") unless secret_matches?(oauth_application, client_secret)
|
478
612
|
end
|
479
|
-
end
|
480
613
|
|
481
|
-
def create_oauth_token
|
482
614
|
case param(grant_type_param)
|
483
615
|
when "authorization_code"
|
616
|
+
|
484
617
|
# fetch oauth grant
|
485
618
|
oauth_grant = db[oauth_grants_table].where(
|
486
619
|
oauth_grants_code_column => param(code_param),
|
487
620
|
oauth_grants_redirect_uri_column => param(redirect_uri_param),
|
488
|
-
oauth_grants_oauth_application_id_column =>
|
489
|
-
|
490
|
-
oauth_applications_account_id_column => oauth_applications_account_id_column
|
491
|
-
).select(oauth_applications_id_column)
|
621
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
622
|
+
oauth_grants_revoked_at_column => nil
|
492
623
|
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
493
|
-
.where(oauth_grants_revoked_at_column => nil)
|
494
624
|
.first
|
495
625
|
|
496
626
|
redirect_response_error("invalid_grant") unless oauth_grant
|
497
627
|
|
628
|
+
# PKCE
|
629
|
+
if oauth_grant[oauth_grants_code_challenge_column]
|
630
|
+
code_verifier = param_or_nil(code_verifier_param)
|
631
|
+
|
632
|
+
unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
|
633
|
+
redirect_response_error("invalid_request")
|
634
|
+
end
|
635
|
+
elsif oauth_require_pkce
|
636
|
+
redirect_response_error("code_challenge_required")
|
637
|
+
end
|
638
|
+
|
498
639
|
create_params = {
|
499
640
|
oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
|
500
641
|
oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
|
@@ -502,43 +643,48 @@ module Rodauth
|
|
502
643
|
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
503
644
|
}
|
504
645
|
|
505
|
-
if oauth_grant[oauth_grants_access_type_column] == "offline"
|
506
|
-
create_params[oauth_tokens_refresh_token_column] = oauth_unique_id_generator
|
507
|
-
end
|
508
646
|
# revoke oauth grant
|
509
647
|
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
510
648
|
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
511
649
|
|
512
|
-
generate_oauth_token(create_params)
|
650
|
+
generate_oauth_token(create_params, oauth_grant[oauth_grants_access_type_column] == "offline")
|
651
|
+
|
513
652
|
when "refresh_token"
|
514
|
-
# fetch oauth
|
515
|
-
oauth_token =
|
516
|
-
|
517
|
-
oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
|
518
|
-
oauth_applications_client_id_column => param(client_id_param),
|
519
|
-
oauth_applications_account_id_column => account_id
|
520
|
-
).select(oauth_applications_id_column)
|
653
|
+
# fetch oauth token
|
654
|
+
oauth_token = oauth_token_by_refresh_token(param(refresh_token_param)).where(
|
655
|
+
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column]
|
521
656
|
).where(oauth_grants_revoked_at_column => nil).first
|
522
657
|
|
523
658
|
redirect_response_error("invalid_grant") unless oauth_token
|
524
659
|
|
660
|
+
token = oauth_unique_id_generator
|
661
|
+
|
525
662
|
update_params = {
|
526
663
|
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
527
|
-
oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
|
528
|
-
oauth_tokens_token_column => oauth_unique_id_generator
|
664
|
+
oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
|
529
665
|
}
|
530
666
|
|
667
|
+
if oauth_tokens_token_hash_column
|
668
|
+
update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
669
|
+
else
|
670
|
+
update_params[oauth_tokens_token_column] = token
|
671
|
+
end
|
672
|
+
|
531
673
|
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
532
|
-
|
674
|
+
|
675
|
+
oauth_token = begin
|
533
676
|
if ds.supports_returning?(:update)
|
534
677
|
ds.returning.update(update_params)
|
535
678
|
else
|
536
679
|
ds.update(update_params)
|
537
680
|
ds.first
|
538
681
|
end
|
539
|
-
|
540
|
-
|
682
|
+
rescue Sequel::UniqueConstraintViolation
|
683
|
+
retry
|
541
684
|
end
|
685
|
+
|
686
|
+
oauth_token[oauth_tokens_token_column] = token
|
687
|
+
oauth_token
|
542
688
|
else
|
543
689
|
redirect_response_error("invalid_grant")
|
544
690
|
end
|
@@ -556,39 +702,40 @@ module Rodauth
|
|
556
702
|
end
|
557
703
|
|
558
704
|
def revoke_oauth_token
|
559
|
-
# one can only revoke tokens which haven't been revoked before, and which are
|
560
|
-
# either our tokens, or tokens from applications we own.
|
561
|
-
ds = db[oauth_tokens_table]
|
562
|
-
.where(oauth_tokens_revoked_at_column => nil)
|
563
|
-
.where(
|
564
|
-
Sequel.or(
|
565
|
-
oauth_tokens_account_id_column => account_id,
|
566
|
-
oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
|
567
|
-
oauth_applications_client_id_column => param(client_id_param),
|
568
|
-
oauth_applications_account_id_column => account_id
|
569
|
-
).select(oauth_applications_id_column)
|
570
|
-
)
|
571
|
-
)
|
572
705
|
ds = case token_type_hint
|
573
706
|
when "access_token"
|
574
|
-
|
707
|
+
oauth_token_by_token(token)
|
575
708
|
when "refresh_token"
|
576
|
-
|
709
|
+
oauth_token_by_refresh_token(token)
|
577
710
|
end
|
711
|
+
# one can only revoke tokens which haven't been revoked before, and which are
|
712
|
+
# either our tokens, or tokens from applications we own.
|
713
|
+
oauth_token = ds.where(oauth_tokens_revoked_at_column => nil)
|
714
|
+
.where(
|
715
|
+
Sequel.or(
|
716
|
+
oauth_tokens_account_id_column => account_id,
|
717
|
+
oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
|
718
|
+
oauth_applications_client_id_column => param(client_id_param),
|
719
|
+
oauth_applications_account_id_column => account_id
|
720
|
+
).select(oauth_applications_id_column)
|
721
|
+
)
|
722
|
+
).first
|
578
723
|
|
579
|
-
oauth_token = ds.first
|
580
724
|
redirect_response_error("invalid_request") unless oauth_token
|
581
725
|
|
582
726
|
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
583
727
|
|
584
728
|
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
585
729
|
|
586
|
-
if ds.supports_returning?(:update)
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
730
|
+
oauth_token = if ds.supports_returning?(:update)
|
731
|
+
ds.returning.update(update_params)
|
732
|
+
else
|
733
|
+
ds.update(update_params)
|
734
|
+
ds.first
|
735
|
+
end
|
736
|
+
|
737
|
+
oauth_token[oauth_tokens_token_column] = token
|
738
|
+
oauth_token
|
592
739
|
|
593
740
|
# If the particular
|
594
741
|
# token is a refresh token and the authorization server supports the
|
@@ -606,11 +753,19 @@ module Rodauth
|
|
606
753
|
throw_json_response_error(invalid_oauth_response_status, error_code)
|
607
754
|
else
|
608
755
|
redirect_url = URI.parse(redirect_url)
|
609
|
-
query_params = [
|
756
|
+
query_params = []
|
757
|
+
|
758
|
+
query_params << if respond_to?(:"#{error_code}_error_code")
|
759
|
+
"error=#{send(:"#{error_code}_error_code")}"
|
760
|
+
else
|
761
|
+
"error=#{error_code}"
|
762
|
+
end
|
763
|
+
|
610
764
|
if respond_to?(:"#{error_code}_message")
|
611
765
|
message = send(:"#{error_code}_message")
|
612
766
|
query_params << ["error_description=#{CGI.escape(message)}"]
|
613
767
|
end
|
768
|
+
|
614
769
|
query_params << redirect_url.query if redirect_url.query
|
615
770
|
redirect_url.query = query_params.join("&")
|
616
771
|
redirect(redirect_url.to_s)
|
@@ -619,19 +774,30 @@ module Rodauth
|
|
619
774
|
|
620
775
|
def throw_json_response_error(status, error_code)
|
621
776
|
set_response_error_status(status)
|
622
|
-
|
777
|
+
code = if respond_to?(:"#{error_code}_error_code")
|
778
|
+
send(:"#{error_code}_error_code")
|
779
|
+
else
|
780
|
+
error_code
|
781
|
+
end
|
782
|
+
payload = { "error" => code }
|
623
783
|
payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
|
624
|
-
json_payload =
|
625
|
-
request.send(:convert_to_json, payload)
|
626
|
-
else
|
627
|
-
JSON.dump(payload)
|
628
|
-
end
|
784
|
+
json_payload = _json_response_body(payload)
|
629
785
|
response["Content-Type"] ||= json_response_content_type
|
630
786
|
response["WWW-Authenticate"] = "Bearer" if status == 401
|
631
787
|
response.write(json_payload)
|
632
788
|
request.halt
|
633
789
|
end
|
634
790
|
|
791
|
+
unless method_defined?(:_json_response_body)
|
792
|
+
def _json_response_body(hash)
|
793
|
+
if request.respond_to?(:convert_to_json)
|
794
|
+
request.send(:convert_to_json, hash)
|
795
|
+
else
|
796
|
+
JSON.dump(hash)
|
797
|
+
end
|
798
|
+
end
|
799
|
+
end
|
800
|
+
|
635
801
|
def authorization_required
|
636
802
|
if json_request?
|
637
803
|
throw_json_response_error(authorization_required_error_status, "invalid_client")
|
@@ -654,25 +820,59 @@ module Rodauth
|
|
654
820
|
ACCESS_TYPES = %w[offline online].freeze
|
655
821
|
|
656
822
|
def check_valid_access_type?
|
657
|
-
access_type =
|
658
|
-
access_type
|
823
|
+
access_type = param_or_nil(access_type_param)
|
824
|
+
!access_type || ACCESS_TYPES.include?(access_type)
|
825
|
+
end
|
826
|
+
|
827
|
+
APPROVAL_PROMPTS = %w[force auto].freeze
|
828
|
+
|
829
|
+
def check_valid_approval_prompt?
|
830
|
+
approval_prompt = param_or_nil(approval_prompt_param)
|
831
|
+
!approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
|
659
832
|
end
|
660
833
|
|
661
834
|
def check_valid_response_type?
|
662
|
-
response_type =
|
835
|
+
response_type = param_or_nil(response_type_param)
|
663
836
|
|
664
|
-
return true if response_type.
|
837
|
+
return true if response_type.nil? || response_type == "code"
|
665
838
|
|
666
839
|
return use_oauth_implicit_grant_type if response_type == "token"
|
667
840
|
|
668
841
|
false
|
669
842
|
end
|
670
843
|
|
844
|
+
# PKCE
|
845
|
+
|
846
|
+
def validate_pkce_challenge_params
|
847
|
+
if param_or_nil(code_challenge_param)
|
848
|
+
|
849
|
+
challenge_method = param_or_nil(code_challenge_method_param)
|
850
|
+
redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
|
851
|
+
else
|
852
|
+
return unless oauth_require_pkce
|
853
|
+
|
854
|
+
redirect_response_error("code_challenge_required")
|
855
|
+
end
|
856
|
+
end
|
857
|
+
|
858
|
+
def check_valid_grant_challenge?(grant, verifier)
|
859
|
+
challenge = grant[oauth_grants_code_challenge_column]
|
860
|
+
|
861
|
+
case grant[oauth_grants_code_challenge_method_column]
|
862
|
+
when "plain"
|
863
|
+
challenge == verifier
|
864
|
+
when "S256"
|
865
|
+
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
|
866
|
+
generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
|
867
|
+
|
868
|
+
challenge == generated_challenge
|
869
|
+
else
|
870
|
+
redirect_response_error("unsupported_transform_algorithm")
|
871
|
+
end
|
872
|
+
end
|
873
|
+
|
671
874
|
# /oauth-token
|
672
875
|
route(:oauth_token) do |r|
|
673
|
-
throw_json_response_error(authorization_required_error_status, "invalid_client") unless logged_in?
|
674
|
-
|
675
|
-
# access-token
|
676
876
|
r.post do
|
677
877
|
catch_error do
|
678
878
|
validate_oauth_token_params
|
@@ -687,18 +887,14 @@ module Rodauth
|
|
687
887
|
response.status = 200
|
688
888
|
response["Content-Type"] ||= json_response_content_type
|
689
889
|
json_response = {
|
690
|
-
"token" => oauth_token[
|
890
|
+
"token" => oauth_token[oauth_tokens_token_column],
|
691
891
|
"token_type" => oauth_token_type,
|
692
892
|
"expires_in" => oauth_token_expires_in
|
693
893
|
}
|
694
894
|
|
695
|
-
json_response["refresh_token"] = oauth_token[
|
895
|
+
json_response["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[:refresh_token]
|
696
896
|
|
697
|
-
json_payload =
|
698
|
-
request.send(:convert_to_json, json_response)
|
699
|
-
else
|
700
|
-
JSON.dump(json_response)
|
701
|
-
end
|
897
|
+
json_payload = _json_response_body(json_response)
|
702
898
|
response.write(json_payload)
|
703
899
|
request.halt
|
704
900
|
end
|
@@ -727,15 +923,11 @@ module Rodauth
|
|
727
923
|
response.status = 200
|
728
924
|
response["Content-Type"] ||= json_response_content_type
|
729
925
|
json_response = {
|
730
|
-
"token" => oauth_token[
|
731
|
-
"refresh_token" => oauth_token[
|
732
|
-
"revoked_at" => oauth_token[
|
926
|
+
"token" => oauth_token[oauth_tokens_token_column],
|
927
|
+
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
928
|
+
"revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
|
733
929
|
}
|
734
|
-
json_payload =
|
735
|
-
request.send(:convert_to_json, json_response)
|
736
|
-
else
|
737
|
-
JSON.dump(json_response)
|
738
|
-
end
|
930
|
+
json_payload = _json_response_body(json_response)
|
739
931
|
response.write(json_payload)
|
740
932
|
request.halt
|
741
933
|
else
|
@@ -751,15 +943,14 @@ module Rodauth
|
|
751
943
|
# /oauth-authorize
|
752
944
|
route(:oauth_authorize) do |r|
|
753
945
|
require_account
|
946
|
+
validate_oauth_grant_params
|
947
|
+
try_approval_prompt if request.get?
|
754
948
|
|
755
949
|
r.get do
|
756
|
-
validate_oauth_grant_params
|
757
950
|
authorize_view
|
758
951
|
end
|
759
952
|
|
760
953
|
r.post do
|
761
|
-
validate_oauth_grant_params
|
762
|
-
|
763
954
|
code = nil
|
764
955
|
query_params = []
|
765
956
|
fragment_params = []
|
@@ -775,9 +966,9 @@ module Rodauth
|
|
775
966
|
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
776
967
|
oauth_tokens_scopes_column => scopes
|
777
968
|
}
|
778
|
-
oauth_token = generate_oauth_token(create_params)
|
969
|
+
oauth_token = generate_oauth_token(create_params, false)
|
779
970
|
|
780
|
-
fragment_params << ["access_token=#{oauth_token[
|
971
|
+
fragment_params << ["access_token=#{oauth_token[oauth_tokens_token_column]}"]
|
781
972
|
fragment_params << ["token_type=#{oauth_token_type}"]
|
782
973
|
fragment_params << ["expires_in=#{oauth_token_expires_in}"]
|
783
974
|
when "code", "", nil
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rodauth-oauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Cardoso
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-05-
|
11
|
+
date: 2020-05-29 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Implementation of the OAuth 2.0 protocol on top of rodauth.
|
14
14
|
email:
|