jwt_sessions 2.4.1 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +108 -81
- data/lib/jwt_sessions/errors.rb +1 -0
- data/lib/jwt_sessions/refresh_token.rb +4 -2
- data/lib/jwt_sessions/session.rb +8 -8
- data/lib/jwt_sessions/store_adapters.rb +1 -1
- data/lib/jwt_sessions/store_adapters/abstract_store_adapter.rb +2 -1
- data/lib/jwt_sessions/store_adapters/memory_store_adapter.rb +11 -3
- data/lib/jwt_sessions/store_adapters/redis_store_adapter.rb +12 -12
- data/lib/jwt_sessions/token.rb +8 -4
- data/lib/jwt_sessions/version.rb +1 -1
- data/test/units/jwt_sessions/test_refresh_token.rb +16 -0
- data/test/units/jwt_sessions/test_session.rb +14 -2
- data/test/units/jwt_sessions/test_token.rb +4 -4
- metadata +13 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1ec082d39caff5259e52ca85c48a281d0673e7bb64c04d9033b241f99113c726
|
4
|
+
data.tar.gz: 6c8a90de8b2814bfe148d168706efee94f34e84b01412515d172b9807c7883cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 433a7a5c8495505bc568641c9d733efaac8d626236ed3fa5484b3465800fcc1deac65fdb02c4d5c2261d725660fb176baf53d710952a9616296821b6852b1a7c
|
7
|
+
data.tar.gz: 4a5fdfbe4d0e20868544280c5245d8dfbb5a2eafa80940ff30f6572f88afcfd38383123b067e5adf5e9b1e199dd8d908b4660f4521637c789d6a1e06de2b3f1a
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
## 2.5.2 (July 06, 2020)
|
2
|
+
|
3
|
+
Bugfixes:
|
4
|
+
|
5
|
+
- fixed `Using the last argument as keyword parameters is deprecated;` warnings
|
6
|
+
|
7
|
+
## 2.5.1 (April 20, 2020)
|
8
|
+
|
9
|
+
Features:
|
10
|
+
|
11
|
+
- added changelog
|
12
|
+
|
13
|
+
Bugfixes:
|
14
|
+
|
15
|
+
- fixed double exp key in payload
|
16
|
+
|
17
|
+
Support:
|
18
|
+
|
19
|
+
- moved decode error text to a constant within token class
|
20
|
+
|
21
|
+
## 2.5.0 (April 12, 2020)
|
22
|
+
|
23
|
+
Features:
|
24
|
+
|
25
|
+
- added new error class `JWTSessions::Errors::Expired`
|
26
|
+
|
27
|
+
## 2.4.3 (September 19, 2019)
|
28
|
+
|
29
|
+
Bugfixes:
|
30
|
+
|
31
|
+
- fixed lookup for refresh token for namespaced sessions
|
32
|
+
|
33
|
+
Support:
|
34
|
+
|
35
|
+
- updated sqlite to ~> 1.4 in `dummy_api`
|
36
|
+
- added 2.6.3 Ruby to CI
|
data/README.md
CHANGED
@@ -8,50 +8,51 @@ XSS/CSRF safe JWT auth designed for SPA
|
|
8
8
|
|
9
9
|
## Table of Contents
|
10
10
|
|
11
|
-
- [Synopsis](#synopsis)
|
12
|
-
- [Installation](#installation)
|
13
|
-
- [Getting Started](#getting-started)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
- [Configuration](#configuration)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
- [
|
26
|
-
|
27
|
-
|
28
|
-
- [
|
29
|
-
- [
|
30
|
-
- [
|
11
|
+
- [Synopsis](#synopsis)
|
12
|
+
- [Installation](#installation)
|
13
|
+
- [Getting Started](#getting-started)
|
14
|
+
- [Creating a session](#creating-a-session)
|
15
|
+
- [Rails integration](#rails-integration)
|
16
|
+
- [Non-Rails usage](#non-rails-usage)
|
17
|
+
- [Configuration](#configuration)
|
18
|
+
- [Token store](#token-store)
|
19
|
+
- [JWT signature](#jwt-signature)
|
20
|
+
- [Request headers and cookies names](#request-headers-and-cookies-names)
|
21
|
+
- [Expiration time](#expiration-time)
|
22
|
+
- [Exceptions](#exceptions)
|
23
|
+
- [CSRF and cookies](#csrf-and-cookies)
|
24
|
+
- [Refresh with access token](#refresh-with-access-token)
|
25
|
+
- [Refresh token hijack protection](#refresh-token-hijack-protection)
|
26
|
+
- [Flush Sessions](#flush-sessions)
|
27
|
+
- [Sessions namespace](#sessions-namespace)
|
28
|
+
- [Logout](#logout)
|
29
|
+
- [Examples](#examples)
|
30
|
+
- [Contributing](#contributing)
|
31
|
+
- [License](#license)
|
31
32
|
|
32
33
|
## Synopsis
|
33
34
|
|
34
|
-
|
35
|
+
The primary goal of this gem is to provide configurable, manageable, and safe stateful sessions based on JSON Web Tokens.
|
35
36
|
|
36
|
-
The gem stores JWT based sessions on the backend (currently,
|
37
|
+
The gem stores JWT based sessions on the backend (currently, Redis and memory stores are supported), making it possible to manage sessions, reset passwords and logout users in a reliable and secure way.
|
37
38
|
|
38
|
-
It
|
39
|
+
It is designed to be framework agnostic, yet easily integrable, and Rails integration is available out of the box.
|
39
40
|
|
40
|
-
|
41
|
+
The core concept behind `jwt_sessions` is that each session is represented by a pair of tokens: `access` and `refresh`. The session store is used to handle CSRF checks and prevent refresh token hijacking. Both tokens have configurable expiration times but in general the refresh token is supposed to have a longer lifespan than the access token. The access token is used to retrieve secure resources and the refresh token is used to renew the access token once it has expired. The default token store uses Redis.
|
41
42
|
|
42
|
-
All tokens are encoded and decoded by [ruby-jwt](https://github.com/jwt/ruby-jwt) gem
|
43
|
+
All tokens are encoded and decoded by [ruby-jwt](https://github.com/jwt/ruby-jwt) gem. Its reserved claim names are supported and it can configure claim checks and cryptographic signing algorithms supported by it.
|
43
44
|
`jwt_sessions` itself uses `ext` claim and `HS256` signing by default.
|
44
45
|
|
45
46
|
|
46
47
|
## Installation
|
47
48
|
|
48
|
-
Put this line in your Gemfile
|
49
|
+
Put this line in your Gemfile:
|
49
50
|
|
50
51
|
```ruby
|
51
52
|
gem "jwt_sessions"
|
52
53
|
```
|
53
54
|
|
54
|
-
Then run
|
55
|
+
Then run:
|
55
56
|
|
56
57
|
```
|
57
58
|
bundle install
|
@@ -59,18 +60,18 @@ bundle install
|
|
59
60
|
|
60
61
|
## Getting Started
|
61
62
|
|
62
|
-
You should configure an encryption algorithm and specify the encryption key. By default the gem uses `HS256
|
63
|
+
You should configure an encryption algorithm and specify the encryption key. By default the gem uses the `HS256` signing algorithm.
|
63
64
|
|
64
65
|
```ruby
|
65
66
|
JWTSessions.encryption_key = "secret"
|
66
67
|
```
|
67
68
|
|
68
|
-
`Authorization` mixin provides helper methods which are used to retrieve access and refresh tokens from incoming requests and verify CSRF token if needed. It assumes that a token can be found either in a cookie or in a header (cookie and header names are configurable). It tries to retrieve
|
69
|
+
`Authorization` mixin provides helper methods which are used to retrieve the access and refresh tokens from incoming requests and verify the CSRF token if needed. It assumes that a token can be found either in a cookie or in a header (cookie and header names are configurable). It tries to retrieve the token from headers first and then from cookies (CSRF check included) if the header check fails.
|
69
70
|
|
70
71
|
### Creating a session
|
71
72
|
|
72
73
|
Each token contains a payload with custom session info. The payload is a regular Ruby hash. \
|
73
|
-
Usually, it contains user ID or other data which
|
74
|
+
Usually, it contains a user ID or other data which help identify the current user but the payload can be an empty hash as well.
|
74
75
|
|
75
76
|
```ruby
|
76
77
|
> payload = { user_id: user.id }
|
@@ -84,7 +85,7 @@ Generate the session with a custom payload. By default the same payload is sewn
|
|
84
85
|
=> #<JWTSessions::Session:0x00007fbe2cce9ea0...>
|
85
86
|
```
|
86
87
|
|
87
|
-
Sometimes it makes sense to keep different data within the payloads of access and refresh tokens. \
|
88
|
+
Sometimes it makes sense to keep different data within the payloads of the access and refresh tokens. \
|
88
89
|
The access token may contain rich data including user settings, etc., while the appropriate refresh token will include only the bare minimum which will be required to reconstruct a payload for the new access token during refresh.
|
89
90
|
|
90
91
|
```ruby
|
@@ -117,10 +118,10 @@ To perform the refresh do:
|
|
117
118
|
Available `JWTSessions::Session.new` options:
|
118
119
|
|
119
120
|
- **payload**: a hash object with session data which will be included into an access token payload. Default is an empty hash.
|
120
|
-
- **refresh_payload**: a hash object with session data which will be included into a refresh token payload. Default is
|
121
|
-
- **access_claims**: a hash object with [JWT claims](https://github.com/jwt/ruby-jwt#support-for-reserved-claim-names) which will be validated within the access token payload.
|
121
|
+
- **refresh_payload**: a hash object with session data which will be included into a refresh token payload. Default is the value of the access payload.
|
122
|
+
- **access_claims**: a hash object with [JWT claims](https://github.com/jwt/ruby-jwt#support-for-reserved-claim-names) which will be validated within the access token payload. For example, `{ aud: ["admin"], verify_aud: true }` means that the token can be used only by "admin" audience. Also, the endpoint can automatically validate claims instead. See `token_claims` method.
|
122
123
|
- **refresh_claims**: a hash object with [JWT claims](https://github.com/jwt/ruby-jwt#support-for-reserved-claim-names) which will be validated within the refresh token payload.
|
123
|
-
- **namespace**: a string object which helps to group sessions by a custom criteria. For example, sessions can be grouped by user ID,
|
124
|
+
- **namespace**: a string object which helps to group sessions by a custom criteria. For example, sessions can be grouped by user ID, making it possible to logout the user from all devices. More info [Sessions Namespace](#sessions-namespace).
|
124
125
|
- **refresh_by_access_allowed**: a boolean value. Default is false. It links access and refresh tokens (adds refresh token ID to access payload), making it possible to perform a session refresh by the last expired access token. See [Refresh with access token](#refresh-with-access-token).
|
125
126
|
- **access_exp**: an integer value. Contains an access token expiration time in seconds. The value overrides global settings. See [Expiration time](#expiration-time).
|
126
127
|
- **refresh_exp**: an integer value. Contains a refresh token expiration time in seconds. The value overrides global settings. See [Expiration time](#expiration-time).
|
@@ -132,11 +133,11 @@ Helper methods within `Authorization` mixin:
|
|
132
133
|
- **found_token**: a raw token found within the request.
|
133
134
|
- **payload**: a decoded token's payload.
|
134
135
|
- **claimless_payload**: a decoded token's payload without claims validation (can be used for checking data of an expired token).
|
135
|
-
- **token_claims**: the method should be defined by a developer
|
136
|
+
- **token_claims**: the method should be defined by a developer and is expected to return a hash-like object with claims to be validated within a token's payload.
|
136
137
|
|
137
138
|
### Rails integration
|
138
139
|
|
139
|
-
Include `JWTSessions::RailsAuthorization` in your controllers
|
140
|
+
Include `JWTSessions::RailsAuthorization` in your controllers and add `JWTSessions::Errors::Unauthorized` exception handling if needed.
|
140
141
|
|
141
142
|
```ruby
|
142
143
|
class ApplicationController < ActionController::API
|
@@ -152,14 +153,14 @@ end
|
|
152
153
|
```
|
153
154
|
|
154
155
|
Specify an encryption key for JSON Web Tokens in `config/initializers/jwt_session.rb` \
|
155
|
-
It
|
156
|
+
It is advisable to store the key itself within the app secrets.
|
156
157
|
|
157
158
|
```ruby
|
158
159
|
JWTSessions.algorithm = "HS256"
|
159
160
|
JWTSessions.encryption_key = Rails.application.secrets.secret_jwt_encryption_key
|
160
161
|
```
|
161
162
|
|
162
|
-
Most of the encryption algorithms require private and public keys to sign a token,
|
163
|
+
Most of the encryption algorithms require private and public keys to sign a token. However, HMAC requires only a single key and you can use the `encryption_key` shortcut to sign the token. For other algorithms you must specify private and public keys separately.
|
163
164
|
|
164
165
|
```ruby
|
165
166
|
JWTSessions.algorithm = "RS256"
|
@@ -167,10 +168,11 @@ JWTSessions.private_key = OpenSSL::PKey::RSA.generate(2048)
|
|
167
168
|
JWTSessions.public_key = JWTSessions.private_key.public_key
|
168
169
|
```
|
169
170
|
|
170
|
-
You can build login controller to receive access, refresh and
|
171
|
-
Refresh controller
|
172
|
-
|
173
|
-
|
171
|
+
You can build a login controller to receive access, refresh and CSRF tokens in exchange for the user's login/password. \
|
172
|
+
Refresh controller allows you to get a new access token using the refresh token after access is expired. \
|
173
|
+
|
174
|
+
Here is an example of a simple login controller, which returns a set of tokens as a plain JSON response. \
|
175
|
+
It is also possible to set tokens as cookies in the response instead.
|
174
176
|
|
175
177
|
```ruby
|
176
178
|
class LoginController < ApplicationController
|
@@ -187,7 +189,7 @@ class LoginController < ApplicationController
|
|
187
189
|
end
|
188
190
|
```
|
189
191
|
|
190
|
-
Now you can build a refresh endpoint. To protect the endpoint use before_action `authorize_refresh_request!`. \
|
192
|
+
Now you can build a refresh endpoint. To protect the endpoint use the before_action `authorize_refresh_request!`. \
|
191
193
|
The endpoint itself should return a renewed access token.
|
192
194
|
|
193
195
|
```ruby
|
@@ -205,15 +207,16 @@ class RefreshController < ApplicationController
|
|
205
207
|
end
|
206
208
|
end
|
207
209
|
```
|
208
|
-
|
209
|
-
|
210
|
+
|
211
|
+
In the above example, `found_token` is a token fetched from request headers or cookies. In the context of `RefreshController` it is a refresh token. \
|
212
|
+
The refresh request with headers must include `X-Refresh-Token` (header name is configurable) with the refresh token.
|
210
213
|
|
211
214
|
```
|
212
215
|
X-Refresh-Token: eyJhbGciOiJIUzI1NiJ9...
|
213
216
|
POST /refresh
|
214
217
|
```
|
215
218
|
|
216
|
-
|
219
|
+
When there are login and refresh endpoints, you can protect the rest of your secured controllers with `before_action :authorize_access_request!`.
|
217
220
|
|
218
221
|
```ruby
|
219
222
|
class UsersController < ApplicationController
|
@@ -228,6 +231,7 @@ class UsersController < ApplicationController
|
|
228
231
|
end
|
229
232
|
end
|
230
233
|
```
|
234
|
+
|
231
235
|
Headers must include `Authorization: Bearer` with access token.
|
232
236
|
|
233
237
|
```
|
@@ -244,7 +248,7 @@ end
|
|
244
248
|
```
|
245
249
|
|
246
250
|
Methods `authorize_refresh_request!` and `authorize_access_request!` will always try to fetch the tokens from the headers first and then from the cookies.
|
247
|
-
For the cases when an endpoint must support only one specific token transport the
|
251
|
+
For the cases when an endpoint must support only one specific token transport the following authorization methods can be used instead:
|
248
252
|
|
249
253
|
```ruby
|
250
254
|
authorize_by_access_cookie!
|
@@ -255,7 +259,7 @@ authorize_by_refresh_header!
|
|
255
259
|
|
256
260
|
### Non-Rails usage
|
257
261
|
|
258
|
-
You must include `JWTSessions::Authorization` module to your auth class and
|
262
|
+
You must include `JWTSessions::Authorization` module to your auth class and within it implement the following methods:
|
259
263
|
|
260
264
|
1. request_headers
|
261
265
|
|
@@ -282,7 +286,7 @@ end
|
|
282
286
|
```
|
283
287
|
|
284
288
|
Example Sinatra app. \
|
285
|
-
NOTE:
|
289
|
+
NOTE: Rack updates HTTP headers by using the `HTTP_` prefix, upcasing and underscores for the sake of simplicity. JWTSessions token header names are converted to the rack-style in this example.
|
286
290
|
|
287
291
|
```ruby
|
288
292
|
require "sinatra/base"
|
@@ -344,9 +348,9 @@ List of configurable settings with their default values.
|
|
344
348
|
|
345
349
|
##### Token store
|
346
350
|
|
347
|
-
In order to configure token store you should set up a store adapter in a following way: `JWTSessions.token_store = :redis, { redis_url: 'redis://127.0.0.1:6379/0' }` (options can be omitted). Currently supported stores are `:redis` and `:memory`. Please note, that if you want to use Redis as a store then you should have `redis` gem listed in your Gemfile. If you
|
351
|
+
In order to configure a token store you should set up a store adapter in a following way: `JWTSessions.token_store = :redis, { redis_url: 'redis://127.0.0.1:6379/0' }` (options can be omitted). Currently supported stores are `:redis` and `:memory`. Please note, that if you want to use Redis as a store then you should have `redis` gem listed in your Gemfile. If you do not configure the adapter explicitly, this gem will try to load `redis` and use it. Otherwise it will fall back to a `memory` adapter.
|
348
352
|
|
349
|
-
Memory store accepts
|
353
|
+
Memory store only accepts a `prefix` (used for Redis db keys). Here is a default configuration for Redis:
|
350
354
|
|
351
355
|
```ruby
|
352
356
|
JWTSessions.token_store = :redis, {
|
@@ -371,7 +375,7 @@ JWTSessions.token_store = :redis, { redis_url: "redis://localhost:6397" }
|
|
371
375
|
JWTSessions.algorithm = "HS256"
|
372
376
|
```
|
373
377
|
|
374
|
-
You need to specify a secret to use for HMAC
|
378
|
+
You need to specify a secret to use for HMAC as this setting does not have a default value.
|
375
379
|
|
376
380
|
```ruby
|
377
381
|
JWTSessions.encryption_key = "secret"
|
@@ -384,9 +388,9 @@ JWTSessions.private_key = "abcd"
|
|
384
388
|
JWTSessions.public_key = "efjh"
|
385
389
|
```
|
386
390
|
|
387
|
-
NOTE: ED25519 and HS512256 require rbnacl installation in order to make it work.
|
391
|
+
NOTE: ED25519 and HS512256 require `rbnacl` installation in order to make it work.
|
388
392
|
|
389
|
-
jwt_sessions only uses `exp` claim by default when it decodes tokens
|
393
|
+
jwt_sessions only uses `exp` claim by default when it decodes tokens and you can specify which additional claims to use by
|
390
394
|
setting `jwt_options`. You can also specify leeway to account for clock skew.
|
391
395
|
|
392
396
|
```ruby
|
@@ -413,11 +417,11 @@ class UsersController < ApplicationController
|
|
413
417
|
end
|
414
418
|
```
|
415
419
|
|
416
|
-
Claims are also supported by `JWTSessions::Session
|
420
|
+
Claims are also supported by `JWTSessions::Session` and you can pass `access_claims` and `refresh_claims` options in the initializer.
|
417
421
|
|
418
422
|
##### Request headers and cookies names
|
419
423
|
|
420
|
-
Default request headers/cookies names can be
|
424
|
+
Default request headers/cookies names can be reconfigured.
|
421
425
|
|
422
426
|
```ruby
|
423
427
|
JWTSessions.access_header = "Authorization"
|
@@ -429,19 +433,28 @@ JWTSessions.csrf_header = "X-CSRF-Token"
|
|
429
433
|
|
430
434
|
##### Expiration time
|
431
435
|
|
432
|
-
Access token must have a short life span, while refresh tokens can be stored for a longer time period
|
436
|
+
Access token must have a short life span, while refresh tokens can be stored for a longer time period.
|
433
437
|
|
434
438
|
```ruby
|
435
439
|
JWTSessions.access_exp_time = 3600 # 1 hour in seconds
|
436
440
|
JWTSessions.refresh_exp_time = 604800 # 1 week in seconds
|
437
441
|
```
|
438
442
|
|
439
|
-
It
|
443
|
+
It is defined globally, but can be overridden on a session level. See `JWTSessions::Session.new` options for more info.
|
444
|
+
|
445
|
+
##### Exceptions
|
446
|
+
|
447
|
+
`JWTSessions::Errors::Error` - base class, all possible exceptions are inhereted from it. \
|
448
|
+
`JWTSessions::Errors::Malconfigured` - some required gem settings are empty, or methods are not implemented. \
|
449
|
+
`JWTSessions::Errors::InvalidPayload` - token's payload doesn't contain required keys or they are invalid. \
|
450
|
+
`JWTSessions::Errors::Unauthorized` - token can't be decoded or JWT claims are invalid. \
|
451
|
+
`JWTSessions::Errors::ClaimsVerification` - JWT claims are invalid (inherited from `JWTSessions::Errors::Unauthorized`). \
|
452
|
+
`JWTSessions::Errors::Expired` - token is expired (inherited from `JWTSessions::Errors::ClaimsVerification`).
|
440
453
|
|
441
454
|
#### CSRF and cookies
|
442
455
|
|
443
|
-
|
444
|
-
Although you
|
456
|
+
When you use cookies as your tokens transport it becomes vulnerable to CSRF. That is why both the login and refresh methods of the `Session` class produce CSRF tokens for you. `Authorization` mixin expects that this token is sent with all requests except GET and HEAD in a header specified among this gem's settings (`X-CSRF-Token` by default). Verification will be done automatically and the `Authorization` exception will be raised in case of a mismatch between the token from the header and the one stored in the session. \
|
457
|
+
Although you do not need to mitigate BREACH attacks it is still possible to generate a new masked token with the access token.
|
445
458
|
|
446
459
|
```ruby
|
447
460
|
session = JWTSessions::Session.new
|
@@ -450,10 +463,11 @@ session.masked_csrf(access_token)
|
|
450
463
|
|
451
464
|
##### Refresh with access token
|
452
465
|
|
453
|
-
Sometimes it
|
454
|
-
|
455
|
-
Session accepts `refresh_by_access_allowed: true` setting, which links the access token to the
|
456
|
-
|
466
|
+
Sometimes it is not secure enough to store the refresh tokens in web / JS clients. \
|
467
|
+
This is why you have the option to only use an access token and to not pass the refresh token to the client at all. \
|
468
|
+
Session accepts `refresh_by_access_allowed: true` setting, which links the access token to the corresponding refresh token.
|
469
|
+
|
470
|
+
Example Rails login controller, which passes an access token token via cookies and renders CSRF:
|
457
471
|
|
458
472
|
```ruby
|
459
473
|
class LoginController < ApplicationController
|
@@ -477,17 +491,29 @@ class LoginController < ApplicationController
|
|
477
491
|
end
|
478
492
|
```
|
479
493
|
|
480
|
-
The gem provides
|
494
|
+
The gem provides the ability to refresh the session by access token.
|
481
495
|
|
482
496
|
```ruby
|
483
497
|
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
|
484
498
|
tokens = session.refresh_by_access_payload
|
485
499
|
```
|
486
500
|
|
487
|
-
In case of token forgery and successful refresh performed by an
|
488
|
-
To protect the endpoint use before_action `authorize_refresh_by_access_request!`. \
|
489
|
-
|
490
|
-
|
501
|
+
In case of token forgery and successful refresh performed by an attacker the original user will have to logout. \
|
502
|
+
To protect the endpoint use the before_action `authorize_refresh_by_access_request!`. \
|
503
|
+
Refresh should be performed once the access token is already expired and we need to use the `claimless_payload` method in order to skip JWT expiration validation (and other claims) in order to proceed.
|
504
|
+
|
505
|
+
Optionally `refresh_by_access_payload` accepts a block argument (the same way `refresh` method does).
|
506
|
+
The block will be called if the refresh action is performed before the access token is expired.
|
507
|
+
Thereby it's possible to prohibit users from making refresh calls while their access token is still active.
|
508
|
+
|
509
|
+
```ruby
|
510
|
+
tokens = session.refresh_by_access_payload do
|
511
|
+
# here goes malicious activity alert
|
512
|
+
raise JWTSessions::Errors::Unauthorized, "Refresh action is performed before the expiration of the access token."
|
513
|
+
end
|
514
|
+
```
|
515
|
+
|
516
|
+
Example Rails refresh by access controller with cookies as token transport:
|
491
517
|
|
492
518
|
```ruby
|
493
519
|
class RefreshController < ApplicationController
|
@@ -507,7 +533,7 @@ end
|
|
507
533
|
|
508
534
|
```
|
509
535
|
|
510
|
-
For the cases when an endpoint must support only one specific token transport the
|
536
|
+
For the cases when an endpoint must support only one specific token transport the following auth methods can be used instead:
|
511
537
|
|
512
538
|
```ruby
|
513
539
|
authorize_refresh_by_access_cookie!
|
@@ -516,8 +542,8 @@ authorize_refresh_by_access_header!
|
|
516
542
|
|
517
543
|
#### Refresh token hijack protection
|
518
544
|
|
519
|
-
There is a security recommendation regarding the usage of refresh tokens: only perform refresh when an access token
|
520
|
-
|
545
|
+
There is a security recommendation regarding the usage of refresh tokens: only perform refresh when an access token expires. \
|
546
|
+
Sessions are always defined by a pair of tokens and there cannot be multiple access tokens for a single refresh token. Simultaneous usage of the refresh token by multiple users can be easily noticed as refresh will be performed before the expiration of the access token by one of the users. As a result, `refresh` method of the `Session` class supports an optional block as one of its arguments which will be executed only in case of refresh being performed before the expiration of the access token.
|
521
547
|
|
522
548
|
```ruby
|
523
549
|
session = JwtSessions::Session.new(payload: payload)
|
@@ -526,7 +552,7 @@ session.refresh(refresh_token) { |refresh_token_uid, access_token_expiration| ..
|
|
526
552
|
|
527
553
|
## Flush Sessions
|
528
554
|
|
529
|
-
Flush a session by its refresh token. The method returns number of flushed sessions
|
555
|
+
Flush a session by its refresh token. The method returns number of flushed sessions:
|
530
556
|
|
531
557
|
```ruby
|
532
558
|
session = JWTSessions::Session.new
|
@@ -534,7 +560,7 @@ tokens = session.login
|
|
534
560
|
session.flush_by_token(tokens[:refresh]) # => 1
|
535
561
|
```
|
536
562
|
|
537
|
-
Flush a session by its access token
|
563
|
+
Flush a session by its access token:
|
538
564
|
|
539
565
|
```ruby
|
540
566
|
session = JWTSessions::Session.new(refresh_by_access_allowed: true)
|
@@ -545,7 +571,7 @@ session = JWTSessions::Session.new(refresh_by_access_allowed: true, payload: pay
|
|
545
571
|
session.flush_by_access_payload
|
546
572
|
```
|
547
573
|
|
548
|
-
Or by refresh token UID
|
574
|
+
Or by refresh token UID:
|
549
575
|
|
550
576
|
```ruby
|
551
577
|
session.flush_by_uid(uid) # => 1
|
@@ -553,27 +579,28 @@ session.flush_by_uid(uid) # => 1
|
|
553
579
|
|
554
580
|
##### Sessions namespace
|
555
581
|
|
556
|
-
It's possible to group sessions by custom namespaces
|
582
|
+
It's possible to group sessions by custom namespaces:
|
557
583
|
|
558
584
|
```ruby
|
559
585
|
session = JWTSessions::Session.new(namespace: "account-1")
|
560
586
|
```
|
561
587
|
|
562
|
-
|
588
|
+
Selectively flush sessions by namespace:
|
563
589
|
|
564
590
|
```ruby
|
565
591
|
session = JWTSessions::Session.new(namespace: "ie-sessions")
|
566
592
|
session.flush_namespaced # will flush all sessions which belong to the same namespace
|
567
593
|
```
|
568
594
|
|
569
|
-
|
595
|
+
Flush access tokens only:
|
570
596
|
|
571
597
|
```ruby
|
572
598
|
session = JWTSessions::Session.new(namespace: "ie-sessions")
|
573
599
|
session.flush_namespaced_access_tokens # will flush all access tokens which belong to the same namespace, but will keep refresh tokens
|
574
600
|
```
|
575
601
|
|
576
|
-
|
602
|
+
Force flush of all app sessions:
|
603
|
+
|
577
604
|
```ruby
|
578
605
|
JWTSessions::Session.flush_all
|
579
606
|
```
|
@@ -583,14 +610,14 @@ JWTSessions::Session.flush_all
|
|
583
610
|
To logout you need to remove both access and refresh tokens from the store. \
|
584
611
|
Flush sessions methods can be used to perform logout. \
|
585
612
|
Refresh token or refresh token UID is required to flush a session. \
|
586
|
-
To logout with an access token `refresh_by_access_allowed`
|
613
|
+
To logout with an access token, `refresh_by_access_allowed` should be set to true on access token creation. If logout by access token is allowed it is recommended to ignore the expiration claim and to allow to logout with the expired access token.
|
587
614
|
|
588
615
|
## Examples
|
589
616
|
|
590
617
|
[Rails API](test/support/dummy_api) \
|
591
618
|
[Sinatra API](test/support/dummy_sinatra_api)
|
592
619
|
|
593
|
-
You can use a mixed approach for the cases when you
|
620
|
+
You can use a mixed approach for the cases when you would like to store an access token in localStorage and refresh token in HTTP-only secure cookies. \
|
594
621
|
Rails controllers setup example:
|
595
622
|
|
596
623
|
```ruby
|
data/lib/jwt_sessions/errors.rb
CHANGED
@@ -41,8 +41,10 @@ module JWTSessions
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
|
45
|
-
|
44
|
+
# first_match should be set to true when
|
45
|
+
# we need to search through the all namespaces
|
46
|
+
def find(uid, store, namespace = nil, first_match: false)
|
47
|
+
token_attrs = store.fetch_refresh(uid, namespace, first_match)
|
46
48
|
raise Errors::Unauthorized, "Refresh token not found" if token_attrs.empty?
|
47
49
|
build_with_token_attrs(store, uid, token_attrs, namespace)
|
48
50
|
end
|
data/lib/jwt_sessions/session.rb
CHANGED
@@ -37,7 +37,7 @@ module JWTSessions
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def session_exists?(token, token_type = :access)
|
40
|
-
send(:"#{token_type}_token_data", token)
|
40
|
+
send(:"#{token_type}_token_data", token, true)
|
41
41
|
true
|
42
42
|
rescue Errors::Unauthorized
|
43
43
|
false
|
@@ -114,7 +114,7 @@ module JWTSessions
|
|
114
114
|
ruid = retrieve_val_from(external_payload, :access, "ruid", "refresh uid")
|
115
115
|
uid = retrieve_val_from(external_payload, :access, "uid", "access uid")
|
116
116
|
|
117
|
-
refresh_token = RefreshToken.find(ruid, JWTSessions.token_store)
|
117
|
+
refresh_token = RefreshToken.find(ruid, JWTSessions.token_store, first_match: true)
|
118
118
|
return false unless uid == refresh_token.access_uid
|
119
119
|
|
120
120
|
CSRFToken.new(refresh_token.csrf).valid_authenticity_token?(external_csrf_token)
|
@@ -142,20 +142,20 @@ module JWTSessions
|
|
142
142
|
end
|
143
143
|
|
144
144
|
def refresh_csrf(refresh_token)
|
145
|
-
refresh_token_instance = refresh_token_data(refresh_token)
|
145
|
+
refresh_token_instance = refresh_token_data(refresh_token, true)
|
146
146
|
CSRFToken.new(refresh_token_instance.csrf)
|
147
147
|
end
|
148
148
|
|
149
|
-
def access_token_data(token)
|
149
|
+
def access_token_data(token, _first_match = false)
|
150
150
|
uid = token_uid(token, :access, @access_claims)
|
151
151
|
data = store.fetch_access(uid)
|
152
152
|
raise Errors::Unauthorized, "Access token not found" if data.empty?
|
153
153
|
data
|
154
154
|
end
|
155
155
|
|
156
|
-
def refresh_token_data(token)
|
156
|
+
def refresh_token_data(token, first_match = false)
|
157
157
|
uid = token_uid(token, :refresh, @refresh_claims)
|
158
|
-
retrieve_refresh_token(uid)
|
158
|
+
retrieve_refresh_token(uid, first_match: first_match)
|
159
159
|
end
|
160
160
|
|
161
161
|
def token_uid(token, type, claims)
|
@@ -177,8 +177,8 @@ module JWTSessions
|
|
177
177
|
val
|
178
178
|
end
|
179
179
|
|
180
|
-
def retrieve_refresh_token(uid)
|
181
|
-
@_refresh = RefreshToken.find(uid, store, namespace)
|
180
|
+
def retrieve_refresh_token(uid, first_match: false)
|
181
|
+
@_refresh = RefreshToken.find(uid, store, namespace, first_match: first_match)
|
182
182
|
end
|
183
183
|
|
184
184
|
def tokens_hash
|
@@ -9,7 +9,7 @@ module JWTSessions
|
|
9
9
|
def self.build_by_name(adapter, options = nil)
|
10
10
|
camelized_adapter = adapter.to_s.split('_').map(&:capitalize).join
|
11
11
|
adapter_class_name = "#{camelized_adapter}StoreAdapter"
|
12
|
-
StoreAdapters.const_get(adapter_class_name).new(options || {})
|
12
|
+
StoreAdapters.const_get(adapter_class_name).new(**(options || {}))
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -5,7 +5,7 @@ module JWTSessions
|
|
5
5
|
class MemoryStoreAdapter < AbstractStoreAdapter
|
6
6
|
attr_reader :storage
|
7
7
|
|
8
|
-
def initialize(options)
|
8
|
+
def initialize(**options)
|
9
9
|
raise ArgumentError, "Memory store doesn't support any options" if options.any?
|
10
10
|
@storage = Hash.new do |h, k|
|
11
11
|
h[k] = Hash.new { |hh, kk| hh[kk] = {} }
|
@@ -22,8 +22,16 @@ module JWTSessions
|
|
22
22
|
storage[""]["access"].store(uid, access_token)
|
23
23
|
end
|
24
24
|
|
25
|
-
def fetch_refresh(uid, namespace)
|
26
|
-
|
25
|
+
def fetch_refresh(uid, namespace, first_match = false)
|
26
|
+
if first_match
|
27
|
+
storage.keys.each do |namespace_key|
|
28
|
+
val = value_if_not_expired(uid, "refresh", namespace_key)
|
29
|
+
return val unless val.empty?
|
30
|
+
end
|
31
|
+
{}
|
32
|
+
else
|
33
|
+
value_if_not_expired(uid, "refresh", namespace.to_s)
|
34
|
+
end
|
27
35
|
end
|
28
36
|
|
29
37
|
def persist_refresh(uid:, access_expiration:, access_uid:, csrf:, expiration:, namespace: "")
|
@@ -12,7 +12,7 @@ module JWTSessions
|
|
12
12
|
|
13
13
|
begin
|
14
14
|
require "redis"
|
15
|
-
@storage = configure_redis_client(options)
|
15
|
+
@storage = configure_redis_client(**options)
|
16
16
|
rescue LoadError => e
|
17
17
|
msg = "Could not load the 'redis' gem, please add it to your gemfile or " \
|
18
18
|
"configure a different adapter (e.g. JWTSessions.store_adapter = :memory)"
|
@@ -31,8 +31,9 @@ module JWTSessions
|
|
31
31
|
storage.expireat(key, expiration)
|
32
32
|
end
|
33
33
|
|
34
|
-
def fetch_refresh(uid, namespace)
|
35
|
-
|
34
|
+
def fetch_refresh(uid, namespace, first_match = false)
|
35
|
+
key = first_match ? first_refresh_key(uid) : full_refresh_key(uid, namespace)
|
36
|
+
values = storage.hmget(key, *REFRESH_KEYS).compact
|
36
37
|
|
37
38
|
return {} if values.length != REFRESH_KEYS.length
|
38
39
|
REFRESH_KEYS.each_with_index.each_with_object({}) { |(key, index), acc| acc[key] = values[index] }
|
@@ -69,7 +70,8 @@ module JWTSessions
|
|
69
70
|
end
|
70
71
|
|
71
72
|
def destroy_refresh(uid, namespace)
|
72
|
-
|
73
|
+
key = full_refresh_key(uid, namespace)
|
74
|
+
storage.del(key)
|
73
75
|
end
|
74
76
|
|
75
77
|
def destroy_access(uid)
|
@@ -107,16 +109,14 @@ module JWTSessions
|
|
107
109
|
"#{prefix}_#{namespace}_refresh_#{uid}"
|
108
110
|
end
|
109
111
|
|
110
|
-
def
|
111
|
-
|
112
|
-
|
113
|
-
else
|
114
|
-
full_refresh_key(uid, namespace)
|
115
|
-
end
|
112
|
+
def first_refresh_key(uid)
|
113
|
+
key = full_refresh_key(uid, "*")
|
114
|
+
(storage.keys(key) || []).first
|
116
115
|
end
|
117
116
|
|
118
|
-
def
|
119
|
-
|
117
|
+
def refresh_key(uid, namespace)
|
118
|
+
namespace = "*" if namespace.to_s.empty?
|
119
|
+
full_refresh_key(uid, namespace)
|
120
120
|
end
|
121
121
|
|
122
122
|
def access_key(uid)
|
data/lib/jwt_sessions/token.rb
CHANGED
@@ -4,6 +4,8 @@ require "jwt"
|
|
4
4
|
|
5
5
|
module JWTSessions
|
6
6
|
class Token
|
7
|
+
DECODE_ERROR = "cannot decode the token"
|
8
|
+
|
7
9
|
class << self
|
8
10
|
def encode(payload)
|
9
11
|
exp_payload = meta.merge(payload)
|
@@ -13,23 +15,25 @@ module JWTSessions
|
|
13
15
|
def decode(token, claims = {})
|
14
16
|
decode_options = { algorithm: JWTSessions.algorithm }.merge(JWTSessions.jwt_options.to_h).merge(claims)
|
15
17
|
JWT.decode(token, JWTSessions.public_key, JWTSessions.validate?, decode_options)
|
16
|
-
rescue JWT::
|
18
|
+
rescue JWT::ExpiredSignature => e
|
19
|
+
raise Errors::Expired, e.message
|
20
|
+
rescue JWT::InvalidIssuerError, JWT::InvalidIatError, JWT::InvalidAudError, JWT::InvalidSubError, JWT::InvalidJtiError => e
|
17
21
|
raise Errors::ClaimsVerification, e.message
|
18
22
|
rescue JWT::DecodeError => e
|
19
23
|
raise Errors::Unauthorized, e.message
|
20
24
|
rescue StandardError
|
21
|
-
raise Errors::Unauthorized,
|
25
|
+
raise Errors::Unauthorized, DECODE_ERROR
|
22
26
|
end
|
23
27
|
|
24
28
|
def decode!(token)
|
25
29
|
decode_options = { algorithm: JWTSessions.algorithm }
|
26
30
|
JWT.decode(token, JWTSessions.public_key, false, decode_options)
|
27
31
|
rescue StandardError
|
28
|
-
raise Errors::Unauthorized,
|
32
|
+
raise Errors::Unauthorized, DECODE_ERROR
|
29
33
|
end
|
30
34
|
|
31
35
|
def meta
|
32
|
-
{ exp
|
36
|
+
{ "exp" => JWTSessions.access_expiration }
|
33
37
|
end
|
34
38
|
end
|
35
39
|
end
|
data/lib/jwt_sessions/version.rb
CHANGED
@@ -7,6 +7,8 @@ class TestRefreshToken < Minitest::Test
|
|
7
7
|
attr_reader :csrf, :token, :access_uid
|
8
8
|
|
9
9
|
def setup
|
10
|
+
JWTSessions::Session.flush_all
|
11
|
+
|
10
12
|
JWTSessions.encryption_key = "secure encryption"
|
11
13
|
@access_uid = SecureRandom.uuid
|
12
14
|
@csrf = JWTSessions::CSRFToken.new
|
@@ -39,4 +41,18 @@ class TestRefreshToken < Minitest::Test
|
|
39
41
|
JWTSessions::RefreshToken.find(token.uid, JWTSessions.token_store, nil)
|
40
42
|
end
|
41
43
|
end
|
44
|
+
|
45
|
+
def test_all
|
46
|
+
access_uid_2 = SecureRandom.uuid
|
47
|
+
csrf_2 = JWTSessions::CSRFToken.new
|
48
|
+
token_2 = JWTSessions::RefreshToken.create(
|
49
|
+
csrf_2.encoded,
|
50
|
+
access_uid_2,
|
51
|
+
JWTSessions.access_expiration - 5,
|
52
|
+
JWTSessions.token_store,
|
53
|
+
{},
|
54
|
+
nil
|
55
|
+
)
|
56
|
+
assert_equal [token.token, token_2.token].sort, JWTSessions::RefreshToken.all(nil, JWTSessions.token_store).map(&:token).sort
|
57
|
+
end
|
42
58
|
end
|
@@ -69,6 +69,18 @@ class TestSession < Minitest::Test
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
+
def test_refresh_with_namespace
|
73
|
+
@new_session = JWTSessions::Session.new(
|
74
|
+
payload: payload,
|
75
|
+
namespace: "custom-namespace"
|
76
|
+
)
|
77
|
+
new_tokens = @new_session.login
|
78
|
+
refreshed_tokens = @new_session.refresh(new_tokens[:refresh])
|
79
|
+
decoded_access = JWTSessions::Token.decode(refreshed_tokens[:access]).first
|
80
|
+
assert_equal REFRESH_KEYS, refreshed_tokens.keys.sort
|
81
|
+
assert_equal payload[:test], decoded_access["test"]
|
82
|
+
end
|
83
|
+
|
72
84
|
def test_refresh_by_access_payload
|
73
85
|
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
|
74
86
|
session.login
|
@@ -296,7 +308,7 @@ class TestSession < Minitest::Test
|
|
296
308
|
|
297
309
|
session.flush_namespaced_access_tokens
|
298
310
|
ruid = session.instance_variable_get(:"@_refresh").uid
|
299
|
-
refresh_token = JWTSessions::RefreshToken.find(ruid, JWTSessions.token_store,
|
311
|
+
refresh_token = JWTSessions::RefreshToken.find(ruid, JWTSessions.token_store, namespace)
|
300
312
|
assert_equal "", refresh_token.access_uid
|
301
313
|
assert_equal "", refresh_token.access_expiration
|
302
314
|
|
@@ -306,7 +318,7 @@ class TestSession < Minitest::Test
|
|
306
318
|
end
|
307
319
|
auid = session.instance_variable_get(:"@_access").uid
|
308
320
|
access_token = JWTSessions::AccessToken.find(auid, JWTSessions.token_store)
|
309
|
-
refresh_token = JWTSessions::RefreshToken.find(ruid, JWTSessions.token_store,
|
321
|
+
refresh_token = JWTSessions::RefreshToken.find(ruid, JWTSessions.token_store, namespace)
|
310
322
|
|
311
323
|
assert_equal false, access_token.uid.size.zero?
|
312
324
|
assert_equal false, access_token.expiration.size.zero?
|
@@ -110,11 +110,11 @@ class TestToken < Minitest::Test
|
|
110
110
|
def test_token_leeway_decode
|
111
111
|
JWTSessions.encryption_key = "abcdefghijklmnopqrstuvwxyzABCDEF"
|
112
112
|
JWTSessions.jwt_options.leeway = 50
|
113
|
-
token = JWTSessions::Token.encode(payload.merge(exp
|
113
|
+
token = JWTSessions::Token.encode(payload.merge("exp" => Time.now.to_i - 20))
|
114
114
|
decoded = JWTSessions::Token.decode(token).first
|
115
115
|
assert_equal payload["user_id"], decoded["user_id"]
|
116
116
|
assert_equal payload["secret"], decoded["secret"]
|
117
|
-
token = JWTSessions::Token.encode(payload.merge(exp
|
117
|
+
token = JWTSessions::Token.encode(payload.merge("exp" => Time.now.to_i - 100))
|
118
118
|
assert_raises JWTSessions::Errors::Unauthorized do
|
119
119
|
JWTSessions::Token.decode(token)
|
120
120
|
end
|
@@ -141,8 +141,8 @@ class TestToken < Minitest::Test
|
|
141
141
|
end
|
142
142
|
|
143
143
|
def test_payload_exp_time
|
144
|
-
token = JWTSessions::Token.encode(payload.merge(exp
|
145
|
-
assert_raises JWTSessions::Errors::
|
144
|
+
token = JWTSessions::Token.encode(payload.merge("exp" => Time.now.to_i - (3600 * 24)))
|
145
|
+
assert_raises JWTSessions::Errors::Expired do
|
146
146
|
JWTSessions::Token.decode(token)
|
147
147
|
end
|
148
148
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jwt_sessions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yulia Oletskaya
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-07-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jwt
|
@@ -92,6 +92,7 @@ executables: []
|
|
92
92
|
extensions: []
|
93
93
|
extra_rdoc_files: []
|
94
94
|
files:
|
95
|
+
- CHANGELOG.md
|
95
96
|
- LICENSE
|
96
97
|
- README.md
|
97
98
|
- lib/jwt_sessions.rb
|
@@ -120,7 +121,11 @@ files:
|
|
120
121
|
homepage: http://rubygems.org/gems/jwt_sessions
|
121
122
|
licenses:
|
122
123
|
- MIT
|
123
|
-
metadata:
|
124
|
+
metadata:
|
125
|
+
homepage_uri: https://github.com/tuwukee/jwt_sessions
|
126
|
+
changelog_uri: https://github.com/tuwukee/jwt_sessions/blob/master/CHANGELOG.md
|
127
|
+
source_code_uri: https://github.com/tuwukee/jwt_sessions
|
128
|
+
bug_tracker_uri: https://github.com/tuwukee/jwt_sessions/issues
|
124
129
|
post_install_message:
|
125
130
|
rdoc_options: []
|
126
131
|
require_paths:
|
@@ -136,18 +141,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
141
|
- !ruby/object:Gem::Version
|
137
142
|
version: '0'
|
138
143
|
requirements: []
|
139
|
-
|
140
|
-
rubygems_version: 2.7.6
|
144
|
+
rubygems_version: 3.0.3
|
141
145
|
signing_key:
|
142
146
|
specification_version: 4
|
143
147
|
summary: JWT Sessions
|
144
148
|
test_files:
|
145
149
|
- test/units/test_jwt_sessions.rb
|
146
150
|
- test/units/test_token_store.rb
|
147
|
-
- test/units/jwt_sessions/store_adapters/test_memory_store_adapter.rb
|
148
|
-
- test/units/jwt_sessions/store_adapters/test_redis_store_adapter.rb
|
149
|
-
- test/units/jwt_sessions/test_access_token.rb
|
150
151
|
- test/units/jwt_sessions/test_csrf_token.rb
|
152
|
+
- test/units/jwt_sessions/test_access_token.rb
|
153
|
+
- test/units/jwt_sessions/store_adapters/test_redis_store_adapter.rb
|
154
|
+
- test/units/jwt_sessions/store_adapters/test_memory_store_adapter.rb
|
155
|
+
- test/units/jwt_sessions/test_token.rb
|
151
156
|
- test/units/jwt_sessions/test_refresh_token.rb
|
152
157
|
- test/units/jwt_sessions/test_session.rb
|
153
|
-
- test/units/jwt_sessions/test_token.rb
|