jwt_sessions 2.4.1 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: d60c5dcae06e9f49ee1e99125baab8285156bd54eb5b36a3589001cee55acd6e
4
- data.tar.gz: faa1cf67e08b8a320b525a918260dd4cf61f0aaf5f4ea1160f0e82ffff2e97b6
2
+ SHA1:
3
+ metadata.gz: 603df513aaba9a73e5360895ea7fffddf52dd02e
4
+ data.tar.gz: f70b523406ae364b05d16b45423d3b98971f4c75
5
5
  SHA512:
6
- metadata.gz: eb844adfe82415bc6126f5a32bceeb95234fbe8172ca427dbeaf7535bc4ae745cee3e33207b9e017c72098b8bf08b0d05cc302b952690568e5ca11fe12ad7629
7
- data.tar.gz: 38b0299aa445d3a1558ed66e5a8d805850635065bf17d4de3d4f7f7b6c81a5962cac801bd79ae019209a8df96a4da64592897c6e45355e763b6220f5dfbed1af
6
+ metadata.gz: f937d9d3362f0ec7c63dc4af813038a3a0b49647a29e6f0338583ae106094cc9677bc83790ae330df3ef46d1159d441cab595cb62f274294a1fdc7af23ecb2cb
7
+ data.tar.gz: fd37c7ec5f318893567dfce15ba8562c35a25b0ec8c53c12707947ba0f81e9d973144c0f153364ff1e3a92d17eab58c8a91ec1f4f67f0f84457334c83da38f8e
data/README.md CHANGED
@@ -8,50 +8,50 @@ 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
- * [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
- + [CSRF and cookies](#csrf-and-cookies)
23
- + [Refresh with access token](#refresh-with-access-token)
24
- + [Refresh token hijack protection](#refresh-token-hijack-protection)
25
- - [Flush Sessions](#flush-sessions)
26
- + [Sessions Namespace](#sessions-namespace)
27
- + [Logout](#logout)
28
- - [Examples](#examples)
29
- - [Contributing](#contributing)
30
- - [License](#license)
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
+ - [CSRF and cookies](#csrf-and-cookies)
23
+ - [Refresh with access token](#refresh-with-access-token)
24
+ - [Refresh token hijack protection](#refresh-token-hijack-protection)
25
+ - [Flush Sessions](#flush-sessions)
26
+ - [Sessions namespace](#sessions-namespace)
27
+ - [Logout](#logout)
28
+ - [Examples](#examples)
29
+ - [Contributing](#contributing)
30
+ - [License](#license)
31
31
 
32
32
  ## Synopsis
33
33
 
34
- Main goal of this gem is to provide configurable, manageable, and safe stateful sessions based on JSON Web Tokens.
34
+ The primary goal of this gem is to provide configurable, manageable, and safe stateful sessions based on JSON Web Tokens.
35
35
 
36
- The gem stores JWT based sessions on the backend (currently, redis and memory stores are supported), making it possible to manage sessions, reset passwords, logout users in a reliable and secure way.
36
+ 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
37
 
38
- It's designed to be framework agnostic yet is easily integrable, and Rails integration is available out of the box.
38
+ It is designed to be framework agnostic, yet easily integrable, and Rails integration is available out of the box.
39
39
 
40
- Core concept behind `jwt_sessions` is that each session is represented by a pair of tokens: access and refresh, and a session store is used to handle CSRF checks and refresh token hijacking. Both tokens have configurable expiration times, but in general refresh token is supposed to have a longer lifespan than an access token. Access token is used to retrieve secured resources and refresh token is used to renew the access token once it's expired. Default token store is based on redis.
40
+ 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
41
 
42
- All tokens are encoded and decoded by [ruby-jwt](https://github.com/jwt/ruby-jwt) gem, and its reserved claim names are supported as well as it's allowed to configure claim checks and cryptographic signing algorithms supported by it.
42
+ 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
43
  `jwt_sessions` itself uses `ext` claim and `HS256` signing by default.
44
44
 
45
45
 
46
46
  ## Installation
47
47
 
48
- Put this line in your Gemfile
48
+ Put this line in your Gemfile:
49
49
 
50
50
  ```ruby
51
51
  gem "jwt_sessions"
52
52
  ```
53
53
 
54
- Then run
54
+ Then run:
55
55
 
56
56
  ```
57
57
  bundle install
@@ -59,18 +59,18 @@ bundle install
59
59
 
60
60
  ## Getting Started
61
61
 
62
- You should configure an encryption algorithm and specify the encryption key. By default the gem uses `HS256`.
62
+ You should configure an encryption algorithm and specify the encryption key. By default the gem uses the `HS256` signing algorithm.
63
63
 
64
64
  ```ruby
65
65
  JWTSessions.encryption_key = "secret"
66
66
  ```
67
67
 
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 it from headers first, then from cookies (CSRF check included) if the headers check failed.
68
+ `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
69
 
70
70
  ### Creating a session
71
71
 
72
72
  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 helps to identify current user but it's not necessary, the payload can be an empty hash as well.
73
+ 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
74
 
75
75
  ```ruby
76
76
  > payload = { user_id: user.id }
@@ -84,7 +84,7 @@ Generate the session with a custom payload. By default the same payload is sewn
84
84
  => #<JWTSessions::Session:0x00007fbe2cce9ea0...>
85
85
  ```
86
86
 
87
- Sometimes it makes sense to keep different data within the payloads of access and refresh tokens. \
87
+ Sometimes it makes sense to keep different data within the payloads of the access and refresh tokens. \
88
88
  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
89
 
90
90
  ```ruby
@@ -117,10 +117,10 @@ To perform the refresh do:
117
117
  Available `JWTSessions::Session.new` options:
118
118
 
119
119
  - **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 a value of the access payload.
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. F.e. `{ aud: ["admin"], verify_aud: true }` meaning that the token can be used only by "admin" audience. Also, the endpoint can automatically validate claims instead. See `token_claims` method.
120
+ - **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.
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. 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
122
  - **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, then it'll be possible to logout the user from all devises. More info [Sessions Namespace](#sessions-namespace).
123
+ - **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
124
  - **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
125
  - **access_exp**: an integer value. Contains an access token expiration time in seconds. The value overrides global settings. See [Expiration time](#expiration-time).
126
126
  - **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 +132,11 @@ Helper methods within `Authorization` mixin:
132
132
  - **found_token**: a raw token found within the request.
133
133
  - **payload**: a decoded token's payload.
134
134
  - **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, and is expected to return a hash-like object with claims to be validated within a token's payload.
135
+ - **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
136
 
137
137
  ### Rails integration
138
138
 
139
- Include `JWTSessions::RailsAuthorization` in your controllers, add `JWTSessions::Errors::Unauthorized` exceptions handling if needed.
139
+ Include `JWTSessions::RailsAuthorization` in your controllers and add `JWTSessions::Errors::Unauthorized` exception handling if needed.
140
140
 
141
141
  ```ruby
142
142
  class ApplicationController < ActionController::API
@@ -152,14 +152,14 @@ end
152
152
  ```
153
153
 
154
154
  Specify an encryption key for JSON Web Tokens in `config/initializers/jwt_session.rb` \
155
- It's adviced to store the key itself within the app secrets.
155
+ It is advisable to store the key itself within the app secrets.
156
156
 
157
157
  ```ruby
158
158
  JWTSessions.algorithm = "HS256"
159
159
  JWTSessions.encryption_key = Rails.application.secrets.secret_jwt_encryption_key
160
160
  ```
161
161
 
162
- Most of the encryption algorithms require private and public keys to sign a token, yet HMAC only require a single key, so you can use a shortcat `encryption_key` to sign the token. For other algorithms you must specify a private and public keys separately.
162
+ 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
163
 
164
164
  ```ruby
165
165
  JWTSessions.algorithm = "RS256"
@@ -167,10 +167,11 @@ JWTSessions.private_key = OpenSSL::PKey::RSA.generate(2048)
167
167
  JWTSessions.public_key = JWTSessions.private_key.public_key
168
168
  ```
169
169
 
170
- You can build login controller to receive access, refresh and csrf tokens in exchange for user's login/password. \
171
- Refresh controller - to be able to get a new access token using refresh token after access is expired. \
172
- Here is example of a simple login controller, which returns set of tokens as a plain JSON response. \
173
- It's also possible to set tokens as cookies in the response instead.
170
+ You can build a login controller to receive access, refresh and CSRF tokens in exchange for the user's login/password. \
171
+ Refresh controller allows you to get a new access token using the refresh token after access is expired. \
172
+
173
+ Here is an example of a simple login controller, which returns a set of tokens as a plain JSON response. \
174
+ It is also possible to set tokens as cookies in the response instead.
174
175
 
175
176
  ```ruby
176
177
  class LoginController < ApplicationController
@@ -187,7 +188,7 @@ class LoginController < ApplicationController
187
188
  end
188
189
  ```
189
190
 
190
- Now you can build a refresh endpoint. To protect the endpoint use before_action `authorize_refresh_request!`. \
191
+ Now you can build a refresh endpoint. To protect the endpoint use the before_action `authorize_refresh_request!`. \
191
192
  The endpoint itself should return a renewed access token.
192
193
 
193
194
  ```ruby
@@ -205,15 +206,16 @@ class RefreshController < ApplicationController
205
206
  end
206
207
  end
207
208
  ```
208
- In the example `found_token` - is a token fetched from request headers or cookies. In the context of `RefreshController` it's a refresh token. \
209
- The refresh request with headers must include `X-Refresh-Token` (header name is configurable) with refresh token.
209
+
210
+ 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. \
211
+ The refresh request with headers must include `X-Refresh-Token` (header name is configurable) with the refresh token.
210
212
 
211
213
  ```
212
214
  X-Refresh-Token: eyJhbGciOiJIUzI1NiJ9...
213
215
  POST /refresh
214
216
  ```
215
217
 
216
- Now when there're login and refresh endpoints, you can protect the rest of your secure controllers with `before_action :authorize_access_request!`.
218
+ When there are login and refresh endpoints, you can protect the rest of your secured controllers with `before_action :authorize_access_request!`.
217
219
 
218
220
  ```ruby
219
221
  class UsersController < ApplicationController
@@ -228,6 +230,7 @@ class UsersController < ApplicationController
228
230
  end
229
231
  end
230
232
  ```
233
+
231
234
  Headers must include `Authorization: Bearer` with access token.
232
235
 
233
236
  ```
@@ -244,7 +247,7 @@ end
244
247
  ```
245
248
 
246
249
  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 next auth methods can be used instead:
250
+ For the cases when an endpoint must support only one specific token transport the following authorization methods can be used instead:
248
251
 
249
252
  ```ruby
250
253
  authorize_by_access_cookie!
@@ -255,7 +258,7 @@ authorize_by_refresh_header!
255
258
 
256
259
  ### Non-Rails usage
257
260
 
258
- You must include `JWTSessions::Authorization` module to your auth class and implement within it next methods:
261
+ You must include `JWTSessions::Authorization` module to your auth class and within it implement the following methods:
259
262
 
260
263
  1. request_headers
261
264
 
@@ -282,7 +285,7 @@ end
282
285
  ```
283
286
 
284
287
  Example Sinatra app. \
285
- NOTE: Since rack updates HTTP headers by using `HTTP_` prefix, upcasing and using underscores for sake of simplicity JWTSessions tokens header names are converted to rack-style in this example.
288
+ 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
289
 
287
290
  ```ruby
288
291
  require "sinatra/base"
@@ -344,9 +347,9 @@ List of configurable settings with their default values.
344
347
 
345
348
  ##### Token store
346
349
 
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 won't configure the adapter explicitly, this gem will try to load `redis` and use it, otherwise it would fallback to a `memory` adapter.
350
+ 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
351
 
349
- Memory store accepts only `prefix` (used for redis db keys). Here is a default configuration for Redis:
352
+ Memory store only accepts a `prefix` (used for Redis db keys). Here is a default configuration for Redis:
350
353
 
351
354
  ```ruby
352
355
  JWTSessions.token_store = :redis, {
@@ -371,7 +374,7 @@ JWTSessions.token_store = :redis, { redis_url: "redis://localhost:6397" }
371
374
  JWTSessions.algorithm = "HS256"
372
375
  ```
373
376
 
374
- You need to specify a secret to use for HMAC, this setting doesn"t have a default value.
377
+ You need to specify a secret to use for HMAC as this setting does not have a default value.
375
378
 
376
379
  ```ruby
377
380
  JWTSessions.encryption_key = "secret"
@@ -384,9 +387,9 @@ JWTSessions.private_key = "abcd"
384
387
  JWTSessions.public_key = "efjh"
385
388
  ```
386
389
 
387
- NOTE: ED25519 and HS512256 require rbnacl installation in order to make it work.
390
+ NOTE: ED25519 and HS512256 require `rbnacl` installation in order to make it work.
388
391
 
389
- jwt_sessions only uses `exp` claim by default when it decodes tokens, you can specify which additional claims to use by
392
+ jwt_sessions only uses `exp` claim by default when it decodes tokens and you can specify which additional claims to use by
390
393
  setting `jwt_options`. You can also specify leeway to account for clock skew.
391
394
 
392
395
  ```ruby
@@ -413,11 +416,11 @@ class UsersController < ApplicationController
413
416
  end
414
417
  ```
415
418
 
416
- Claims are also supported by `JWTSessions::Session`, you can pass `access_claims` and `refresh_claims` options in the initializer.
419
+ Claims are also supported by `JWTSessions::Session` and you can pass `access_claims` and `refresh_claims` options in the initializer.
417
420
 
418
421
  ##### Request headers and cookies names
419
422
 
420
- Default request headers/cookies names can be re-configured
423
+ Default request headers/cookies names can be reconfigured.
421
424
 
422
425
  ```ruby
423
426
  JWTSessions.access_header = "Authorization"
@@ -429,19 +432,19 @@ JWTSessions.csrf_header = "X-CSRF-Token"
429
432
 
430
433
  ##### Expiration time
431
434
 
432
- Access token must have a short life span, while refresh tokens can be stored for a longer time period
435
+ Access token must have a short life span, while refresh tokens can be stored for a longer time period.
433
436
 
434
437
  ```ruby
435
438
  JWTSessions.access_exp_time = 3600 # 1 hour in seconds
436
439
  JWTSessions.refresh_exp_time = 604800 # 1 week in seconds
437
440
  ```
438
441
 
439
- It's defined globally, but can be overridden on a session level. See `JWTSessions::Session.new` options for more info.
442
+ It is defined globally, but can be overridden on a session level. See `JWTSessions::Session.new` options for more info.
440
443
 
441
444
  #### CSRF and cookies
442
445
 
443
- In case when you use cookies as your tokens transport it gets vulnerable to CSRF. That's why both 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 `Authorization` exception will be raised in case of mismatch between the token from the header and the one stored in session. \
444
- Although you don't need to mitigate BREACH attacks it's still possible to generate a new masked token with the access token.
446
+ 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. \
447
+ Although you do not need to mitigate BREACH attacks it is still possible to generate a new masked token with the access token.
445
448
 
446
449
  ```ruby
447
450
  session = JWTSessions::Session.new
@@ -450,10 +453,11 @@ session.masked_csrf(access_token)
450
453
 
451
454
  ##### Refresh with access token
452
455
 
453
- Sometimes it's not secure enough to store the refresh tokens in web / JS clients. \
454
- That's why you have a possibility to operate only by an access token, and to not pass the refresh to the client at all. \
455
- Session accepts `refresh_by_access_allowed: true` setting, which links the access token to the according refresh token. \
456
- Example Rails login controller, which passes an access token token via cookies and renders CSRF.
456
+ Sometimes it is not secure enough to store the refresh tokens in web / JS clients. \
457
+ 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. \
458
+ Session accepts `refresh_by_access_allowed: true` setting, which links the access token to the corresponding refresh token. \
459
+
460
+ Example Rails login controller, which passes an access token token via cookies and renders CSRF:
457
461
 
458
462
  ```ruby
459
463
  class LoginController < ApplicationController
@@ -477,17 +481,18 @@ class LoginController < ApplicationController
477
481
  end
478
482
  ```
479
483
 
480
- The gem provides an ability to refresh the session by access token.
484
+ The gem provides the ability to refresh the session by access token.
481
485
 
482
486
  ```ruby
483
487
  session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
484
488
  tokens = session.refresh_by_access_payload
485
489
  ```
486
490
 
487
- In case of token forgery and successful refresh performed by an atacker - the original user will have to logout. \
488
- To protect the endpoint use before_action `authorize_refresh_by_access_request!`. \
489
- Example Rails refresh by access controller with cookies as token transport. \
490
- As refresh should be performed once the access token is already expired we need to use `claimless_payload` method in order to skip JWT expiration validation (and other claims) so we can proceed.
491
+ In case of token forgery and successful refresh performed by an attacker the original user will have to logout. \
492
+ To protect the endpoint use the before_action `authorize_refresh_by_access_request!`. \
493
+ 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. \
494
+
495
+ Example Rails refresh by access controller with cookies as token transport:
491
496
 
492
497
  ```ruby
493
498
  class RefreshController < ApplicationController
@@ -507,7 +512,7 @@ end
507
512
 
508
513
  ```
509
514
 
510
- For the cases when an endpoint must support only one specific token transport the next auth methods can be used instead:
515
+ For the cases when an endpoint must support only one specific token transport the following auth methods can be used instead:
511
516
 
512
517
  ```ruby
513
518
  authorize_refresh_by_access_cookie!
@@ -516,8 +521,8 @@ authorize_refresh_by_access_header!
516
521
 
517
522
  #### Refresh token hijack protection
518
523
 
519
- There is a security recommendation regarding the usage of refresh tokens: only perform refresh when an access token gets expired. \
520
- Since sessions are always defined by a pair of tokens and there can't 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 perfomed before the expiration of the access token by one of the users. Because of that `refresh` method of the `Session` class supports 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.
524
+ There is a security recommendation regarding the usage of refresh tokens: only perform refresh when an access token expires. \
525
+ 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
526
 
522
527
  ```ruby
523
528
  session = JwtSessions::Session.new(payload: payload)
@@ -526,7 +531,7 @@ session.refresh(refresh_token) { |refresh_token_uid, access_token_expiration| ..
526
531
 
527
532
  ## Flush Sessions
528
533
 
529
- Flush a session by its refresh token. The method returns number of flushed sessions.
534
+ Flush a session by its refresh token. The method returns number of flushed sessions:
530
535
 
531
536
  ```ruby
532
537
  session = JWTSessions::Session.new
@@ -534,7 +539,7 @@ tokens = session.login
534
539
  session.flush_by_token(tokens[:refresh]) # => 1
535
540
  ```
536
541
 
537
- Flush a session by its access token.
542
+ Flush a session by its access token:
538
543
 
539
544
  ```ruby
540
545
  session = JWTSessions::Session.new(refresh_by_access_allowed: true)
@@ -545,7 +550,7 @@ session = JWTSessions::Session.new(refresh_by_access_allowed: true, payload: pay
545
550
  session.flush_by_access_payload
546
551
  ```
547
552
 
548
- Or by refresh token UID
553
+ Or by refresh token UID:
549
554
 
550
555
  ```ruby
551
556
  session.flush_by_uid(uid) # => 1
@@ -553,27 +558,28 @@ session.flush_by_uid(uid) # => 1
553
558
 
554
559
  ##### Sessions namespace
555
560
 
556
- It's possible to group sessions by custom namespaces
561
+ It's possible to group sessions by custom namespaces:
557
562
 
558
563
  ```ruby
559
564
  session = JWTSessions::Session.new(namespace: "account-1")
560
565
  ```
561
566
 
562
- and selectively flush sessions by namespace
567
+ Selectively flush sessions by namespace:
563
568
 
564
569
  ```ruby
565
570
  session = JWTSessions::Session.new(namespace: "ie-sessions")
566
571
  session.flush_namespaced # will flush all sessions which belong to the same namespace
567
572
  ```
568
573
 
569
- it's posible to flush access tokens only
574
+ Flush access tokens only:
570
575
 
571
576
  ```ruby
572
577
  session = JWTSessions::Session.new(namespace: "ie-sessions")
573
578
  session.flush_namespaced_access_tokens # will flush all access tokens which belong to the same namespace, but will keep refresh tokens
574
579
  ```
575
580
 
576
- To force flush of all app sessions
581
+ Force flush of all app sessions:
582
+
577
583
  ```ruby
578
584
  JWTSessions::Session.flush_all
579
585
  ```
@@ -583,14 +589,14 @@ JWTSessions::Session.flush_all
583
589
  To logout you need to remove both access and refresh tokens from the store. \
584
590
  Flush sessions methods can be used to perform logout. \
585
591
  Refresh token or refresh token UID is required to flush a session. \
586
- To logout with an access token `refresh_by_access_allowed` setting should be set to true on an access token creation. If logout by access token is allowed it's recommended to ignore the expiration claim and to allow to logout with expired access token.
592
+ 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
593
 
588
594
  ## Examples
589
595
 
590
596
  [Rails API](test/support/dummy_api) \
591
597
  [Sinatra API](test/support/dummy_sinatra_api)
592
598
 
593
- You can use a mixed approach for the cases when you'd like to store an access token in localStorage and refresh token in HTTP-only secure cookies. \
599
+ 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
600
  Rails controllers setup example:
595
601
 
596
602
  ```ruby
@@ -41,8 +41,10 @@ module JWTSessions
41
41
  end
42
42
  end
43
43
 
44
- def find(uid, store, namespace = nil)
45
- token_attrs = store.fetch_refresh(uid, namespace)
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
@@ -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)
@@ -22,7 +22,7 @@ module JWTSessions
22
22
  storage[""]["access"].store(uid, access_token)
23
23
  end
24
24
 
25
- def fetch_refresh(uid, namespace)
25
+ def fetch_refresh(uid, namespace, _first_match = false)
26
26
  value_if_not_expired(uid, "refresh", namespace.to_s)
27
27
  end
28
28
 
@@ -31,8 +31,9 @@ module JWTSessions
31
31
  storage.expireat(key, expiration)
32
32
  end
33
33
 
34
- def fetch_refresh(uid, namespace)
35
- values = storage.hmget(refresh_key(uid, namespace), *REFRESH_KEYS).compact
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
- storage.del(refresh_key(uid, namespace))
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 refresh_key(uid, namespace)
111
- if namespace.to_s.empty?
112
- wildcard_refresh_key(uid)
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 wildcard_refresh_key(uid)
119
- (storage.keys(refresh_key(uid, "*")) || []).first
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWTSessions
4
- VERSION = "2.4.1"
4
+ VERSION = "2.4.2"
5
5
  end
@@ -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
@@ -296,7 +296,7 @@ class TestSession < Minitest::Test
296
296
 
297
297
  session.flush_namespaced_access_tokens
298
298
  ruid = session.instance_variable_get(:"@_refresh").uid
299
- refresh_token = JWTSessions::RefreshToken.find(ruid, JWTSessions.token_store, nil)
299
+ refresh_token = JWTSessions::RefreshToken.find(ruid, JWTSessions.token_store, namespace)
300
300
  assert_equal "", refresh_token.access_uid
301
301
  assert_equal "", refresh_token.access_expiration
302
302
 
@@ -306,7 +306,7 @@ class TestSession < Minitest::Test
306
306
  end
307
307
  auid = session.instance_variable_get(:"@_access").uid
308
308
  access_token = JWTSessions::AccessToken.find(auid, JWTSessions.token_store)
309
- refresh_token = JWTSessions::RefreshToken.find(ruid, JWTSessions.token_store, nil)
309
+ refresh_token = JWTSessions::RefreshToken.find(ruid, JWTSessions.token_store, namespace)
310
310
 
311
311
  assert_equal false, access_token.uid.size.zero?
312
312
  assert_equal false, access_token.expiration.size.zero?
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.1
4
+ version: 2.4.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: 2019-05-25 00:00:00.000000000 Z
11
+ date: 2019-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt
@@ -137,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
137
  version: '0'
138
138
  requirements: []
139
139
  rubyforge_project:
140
- rubygems_version: 2.7.6
140
+ rubygems_version: 2.6.13
141
141
  signing_key:
142
142
  specification_version: 4
143
143
  summary: JWT Sessions