jwt_sessions 1.3.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4bdd67c0de6950cd6f99bc26f1500c06367f76f4
4
- data.tar.gz: aa5c2c26f7e15cac228492af9a9b4a1571dab08b
3
+ metadata.gz: 6ab2b9607a84bdecaea7ec538b3ba194ae0d57e4
4
+ data.tar.gz: 3ade912f52efd6078fb676697187145c23e39024
5
5
  SHA512:
6
- metadata.gz: 9399b04dd8484864cb084ef80f284389ec04eb6b22cc8b46e95a5c3670849e6c5c069f10613d058e13c2817809990ab207e360b0a4ed5a0ce656686c7b90c87a
7
- data.tar.gz: 88cc2e5441dc8b0afae1e167ca9b7a3d4f68796927824e8b03727a42a7e83451c790a05ad8fb9050f69b23facae1265f9b7a51a43170a2285ca48df157a99e9b
6
+ metadata.gz: 7761b2f89cf64203f8f82cef4e95c28f6ea602cee0a22d56318bbfd1299cb6e2c3b50ad41b840fddd458dc53b0b249b3d7820a99405b67b2ffb532b900f15d51
7
+ data.tar.gz: d4e087880bfc893c983c7187d2c206c521efe3beb0c0c245161e0c614bc59868a05c474c9479a36f778966c4ab017c220fd9e866448afd83f9444c59f5f5b33c
data/README.md CHANGED
@@ -341,7 +341,7 @@ class UsersController < ApplicationController
341
341
  end
342
342
  ```
343
343
 
344
- Claims are also supported by `JWTSessions::Session`, you can pass `access_claims` and `refresh_claims` options in the initializer
344
+ Claims are also supported by `JWTSessions::Session`, you can pass `access_claims` and `refresh_claims` options in the initializer.
345
345
 
346
346
  ##### Request headers and cookies names
347
347
 
@@ -367,7 +367,7 @@ JWTSessions.refresh_exp_time = 604800 # 1 week in seconds
367
367
  #### CSRF and cookies
368
368
 
369
369
  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. \
370
- Although you don't need to mitigate BREACH attacks it's still possible to generate a new masked token with the access token
370
+ Although you don't need to mitigate BREACH attacks it's still possible to generate a new masked token with the access token.
371
371
 
372
372
  ```ruby
373
373
  session = JWTSessions::Session.new
@@ -376,15 +376,13 @@ session.masked_csrf(access_token)
376
376
 
377
377
  ##### Refresh with access token
378
378
 
379
- Sometimes it's not secure enough to store the refresh tokens in web / JS clients.
380
- That's why you have a possibility to operate only with an access token, and to not pass the refresh to the client at all.
381
- Session accepts `refresh_by_access_allowed: true` setting, which links the access token to the according refresh token.
379
+ Sometimes it's not secure enough to store the refresh tokens in web / JS clients. \
380
+ 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. \
381
+ Session accepts `refresh_by_access_allowed: true` setting, which links the access token to the according refresh token. \
382
382
  Example Rails login controller, which passes an access token token via cookies and renders CSRF.
383
383
 
384
384
  ```ruby
385
385
  class LoginController < ApplicationController
386
- include ActionController::Cookies
387
-
388
386
  def create
389
387
  user = User.find_by!(email: params[:email])
390
388
  if user.authenticate(params[:password])
@@ -392,7 +390,10 @@ class LoginController < ApplicationController
392
390
  payload = { user_id: user.id }
393
391
  session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
394
392
  tokens = session.login
395
- cookies[JWTSessions.access_cookie] = tokens[:access]
393
+ response.set_cookie(JWTSessions.access_cookie,
394
+ value: tokens[:access],
395
+ httponly: true,
396
+ secure: Rails.env.production?)
396
397
 
397
398
  render json: { csrf: tokens[:csrf] }
398
399
  else
@@ -405,22 +406,26 @@ end
405
406
  The gem provides an ability to refresh the session by access token.
406
407
 
407
408
  ```ruby
408
- tokens = session.refresh_by_access(access_token)
409
+ session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
410
+ tokens = session.refresh_by_access_payload
409
411
  ```
410
412
 
411
- In case of token forgery and successful refresh performed by an atacker - the original user will have to logout.
412
- To protect the endpoint use before_action `authorize_refresh_by_access_request!`.
413
- Example Rails refresh by access controller with cookies as token transport.
413
+ In case of token forgery and successful refresh performed by an atacker - the original user will have to logout. \
414
+ To protect the endpoint use before_action `authorize_refresh_by_access_request!`. \
415
+ Example Rails refresh by access controller with cookies as token transport. \
416
+ 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.
414
417
 
415
418
  ```ruby
416
419
  class RefreshController < ApplicationController
417
- include ActionController::Cookies
418
420
  before_action :authorize_refresh_by_access_request!
419
421
 
420
422
  def create
421
- session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
422
- tokens = session.refresh_by_access(found_token)
423
- cookies[JWTSessions.access_cookie] = tokens[:access]
423
+ session = JWTSessions::Session.new(payload: claimless_payload, refresh_by_access_allowed: true)
424
+ tokens = session.refresh_by_access_payload
425
+ response.set_cookie(JWTSessions.access_cookie,
426
+ value: tokens[:access],
427
+ httponly: true,
428
+ secure: Rails.env.production?)
424
429
 
425
430
  render json: { csrf: tokens[:csrf] }
426
431
  end
@@ -440,7 +445,7 @@ session.refresh(refresh_token) { |refresh_token_uid, access_token_expiration| ..
440
445
 
441
446
  ## Flush Sessions
442
447
 
443
- Flush session by refresh token. The method returns number of flushed sessions.
448
+ Flush a session by its refresh token. The method returns number of flushed sessions.
444
449
 
445
450
  ```ruby
446
451
  session = JWTSessions::Session.new
@@ -448,6 +453,14 @@ tokens = session.login
448
453
  session.flush_by_token(tokens[:refresh]) # => 1
449
454
  ```
450
455
 
456
+ Flush a session by its access token.
457
+
458
+ ```ruby
459
+ session = JWTSessions::Session.new(refresh_by_access_allowed: true)
460
+ tokens = session.login
461
+ session.flush_by_access_token(tokens[:access]) # => 1
462
+ ```
463
+
451
464
  Or by refresh token UID
452
465
 
453
466
  ```ruby
@@ -476,9 +489,9 @@ JWTSessions::Session.flush_all
476
489
 
477
490
  ##### Logout
478
491
 
479
- To logout you need to remove both access and refresh tokens from the store.
480
- Flush sessions methods can be used to perform logout.
481
- Refresh token or refresh token UID is required to flush a session.
492
+ To logout you need to remove both access and refresh tokens from the store. \
493
+ Flush sessions methods can be used to perform logout. \
494
+ Refresh token or refresh token UID is required to flush a session. \
482
495
  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.
483
496
 
484
497
  ## Examples
@@ -8,7 +8,7 @@ module JWTSessions
8
8
  @csrf = csrf
9
9
  @uid = uid
10
10
  @expiration = expiration
11
- @payload = payload
11
+ @payload = payload.merge('uid' => uid, 'exp' => expiration.to_i)
12
12
  @store = store
13
13
  end
14
14
 
@@ -25,7 +25,7 @@ module JWTSessions
25
25
  end
26
26
 
27
27
  def token
28
- Token.encode(payload.merge(uid: uid, exp: expiration.to_i))
28
+ Token.encode(payload)
29
29
  end
30
30
 
31
31
  class << self
@@ -27,9 +27,8 @@ module JWTSessions
27
27
  rescue Errors::Unauthorized
28
28
  cookie_based_auth(:access)
29
29
  end
30
- # only latest access token can be used for refresh
31
- invalid_authorization unless session_exists?(:access)
32
- check_csrf(:access)
30
+
31
+ invalid_authorization if should_check_csrf? && @_csrf_check && !JWTSessions::Session.new.valid_access_request?(retrieve_csrf, claimless_payload)
33
32
  end
34
33
 
35
34
  private
@@ -103,5 +102,10 @@ module JWTSessions
103
102
  claims = respond_to?(:token_claims) ? token_claims : {}
104
103
  @_payload ||= Token.decode(found_token, claims).first
105
104
  end
105
+
106
+ # retrieves tokens payload without JWT claims validation
107
+ def claimless_payload
108
+ @_claimless_payload ||= Token.decode!(found_token).first
109
+ end
106
110
  end
107
111
  end
@@ -16,7 +16,7 @@ module JWTSessions
16
16
  @uid = options.fetch(:uid, SecureRandom.uuid)
17
17
  @expiration = options.fetch(:expiration, JWTSessions.refresh_expiration)
18
18
  @namespace = options.fetch(:namespace, nil)
19
- @token = Token.encode(options.fetch(:payload, {}).merge(uid: uid, exp: expiration.to_i))
19
+ @token = Token.encode(options.fetch(:payload, {}).merge('uid' => uid, 'exp' => expiration.to_i))
20
20
  end
21
21
 
22
22
  class << self
@@ -33,7 +33,7 @@ module JWTSessions
33
33
  end
34
34
  end
35
35
 
36
- def find(uid, store, namespace)
36
+ def find(uid, store, namespace = nil)
37
37
  token_attrs = store.fetch_refresh(uid, namespace)
38
38
  raise Errors::Unauthorized, 'Refresh token not found' if token_attrs.empty?
39
39
  build_with_token_attrs(store, uid, token_attrs, namespace)
@@ -61,7 +61,7 @@ module JWTSessions
61
61
  @csrf = csrf
62
62
  @access_uid = access_uid
63
63
  @access_expiration = access_expiration
64
- store.update_refresh(uid, access_uid, access_expiration, csrf, namespace)
64
+ store.update_refresh(uid, access_expiration, access_uid, csrf, namespace)
65
65
  end
66
66
 
67
67
  def destroy
@@ -50,12 +50,19 @@ module JWTSessions
50
50
  refresh_by_uid(&block)
51
51
  end
52
52
 
53
- def refresh_by_access(token, &block)
54
- ruid = access_token_ruid(token)
53
+ def refresh_by_access_payload(&block)
54
+ raise Errors::InvalidPayload if payload.nil?
55
+ ruid = retrive_ruid_from(payload)
55
56
  retrieve_refresh_token(ruid)
56
57
  refresh_by_uid(&block)
57
58
  end
58
59
 
60
+ def flush_by_access_payload
61
+ raise Errors::InvalidPayload if payload.nil?
62
+ ruid = retrive_ruid_from(payload)
63
+ flush_by_uid(ruid)
64
+ end
65
+
59
66
  def flush_by_token(token)
60
67
  uid = token_uid(token, :refresh, @refresh_claims)
61
68
  flush_by_uid(uid)
@@ -85,6 +92,19 @@ module JWTSessions
85
92
  end.count
86
93
  end
87
94
 
95
+ def valid_access_request?(external_csrf_token, external_payload)
96
+ ruid = external_payload.fetch('ruid', nil)
97
+ uid = external_payload.fetch('uid', nil)
98
+ if ruid.nil? || uid.nil?
99
+ message = 'Token payload is invalid'
100
+ raise Errors::InvalidPayload, message
101
+ end
102
+ refresh_token = RefreshToken.find(ruid, JWTSessions.token_store)
103
+ return false unless uid == refresh_token.access_uid
104
+
105
+ CSRFToken.new(refresh_token.csrf).valid_authenticity_token?(external_csrf_token)
106
+ end
107
+
88
108
  private
89
109
 
90
110
  def valid_access_csrf?(access_token, csrf_token)
@@ -133,14 +153,7 @@ module JWTSessions
133
153
  uid
134
154
  end
135
155
 
136
- def access_token_ruid(token)
137
- token_payload = JWTSessions::Token.decode(token, @access_claims).first
138
-
139
- # ensure access token exists in the store
140
- uid = token_payload.fetch('uid', nil)
141
- data = store.fetch_access(uid)
142
- raise Errors::Unauthorized, 'Access token not found' if data.empty?
143
-
156
+ def retrive_ruid_from(token_payload)
144
157
  ruid = token_payload.fetch('ruid', nil)
145
158
  if ruid.nil?
146
159
  message = "Access token payload does not contain refresh uid"
@@ -186,6 +199,7 @@ module JWTSessions
186
199
  return unless refresh_by_access_allowed
187
200
  @_access.refresh_uid = @_refresh.uid
188
201
  @access_token = @_access.token
202
+ @payload = @_access.payload
189
203
  end
190
204
 
191
205
  def create_csrf_token
@@ -19,6 +19,13 @@ module JWTSessions
19
19
  raise Errors::Unauthorized, 'could not decode a token'
20
20
  end
21
21
 
22
+ def decode!(token)
23
+ decode_options = { algorithm: JWTSessions.algorithm }
24
+ JWT.decode(token, JWTSessions.public_key, false, decode_options)
25
+ rescue StandardError
26
+ raise Errors::Unauthorized, 'could not decode a token'
27
+ end
28
+
22
29
  def meta
23
30
  { exp: JWTSessions.access_expiration }
24
31
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWTSessions
4
- VERSION = '1.3.0'
4
+ VERSION = '2.0.0'
5
5
  end
@@ -33,14 +33,54 @@ class TestSession < Minitest::Test
33
33
  assert_equal payload[:test], decoded_access['test']
34
34
  end
35
35
 
36
- def test_refresh_by_access
36
+ def test_refresh_by_access_payload
37
37
  session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
38
- tokens = session.login
39
- refreshed_tokens = session.refresh_by_access(tokens[:access])
38
+ session.login
39
+ access1 = session.instance_variable_get('@_access')
40
+ sleep(1)
41
+ refreshed_tokens = session.refresh_by_access_payload
42
+ access2 = session.instance_variable_get('@_access')
40
43
  decoded_access = JWTSessions::Token.decode(refreshed_tokens[:access]).first
41
44
  assert_equal EXPECTED_KEYS, refreshed_tokens.keys.sort
42
45
  assert_equal payload[:test], decoded_access['test']
43
46
  assert_equal session.instance_variable_get('@_refresh').uid, decoded_access['ruid']
47
+ assert_equal access2.expiration > access1.expiration, true
48
+ end
49
+
50
+ def test_refresh_by_access_payload_expired
51
+ JWTSessions.access_exp_time = 0
52
+ session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
53
+ session.login
54
+ refreshed_tokens = session.refresh_by_access_payload
55
+ decoded_access = JWTSessions::Token.decode!(refreshed_tokens[:access]).first
56
+ JWTSessions.access_exp_time = 3600
57
+ assert_equal EXPECTED_KEYS, refreshed_tokens.keys.sort
58
+ assert_equal payload[:test], decoded_access['test']
59
+ assert_equal session.instance_variable_get('@_refresh').uid, decoded_access['ruid']
60
+ end
61
+
62
+ def test_refresh_by_access_payload_with_block_expired
63
+ JWTSessions.access_exp_time = 0
64
+ session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
65
+ session.login
66
+ refreshed_tokens = session.refresh_by_access_payload do
67
+ raise JWTSessions::Errors::Unauthorized
68
+ end
69
+ decoded_access = JWTSessions::Token.decode!(refreshed_tokens[:access]).first
70
+ JWTSessions.access_exp_time = 3600
71
+ assert_equal EXPECTED_KEYS, refreshed_tokens.keys.sort
72
+ assert_equal payload[:test], decoded_access['test']
73
+ assert_equal session.instance_variable_get('@_refresh').uid, decoded_access['ruid']
74
+ end
75
+
76
+ def test_refresh_by_access_payload_with_block_not_expired
77
+ session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
78
+ session.login
79
+ assert_raises JWTSessions::Errors::Unauthorized do
80
+ session.refresh_by_access_payload do
81
+ raise JWTSessions::Errors::Unauthorized
82
+ end
83
+ end
44
84
  end
45
85
 
46
86
  def test_refresh_with_block_not_expired
@@ -76,6 +116,19 @@ class TestSession < Minitest::Test
76
116
  end
77
117
  end
78
118
 
119
+ def test_flush_by_access_token
120
+ session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
121
+ session.login
122
+ refresh_token = session.instance_variable_get(:"@_refresh")
123
+ uid = refresh_token.uid
124
+
125
+ session.flush_by_access_payload
126
+
127
+ assert_raises JWTSessions::Errors::Unauthorized do
128
+ JWTSessions::RefreshToken.find(uid, JWTSessions.token_store, nil)
129
+ end
130
+ end
131
+
79
132
  def test_flush_by_uid
80
133
  refresh_token = @session.instance_variable_get(:"@_refresh")
81
134
  uid = refresh_token.uid
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: 1.3.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yulia Oletskaya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-15 00:00:00.000000000 Z
11
+ date: 2018-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt