jwt_sessions 1.1.0 → 1.2.0

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
  SHA1:
3
- metadata.gz: da93c28c622a3bac51b8f30be0190b0a3c79dda7
4
- data.tar.gz: 29da1c13c80f4b89572d8f1c3ebe07dd031f775b
3
+ metadata.gz: a82e7a774b3c7707f9766cab5daaa7931c02575b
4
+ data.tar.gz: b31598139258b52483ccd593feae380c240d1c24
5
5
  SHA512:
6
- metadata.gz: 4a21622dd516292d881f87a0b22f140b8dc7c17be838b200cfa7ccbcbb8a1a55a7e7bd075f3ea49982c06811a24fdda721f7625f3b821e4dfeff9583311942a2
7
- data.tar.gz: b5897c5ef7e0a3be5db1dcc67df71fd0eb56f4a5f3ee5008782c881b006a34e139be045e6a30e7632f5231bf425c62de3b62b150a9e92895befa522802ef346b
6
+ metadata.gz: c34675a2622944e5e715302bea401442b335abbaeede9b3f77829cee3e7a1d0d1ecc22dc6874ec23954a50f1b91e110b5ab681b3bed538f8dedc8384f67a20b6
7
+ data.tar.gz: 1d7fb0b9052c9a1df4610282b1ab39e63b9a35d0fd5b77a5d35f83417ff8d0cd52d489f470a3feecf9ede8cffa568301801ab7e1262316be0c559dcf0e0296cf
data/README.md CHANGED
@@ -20,6 +20,8 @@ XSS/CSRF safe JWT auth designed for SPA
20
20
  + [Expiration time](#expiration-time)
21
21
  + [CSRF and cookies](#csrf-and-cookies)
22
22
  + [Refresh token hijack protection](#refresh-token-hijack-protection)
23
+ - [Flush Sessions](#flush-sessions)
24
+ + [Sessions Namespace](#sessions-namespace)
23
25
  - [Examples](#examples)
24
26
  - [TODO](#todo)
25
27
  - [Contributing](#contributing)
@@ -82,7 +84,7 @@ JWTSessions.algorithm = 'HS256'
82
84
  JWTSessions.encryption_key = Rails.application.secrets.secret_jwt_encryption_key
83
85
  ```
84
86
 
85
- 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 `encryotion_key` to sign the token. For other algorithms you must specify a private and public keys separately.
87
+ 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.
86
88
 
87
89
  ```ruby
88
90
  JWTSessions.algorithm = 'RS256'
@@ -381,6 +383,42 @@ session = JwtSessions::Session.new(payload: payload)
381
383
  session.refresh(refresh_token) { |refresh_token_uid, access_token_expiration| ... }
382
384
  ```
383
385
 
386
+ ## Flush Sessions
387
+
388
+ Flush session by refresh token. The method returns number of flushed sessions.
389
+
390
+ ```ruby
391
+ session = JWTSessions::Session.new
392
+ tokens = session.login
393
+ session.flush_by_token(tokens[:refresh]) # => 1
394
+ ```
395
+
396
+ Or by refresh token UID
397
+
398
+ ```ruby
399
+ session.flush_by_uid(uid) # => 1
400
+ ```
401
+
402
+ ##### Sessions namespace
403
+
404
+ It's possible to group sessions by custom namespaces
405
+
406
+ ```ruby
407
+ session = JWTSessions::Session.new(namespace: 'account-1')
408
+ ```
409
+
410
+ and selectively flush sessions by namespace
411
+
412
+ ```ruby
413
+ session = JWTSessions::Session.new(namespace: 'ie-sessions')
414
+ session.flush_namespaced # will flush all sessions that belong to the same namespace
415
+ ```
416
+
417
+ To force flush of all app sessions
418
+ ```ruby
419
+ JWTSessions::Session.flush_all
420
+ ```
421
+
384
422
  ## Examples
385
423
 
386
424
  [Rails API](test/support/dummy_api) \
data/lib/jwt_sessions.rb CHANGED
@@ -25,7 +25,6 @@ module JWTSessions
25
25
  DEFAULT_SETTINGS_KEYS = %i[access_cookie
26
26
  access_exp_time
27
27
  access_header
28
- algorithm
29
28
  csrf_header
30
29
  redis_db_name
31
30
  redis_host
@@ -74,6 +73,10 @@ module JWTSessions
74
73
  @algorithm = algo
75
74
  end
76
75
 
76
+ def algorithm
77
+ @algorithm ||= DEFAULT_ALGORITHM
78
+ end
79
+
77
80
  def token_store
78
81
  RedisTokenStore.instance(redis_host, redis_port, redis_db_name, token_prefix)
79
82
  end
@@ -110,7 +113,6 @@ module JWTSessions
110
113
  Time.now.to_i + refresh_exp_time.to_i
111
114
  end
112
115
 
113
-
114
116
  def header_by(token_type)
115
117
  send("#{token_type}_header")
116
118
  end
@@ -16,6 +16,7 @@ module JWTSessions
16
16
  end
17
17
  # triggers token decode and jwt claim checks
18
18
  payload
19
+ invalid_authorization unless session_exists?(token_type)
19
20
  check_csrf(token_type)
20
21
  end
21
22
  end
@@ -50,6 +51,10 @@ module JWTSessions
50
51
  JWTSessions::Session.new.valid_csrf?(found_token, csrf_token, token_type)
51
52
  end
52
53
 
54
+ def session_exists?(token_type)
55
+ JWTSessions::Session.new.session_exists?(found_token, token_type)
56
+ end
57
+
53
58
  def cookieless_auth(token_type)
54
59
  @_csrf_check = false
55
60
  @_raw_token = token_from_headers(token_type)
@@ -43,26 +43,38 @@ module JWTSessions
43
43
  store.expireat(key, expiration)
44
44
  end
45
45
 
46
- def fetch_refresh(uid)
47
- keys = [:csrf, :access_uid, :access_expiration, :expiration]
48
- values = store.hmget(refresh_key(uid), *keys).compact
46
+ def fetch_refresh(uid, namespace)
47
+ keys = %i[csrf access_uid access_expiration expiration]
48
+ values = store.hmget(refresh_key(uid, namespace), *keys).compact
49
49
  return {} if values.length != keys.length
50
- keys.each_with_index.inject({}) { |acc, (key, index)| acc[key] = values[index]; acc }
50
+ keys.each_with_index.each_with_object({}) { |(key, index), acc| acc[key] = values[index]; }
51
51
  end
52
52
 
53
- def persist_refresh(uid, access_expiration, access_uid, csrf, expiration)
54
- key = refresh_key(uid)
55
- update_refresh(uid, access_expiration, access_uid, csrf)
53
+ def persist_refresh(uid, access_expiration, access_uid, csrf, expiration, namespace = nil)
54
+ ns = namespace || ''
55
+ key = refresh_key(uid, ns)
56
+ update_refresh(uid, access_expiration, access_uid, csrf, ns)
56
57
  store.hset(key, :expiration, expiration)
57
58
  store.expireat(key, expiration)
58
59
  end
59
60
 
60
- def update_refresh(uid, access_expiration, access_uid, csrf)
61
- store.hmset(refresh_key(uid), :csrf, csrf, :access_expiration, access_expiration, :access_uid, access_uid)
61
+ def update_refresh(uid, access_expiration, access_uid, csrf, namespace = nil)
62
+ store.hmset(refresh_key(uid, namespace),
63
+ :csrf, csrf,
64
+ :access_expiration, access_expiration,
65
+ :access_uid, access_uid)
62
66
  end
63
67
 
64
- def destroy_refresh(uid)
65
- store.del(refresh_key(uid))
68
+ def all_in_namespace(namespace)
69
+ keys = store.keys(refresh_key('*', namespace))
70
+ (keys || []).each_with_object({}) do |key, acc|
71
+ uid = uid_from_key(key)
72
+ acc[uid] = fetch_refresh(uid, namespace)
73
+ end
74
+ end
75
+
76
+ def destroy_refresh(uid, namespace)
77
+ store.del(refresh_key(uid, namespace))
66
78
  end
67
79
 
68
80
  def destroy_access(uid)
@@ -75,8 +87,21 @@ module JWTSessions
75
87
  "#{prefix}_access_#{uid}"
76
88
  end
77
89
 
78
- def refresh_key(uid)
79
- "#{prefix}_refresh_#{uid}"
90
+ def refresh_key(uid, namespace = nil)
91
+ if namespace
92
+ "#{prefix}_#{namespace}_refresh_#{uid}"
93
+ else
94
+ wildcard_refresh_key(uid)
95
+ end
96
+ end
97
+
98
+ def wildcard_refresh_key(uid)
99
+ keys = store.keys(refresh_key(uid, '*')) || []
100
+ keys.first
101
+ end
102
+
103
+ def uid_from_key(key)
104
+ key.split('_').last
80
105
  end
81
106
  end
82
107
  end
@@ -2,39 +2,58 @@
2
2
 
3
3
  module JWTSessions
4
4
  class RefreshToken
5
- attr_reader :expiration, :uid, :token, :csrf, :access_uid, :access_expiration, :store
5
+ attr_reader :expiration, :uid, :token, :csrf, :access_uid, :access_expiration, :store, :namespace
6
6
 
7
- def initialize(csrf, access_uid, access_expiration, store, payload = {}, uid = SecureRandom.uuid, expiration = JWTSessions.refresh_expiration)
7
+ def initialize(csrf,
8
+ access_uid,
9
+ access_expiration,
10
+ store,
11
+ options = {})
8
12
  @csrf = csrf
9
13
  @access_uid = access_uid
10
14
  @access_expiration = access_expiration
11
- @uid = uid
12
- @expiration = expiration
13
15
  @store = store
14
- @token = Token.encode(payload.merge(uid: uid, exp: expiration.to_i))
16
+ @uid = options.fetch(:uid, SecureRandom.uuid)
17
+ @expiration = options.fetch(:expiration, JWTSessions.refresh_expiration)
18
+ @namespace = options.fetch(:namespace, nil)
19
+ @token = Token.encode(options.fetch(:payload, {}).merge(uid: uid, exp: expiration.to_i))
15
20
  end
16
21
 
17
22
  class << self
18
- def create(csrf, access_uid, access_expiration, store, payload)
19
- inst = new(csrf, access_uid, access_expiration, store, payload)
23
+ def create(csrf, access_uid, access_expiration, store, payload, namespace)
24
+ inst = new(csrf, access_uid, access_expiration, store, payload: payload, namespace: namespace)
20
25
  inst.send(:persist_in_store)
21
26
  inst
22
27
  end
23
28
 
24
- def find(uid, store)
25
- token_attrs = store.fetch_refresh(uid)
29
+ def all(namespace, store)
30
+ tokens = store.all_in_namespace(namespace)
31
+ tokens.map do |uid, token_attrs|
32
+ build_with_token_attrs(store, uid, token_attrs, namespace)
33
+ end
34
+ end
35
+
36
+ def find(uid, store, namespace)
37
+ token_attrs = store.fetch_refresh(uid, namespace)
26
38
  raise Errors::Unauthorized, 'Refresh token not found' if token_attrs.empty?
39
+ build_with_token_attrs(store, uid, token_attrs, namespace)
40
+ end
41
+
42
+ def destroy(uid, store, namespace)
43
+ store.destroy_refresh(uid, namespace)
44
+ end
45
+
46
+ private
47
+
48
+ def build_with_token_attrs(store, uid, token_attrs, namespace)
27
49
  new(token_attrs[:csrf],
28
50
  token_attrs[:access_uid],
29
51
  token_attrs[:access_expiration],
30
52
  store,
31
- {},
32
- uid,
33
- token_attrs[:expiration])
34
- end
35
-
36
- def destroy(uid, store)
37
- store.destroy_refresh(uid)
53
+ namespace: namespace,
54
+ payload: {},
55
+ uid: uid,
56
+ expiration: token_attrs[:expiration])
38
57
  end
39
58
  end
40
59
 
@@ -42,17 +61,17 @@ module JWTSessions
42
61
  @csrf = csrf
43
62
  @access_uid = access_uid
44
63
  @access_expiration = access_expiration
45
- store.update_refresh(uid, access_uid, access_expiration, csrf)
64
+ store.update_refresh(uid, access_uid, access_expiration, csrf, namespace)
46
65
  end
47
66
 
48
67
  def destroy
49
- store.destroy_refresh(uid)
68
+ store.destroy_refresh(uid, namespace)
50
69
  end
51
70
 
52
71
  private
53
72
 
54
73
  def persist_in_store
55
- store.persist_refresh(uid, access_expiration, access_uid, csrf, expiration)
74
+ store.persist_refresh(uid, access_expiration, access_uid, csrf, expiration, namespace)
56
75
  end
57
76
  end
58
77
  end
@@ -3,7 +3,7 @@
3
3
  module JWTSessions
4
4
  class Session
5
5
  attr_reader :access_token, :refresh_token, :csrf_token
6
- attr_accessor :payload, :store, :refresh_payload
6
+ attr_accessor :payload, :store, :refresh_payload, :namespace
7
7
 
8
8
  def initialize(options = {})
9
9
  @store = options.fetch(:store, JWTSessions.token_store)
@@ -11,6 +11,7 @@ module JWTSessions
11
11
  @payload = options.fetch(:payload, {})
12
12
  @access_claims = options.fetch(:access_claims, {})
13
13
  @refresh_claims = options.fetch(:refresh_claims, {})
14
+ @namespace = options.fetch(:namespace, nil)
14
15
  end
15
16
 
16
17
  def login
@@ -25,6 +26,13 @@ module JWTSessions
25
26
  send(:"valid_#{token_type}_csrf?", token, csrf_token)
26
27
  end
27
28
 
29
+ def session_exists?(token, token_type = :access)
30
+ send(:"#{token_type}_token_data", token)
31
+ true
32
+ rescue Errors::Unauthorized
33
+ false
34
+ end
35
+
28
36
  def masked_csrf(access_token)
29
37
  csrf(access_token).token
30
38
  end
@@ -34,6 +42,35 @@ module JWTSessions
34
42
  refresh_by_uid(&block)
35
43
  end
36
44
 
45
+ def flush_by_token(token)
46
+ uid = token_uid(token, :refresh, @refresh_claims)
47
+ flush_by_uid(uid)
48
+ end
49
+
50
+ def flush_by_uid(uid)
51
+ token = retrieve_refresh_token(uid)
52
+
53
+ AccessToken.destroy(token.access_uid, store)
54
+ token.destroy
55
+ end
56
+
57
+ def flush_namespaced
58
+ return 0 unless namespace
59
+ tokens = RefreshToken.all(namespace, store)
60
+ tokens.each do |token|
61
+ AccessToken.destroy(token.access_uid, store)
62
+ token.destroy
63
+ end.count
64
+ end
65
+
66
+ def self.flush_all(store = JWTSessions.token_store)
67
+ tokens = RefreshToken.all(nil, store)
68
+ tokens.each do |token|
69
+ AccessToken.destroy(token.access_uid, store)
70
+ token.destroy
71
+ end.count
72
+ end
73
+
37
74
  private
38
75
 
39
76
  def valid_access_csrf?(access_token, csrf_token)
@@ -52,7 +89,6 @@ module JWTSessions
52
89
 
53
90
  def csrf(access_token)
54
91
  token_data = access_token_data(access_token)
55
- raise Errors::Unauthorized, 'Access token not found' if token_data.empty?
56
92
  CSRFToken.new(token_data[:csrf])
57
93
  end
58
94
 
@@ -63,7 +99,9 @@ module JWTSessions
63
99
 
64
100
  def access_token_data(token)
65
101
  uid = token_uid(token, :access, @access_claims)
66
- store.fetch_access(uid)
102
+ data = store.fetch_access(uid)
103
+ raise Errors::Unauthorized, 'Access token not found' if data.empty?
104
+ data
67
105
  end
68
106
 
69
107
  def refresh_token_data(token)
@@ -82,15 +120,17 @@ module JWTSessions
82
120
  end
83
121
 
84
122
  def retrieve_refresh_token(uid)
85
- @_refresh = RefreshToken.find(uid, store)
123
+ @_refresh = RefreshToken.find(uid, store, namespace)
86
124
  end
87
125
 
88
126
  def tokens_hash
89
- { csrf: csrf_token,
127
+ {
128
+ csrf: csrf_token,
90
129
  access: access_token,
91
130
  access_expires_at: Time.at(@_access.expiration.to_i),
92
131
  refresh: refresh_token,
93
- refresh_expires_at: Time.at(@_refresh.expiration.to_i) }
132
+ refresh_expires_at: Time.at(@_refresh.expiration.to_i)
133
+ }
94
134
  end
95
135
 
96
136
  def check_refresh_on_time
@@ -121,7 +161,8 @@ module JWTSessions
121
161
  @_access.uid,
122
162
  @_access.expiration,
123
163
  store,
124
- @refresh_payload)
164
+ refresh_payload,
165
+ namespace)
125
166
  @refresh_token = @_refresh.token
126
167
  end
127
168
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWTSessions
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
@@ -14,7 +14,8 @@ class TestRefreshToken < Minitest::Test
14
14
  @access_uid,
15
15
  JWTSessions.access_expiration - 5,
16
16
  JWTSessions.token_store,
17
- {})
17
+ {},
18
+ nil)
18
19
  end
19
20
 
20
21
  def test_update
@@ -27,15 +28,15 @@ class TestRefreshToken < Minitest::Test
27
28
  end
28
29
 
29
30
  def test_find
30
- found_token = JWTSessions::RefreshToken.find(token.uid, JWTSessions.token_store)
31
+ found_token = JWTSessions::RefreshToken.find(token.uid, JWTSessions.token_store, nil)
31
32
  assert_equal found_token.access_uid, token.access_uid
32
33
  token.destroy
33
34
  end
34
35
 
35
36
  def test_destroy
36
- JWTSessions::RefreshToken.destroy(token.uid, JWTSessions.token_store)
37
+ JWTSessions::RefreshToken.destroy(token.uid, JWTSessions.token_store, nil)
37
38
  assert_raises JWTSessions::Errors::Unauthorized do
38
- JWTSessions::RefreshToken.find(token.uid, JWTSessions.token_store)
39
+ JWTSessions::RefreshToken.find(token.uid, JWTSessions.token_store, nil)
39
40
  end
40
41
  end
41
42
  end
@@ -14,6 +14,12 @@ class TestSession < Minitest::Test
14
14
  @tokens = session.login
15
15
  end
16
16
 
17
+ def teardown
18
+ redis = Redis.new
19
+ keys = redis.keys('jwt_*')
20
+ keys.each { |k| redis.del(k) }
21
+ end
22
+
17
23
  def test_login
18
24
  decoded_access = JWTSessions::Token.decode(tokens[:access]).first
19
25
  assert_equal EXPECTED_KEYS, tokens.keys.sort
@@ -47,4 +53,62 @@ class TestSession < Minitest::Test
47
53
  assert_equal EXPECTED_KEYS, refreshed_tokens.keys.sort
48
54
  assert_equal payload[:test], decoded_access['test']
49
55
  end
56
+
57
+ def test_flush_by_token
58
+ refresh_token = @session.instance_variable_get(:"@_refresh")
59
+ uid = refresh_token.uid
60
+ assert_equal refresh_token.token, JWTSessions::RefreshToken.find(uid, JWTSessions.token_store, nil).token
61
+
62
+ @session.flush_by_token(refresh_token.token)
63
+
64
+ assert_raises JWTSessions::Errors::Unauthorized do
65
+ JWTSessions::RefreshToken.find(uid, JWTSessions.token_store, nil)
66
+ end
67
+ end
68
+
69
+ def test_flush_by_uid
70
+ refresh_token = @session.instance_variable_get(:"@_refresh")
71
+ uid = refresh_token.uid
72
+
73
+ @session.flush_by_uid(uid)
74
+
75
+ assert_raises JWTSessions::Errors::Unauthorized do
76
+ JWTSessions::RefreshToken.find(uid, JWTSessions.token_store, nil)
77
+ end
78
+ end
79
+
80
+ def test_flush_namespaced
81
+ namespace = 'test_namespace'
82
+ @session1 = JWTSessions::Session.new(payload: payload, namespace: namespace)
83
+ @session2 = JWTSessions::Session.new(payload: payload, namespace: namespace)
84
+ @session1.login
85
+ @session2.login
86
+
87
+ flushed_count = @session1.flush_namespaced
88
+
89
+ assert_equal 2, flushed_count
90
+ assert_raises JWTSessions::Errors::Unauthorized do
91
+ refresh_token = @session1.instance_variable_get(:"@_refresh")
92
+ JWTSessions::RefreshToken.find(refresh_token.uid, JWTSessions.token_store, nil)
93
+ end
94
+
95
+ assert_raises JWTSessions::Errors::Unauthorized do
96
+ refresh_token = @session2.instance_variable_get(:"@_refresh")
97
+ JWTSessions::RefreshToken.find(refresh_token.uid, JWTSessions.token_store, nil)
98
+ end
99
+
100
+ refresh_token = @session.instance_variable_get(:"@_refresh")
101
+ flushed_count = @session.flush_namespaced
102
+ assert_equal 0, flushed_count
103
+ assert_equal refresh_token.token, JWTSessions::RefreshToken.find(refresh_token.uid, JWTSessions.token_store, nil).token
104
+ end
105
+
106
+ def test_flush_all
107
+ refresh_token = @session.instance_variable_get(:"@_refresh")
108
+ flushed_count = JWTSessions::Session.flush_all
109
+ assert_equal 1, flushed_count
110
+ assert_raises JWTSessions::Errors::Unauthorized do
111
+ JWTSessions::RefreshToken.find(refresh_token.uid, JWTSessions.token_store, nil).token
112
+ end
113
+ end
50
114
  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: 1.1.0
4
+ version: 1.2.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-04-21 00:00:00.000000000 Z
11
+ date: 2018-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt