jwt_sessions 1.2.1 → 1.3.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: 1252d6b636f396716b493926fcca3b05a4554c99
4
- data.tar.gz: 2d9e3dfaae57e7d86815387168a59d9fceb11a3b
3
+ metadata.gz: 4bdd67c0de6950cd6f99bc26f1500c06367f76f4
4
+ data.tar.gz: aa5c2c26f7e15cac228492af9a9b4a1571dab08b
5
5
  SHA512:
6
- metadata.gz: 6c0ebdfffa676b2f885dac0764801ad93426002f3b579339277e6c80a7c01f9b98394a28deb250f23fe4419c2cea282d1cd1738d4b95bdc87453b3c25b1ddb0c
7
- data.tar.gz: 3e861393dc58de0eac408022b9c0ac342d262bfb0f5654303704c2126f583b93a3560a7dee1c07d18ffcbfda2e9b472d6e9ec6926ea9d0e53ed8426c067d641a
6
+ metadata.gz: 9399b04dd8484864cb084ef80f284389ec04eb6b22cc8b46e95a5c3670849e6c5c069f10613d058e13c2817809990ab207e360b0a4ed5a0ce656686c7b90c87a
7
+ data.tar.gz: 88cc2e5441dc8b0afae1e167ca9b7a3d4f68796927824e8b03727a42a7e83451c790a05ad8fb9050f69b23facae1265f9b7a51a43170a2285ca48df157a99e9b
data/README.md CHANGED
@@ -19,9 +19,11 @@ XSS/CSRF safe JWT auth designed for SPA
19
19
  + [Request headers and cookies names](#request-headers-and-cookies-names)
20
20
  + [Expiration time](#expiration-time)
21
21
  + [CSRF and cookies](#csrf-and-cookies)
22
+ + [Refresh with access token](#refresh-with-access-token)
22
23
  + [Refresh token hijack protection](#refresh-token-hijack-protection)
23
24
  - [Flush Sessions](#flush-sessions)
24
25
  + [Sessions Namespace](#sessions-namespace)
26
+ + [Logout](#logout)
25
27
  - [Examples](#examples)
26
28
  - [Contributing](#contributing)
27
29
  - [License](#license)
@@ -372,6 +374,60 @@ session = JWTSessions::Session.new
372
374
  session.masked_csrf(access_token)
373
375
  ```
374
376
 
377
+ ##### Refresh with access token
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.
382
+ Example Rails login controller, which passes an access token token via cookies and renders CSRF.
383
+
384
+ ```ruby
385
+ class LoginController < ApplicationController
386
+ include ActionController::Cookies
387
+
388
+ def create
389
+ user = User.find_by!(email: params[:email])
390
+ if user.authenticate(params[:password])
391
+
392
+ payload = { user_id: user.id }
393
+ session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
394
+ tokens = session.login
395
+ cookies[JWTSessions.access_cookie] = tokens[:access]
396
+
397
+ render json: { csrf: tokens[:csrf] }
398
+ else
399
+ render json: 'Invalid email or password', status: :unauthorized
400
+ end
401
+ end
402
+ end
403
+ ```
404
+
405
+ The gem provides an ability to refresh the session by access token.
406
+
407
+ ```ruby
408
+ tokens = session.refresh_by_access(access_token)
409
+ ```
410
+
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.
414
+
415
+ ```ruby
416
+ class RefreshController < ApplicationController
417
+ include ActionController::Cookies
418
+ before_action :authorize_refresh_by_access_request!
419
+
420
+ 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]
424
+
425
+ render json: { csrf: tokens[:csrf] }
426
+ end
427
+ end
428
+
429
+ ```
430
+
375
431
  #### Refresh token hijack protection
376
432
 
377
433
  There is a security recommendation regarding the usage of refresh tokens: only perform refresh when an access token gets expired. \
@@ -418,6 +474,13 @@ To force flush of all app sessions
418
474
  JWTSessions::Session.flush_all
419
475
  ```
420
476
 
477
+ ##### Logout
478
+
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.
482
+ 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
+
421
484
  ## Examples
422
485
 
423
486
  [Rails API](test/support/dummy_api) \
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWTSessions
2
4
  class AccessToken
3
- attr_reader :token, :payload, :uid, :expiration, :csrf, :store
5
+ attr_reader :payload, :uid, :expiration, :csrf, :store
4
6
 
5
7
  def initialize(csrf, payload, store, uid = SecureRandom.uuid, expiration = JWTSessions.access_expiration)
6
8
  @csrf = csrf
@@ -8,13 +10,24 @@ module JWTSessions
8
10
  @expiration = expiration
9
11
  @payload = payload
10
12
  @store = store
11
- @token = Token.encode(payload.merge(uid: uid, exp: expiration.to_i))
12
13
  end
13
14
 
14
15
  def destroy
15
16
  store.destroy_access(uid)
16
17
  end
17
18
 
19
+ def refresh_uid=(uid)
20
+ self.payload['ruid'] = uid
21
+ end
22
+
23
+ def refresh_uid
24
+ payload['ruid']
25
+ end
26
+
27
+ def token
28
+ Token.encode(payload.merge(uid: uid, exp: expiration.to_i))
29
+ end
30
+
18
31
  class << self
19
32
  def create(csrf, payload, store)
20
33
  new(csrf, payload, store).tap do |inst|
@@ -21,6 +21,17 @@ module JWTSessions
21
21
  end
22
22
  end
23
23
 
24
+ def authorize_refresh_by_access_request!
25
+ begin
26
+ cookieless_auth(:access)
27
+ rescue Errors::Unauthorized
28
+ cookie_based_auth(:access)
29
+ end
30
+ # only latest access token can be used for refresh
31
+ invalid_authorization unless session_exists?(:access)
32
+ check_csrf(:access)
33
+ end
34
+
24
35
  private
25
36
 
26
37
  def invalid_authorization
@@ -2,16 +2,24 @@
2
2
 
3
3
  module JWTSessions
4
4
  class Session
5
- attr_reader :access_token, :refresh_token, :csrf_token
6
- attr_accessor :payload, :store, :refresh_payload, :namespace
5
+ attr_reader :access_token,
6
+ :refresh_token,
7
+ :csrf_token
8
+
9
+ attr_accessor :payload,
10
+ :store,
11
+ :refresh_payload,
12
+ :namespace,
13
+ :refresh_by_access_allowed
7
14
 
8
15
  def initialize(options = {})
9
- @store = options.fetch(:store, JWTSessions.token_store)
10
- @refresh_payload = options.fetch(:refresh_payload, {})
11
- @payload = options.fetch(:payload, {})
12
- @access_claims = options.fetch(:access_claims, {})
13
- @refresh_claims = options.fetch(:refresh_claims, {})
14
- @namespace = options.fetch(:namespace, nil)
16
+ @store = options.fetch(:store, JWTSessions.token_store)
17
+ @refresh_payload = options.fetch(:refresh_payload, {})
18
+ @payload = options.fetch(:payload, {})
19
+ @access_claims = options.fetch(:access_claims, {})
20
+ @refresh_claims = options.fetch(:refresh_claims, {})
21
+ @namespace = options.fetch(:namespace, nil)
22
+ @refresh_by_access_allowed = options.fetch(:refresh_by_access_allowed, false)
15
23
  end
16
24
 
17
25
  def login
@@ -42,6 +50,12 @@ module JWTSessions
42
50
  refresh_by_uid(&block)
43
51
  end
44
52
 
53
+ def refresh_by_access(token, &block)
54
+ ruid = access_token_ruid(token)
55
+ retrieve_refresh_token(ruid)
56
+ refresh_by_uid(&block)
57
+ end
58
+
45
59
  def flush_by_token(token)
46
60
  uid = token_uid(token, :refresh, @refresh_claims)
47
61
  flush_by_uid(uid)
@@ -119,6 +133,22 @@ module JWTSessions
119
133
  uid
120
134
  end
121
135
 
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
+
144
+ ruid = token_payload.fetch('ruid', nil)
145
+ if ruid.nil?
146
+ message = "Access token payload does not contain refresh uid"
147
+ raise Errors::InvalidPayload, message
148
+ end
149
+ ruid
150
+ end
151
+
122
152
  def retrieve_refresh_token(uid)
123
153
  @_refresh = RefreshToken.find(uid, store, namespace)
124
154
  end
@@ -149,6 +179,13 @@ module JWTSessions
149
179
  def update_refresh_token
150
180
  @_refresh.update(@_access.uid, @_access.expiration, @_csrf.encoded)
151
181
  @refresh_token = @_refresh.token
182
+ link_access_to_refresh
183
+ end
184
+
185
+ def link_access_to_refresh
186
+ return unless refresh_by_access_allowed
187
+ @_access.refresh_uid = @_refresh.uid
188
+ @access_token = @_access.token
152
189
  end
153
190
 
154
191
  def create_csrf_token
@@ -164,6 +201,7 @@ module JWTSessions
164
201
  refresh_payload,
165
202
  namespace)
166
203
  @refresh_token = @_refresh.token
204
+ link_access_to_refresh
167
205
  end
168
206
 
169
207
  def create_access_token
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWTSessions
4
- VERSION = '1.2.1'
4
+ VERSION = '1.3.0'
5
5
  end
@@ -33,6 +33,16 @@ class TestSession < Minitest::Test
33
33
  assert_equal payload[:test], decoded_access['test']
34
34
  end
35
35
 
36
+ def test_refresh_by_access
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])
40
+ decoded_access = JWTSessions::Token.decode(refreshed_tokens[:access]).first
41
+ assert_equal EXPECTED_KEYS, refreshed_tokens.keys.sort
42
+ assert_equal payload[:test], decoded_access['test']
43
+ assert_equal session.instance_variable_get('@_refresh').uid, decoded_access['ruid']
44
+ end
45
+
36
46
  def test_refresh_with_block_not_expired
37
47
  assert_raises JWTSessions::Errors::Unauthorized do
38
48
  session.refresh(tokens[:refresh]) do
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.2.1
4
+ version: 1.3.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-05-03 00:00:00.000000000 Z
11
+ date: 2018-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt
@@ -54,58 +54,58 @@ dependencies:
54
54
  name: bundler
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
- - - ">="
57
+ - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '0'
59
+ version: '1.16'
60
60
  type: :development
61
61
  prerelease: false
62
62
  version_requirements: !ruby/object:Gem::Requirement
63
63
  requirements:
64
- - - ">="
64
+ - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: '0'
66
+ version: '1.16'
67
67
  - !ruby/object:Gem::Dependency
68
68
  name: minitest
69
69
  requirement: !ruby/object:Gem::Requirement
70
70
  requirements:
71
- - - ">="
71
+ - - "~>"
72
72
  - !ruby/object:Gem::Version
73
- version: '0'
73
+ version: '5.11'
74
74
  type: :development
75
75
  prerelease: false
76
76
  version_requirements: !ruby/object:Gem::Requirement
77
77
  requirements:
78
- - - ">="
78
+ - - "~>"
79
79
  - !ruby/object:Gem::Version
80
- version: '0'
80
+ version: '5.11'
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: pry
83
83
  requirement: !ruby/object:Gem::Requirement
84
84
  requirements:
85
- - - ">="
85
+ - - "~>"
86
86
  - !ruby/object:Gem::Version
87
- version: '0'
87
+ version: '0.11'
88
88
  type: :development
89
89
  prerelease: false
90
90
  version_requirements: !ruby/object:Gem::Requirement
91
91
  requirements:
92
- - - ">="
92
+ - - "~>"
93
93
  - !ruby/object:Gem::Version
94
- version: '0'
94
+ version: '0.11'
95
95
  - !ruby/object:Gem::Dependency
96
96
  name: rake
97
97
  requirement: !ruby/object:Gem::Requirement
98
98
  requirements:
99
- - - ">="
99
+ - - "~>"
100
100
  - !ruby/object:Gem::Version
101
- version: '0'
101
+ version: '12.3'
102
102
  type: :development
103
103
  prerelease: false
104
104
  version_requirements: !ruby/object:Gem::Requirement
105
105
  requirements:
106
- - - ">="
106
+ - - "~>"
107
107
  - !ruby/object:Gem::Version
108
- version: '0'
108
+ version: '12.3'
109
109
  description: XSS/CSRF safe JWT auth designed for SPA
110
110
  email: yulia.oletskaya@gmail.com
111
111
  executables: []