jwt_sessions 1.1.0 → 1.2.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: 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