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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d60c5dcae06e9f49ee1e99125baab8285156bd54eb5b36a3589001cee55acd6e
4
- data.tar.gz: faa1cf67e08b8a320b525a918260dd4cf61f0aaf5f4ea1160f0e82ffff2e97b6
3
+ metadata.gz: 1ec082d39caff5259e52ca85c48a281d0673e7bb64c04d9033b241f99113c726
4
+ data.tar.gz: 6c8a90de8b2814bfe148d168706efee94f34e84b01412515d172b9807c7883cd
5
5
  SHA512:
6
- metadata.gz: eb844adfe82415bc6126f5a32bceeb95234fbe8172ca427dbeaf7535bc4ae745cee3e33207b9e017c72098b8bf08b0d05cc302b952690568e5ca11fe12ad7629
7
- data.tar.gz: 38b0299aa445d3a1558ed66e5a8d805850635065bf17d4de3d4f7f7b6c81a5962cac801bd79ae019209a8df96a4da64592897c6e45355e763b6220f5dfbed1af
6
+ metadata.gz: 433a7a5c8495505bc568641c9d733efaac8d626236ed3fa5484b3465800fcc1deac65fdb02c4d5c2261d725660fb176baf53d710952a9616296821b6852b1a7c
7
+ data.tar.gz: 4a5fdfbe4d0e20868544280c5245d8dfbb5a2eafa80940ff30f6572f88afcfd38383123b067e5adf5e9b1e199dd8d908b4660f4521637c789d6a1e06de2b3f1a
@@ -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
- * [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
+ - [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
- Main goal of this gem is to provide configurable, manageable, and safe stateful sessions based on JSON Web Tokens.
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, redis and memory stores are supported), making it possible to manage sessions, reset passwords, logout users in a reliable and secure way.
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's designed to be framework agnostic yet is easily integrable, and Rails integration is available out of the box.
39
+ It is designed to be framework agnostic, yet easily integrable, and Rails integration is available out of the box.
39
40
 
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.
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, and its reserved claim names are supported as well as it's allowed to configure claim checks and cryptographic signing algorithms supported by it.
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 it from headers first, then from cookies (CSRF check included) if the headers check failed.
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 helps to identify current user but it's not necessary, the payload can be an empty hash as well.
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 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.
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, then it'll be possible to logout the user from all devises. More info [Sessions Namespace](#sessions-namespace).
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, and is expected to return a hash-like object with claims to be validated within a token's payload.
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, add `JWTSessions::Errors::Unauthorized` exceptions handling if needed.
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's adviced to store the key itself within the app secrets.
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, 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.
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 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.
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
- 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.
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
- Now when there're login and refresh endpoints, you can protect the rest of your secure controllers with `before_action :authorize_access_request!`.
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 next auth methods can be used instead:
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 implement within it next methods:
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: 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.
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 won't configure the adapter explicitly, this gem will try to load `redis` and use it, otherwise it would fallback to a `memory` adapter.
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 only `prefix` (used for redis db keys). Here is a default configuration for Redis:
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, this setting doesn"t have a default value.
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, you can specify which additional claims to use by
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`, you can pass `access_claims` and `refresh_claims` options in the initializer.
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 re-configured
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's defined globally, but can be overridden on a session level. See `JWTSessions::Session.new` options for more info.
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
- 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.
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'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.
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 an ability to refresh the session by access token.
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 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.
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 next auth methods can be used instead:
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 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.
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
- and selectively flush sessions by namespace
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
- it's posible to flush access tokens only
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
- To force flush of all app sessions
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` 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.
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'd like to store an access token in localStorage and refresh token in HTTP-only secure cookies. \
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
@@ -5,5 +5,6 @@ module JWTSessions
5
5
  class InvalidPayload < Error; end
6
6
  class Unauthorized < Error; end
7
7
  class ClaimsVerification < Unauthorized; end
8
+ class Expired < ClaimsVerification; end
8
9
  end
9
10
  end
@@ -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
@@ -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
@@ -11,7 +11,8 @@ module JWTSessions
11
11
  raise NotImplementedError
12
12
  end
13
13
 
14
- def fetch_refresh(_uid, _namespace)
14
+ # Set first_match to true to look up through all namespaces
15
+ def fetch_refresh(_uid, _namespace, _first_match)
15
16
  raise NotImplementedError
16
17
  end
17
18
 
@@ -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
- value_if_not_expired(uid, "refresh", namespace.to_s)
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
- 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)
@@ -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::InvalidIssuerError, JWT::InvalidIatError, JWT::InvalidAudError, JWT::InvalidSubError, JWT::InvalidJtiError, JWT::ExpiredSignature => e
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, "could not decode a token"
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, "could not decode a token"
32
+ raise Errors::Unauthorized, DECODE_ERROR
29
33
  end
30
34
 
31
35
  def meta
32
- { exp: JWTSessions.access_expiration }
36
+ { "exp" => JWTSessions.access_expiration }
33
37
  end
34
38
  end
35
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWTSessions
4
- VERSION = "2.4.1"
4
+ VERSION = "2.5.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
@@ -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, nil)
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, nil)
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: Time.now.to_i - 20))
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: Time.now.to_i - 100))
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: Time.now.to_i - (3600 * 24)))
145
- assert_raises JWTSessions::Errors::Unauthorized do
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.1
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: 2019-05-25 00:00:00.000000000 Z
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
- rubyforge_project:
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