jwt_sessions 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4b639d9a9560b309205c8cc09a55d4e29ade5af7
4
+ data.tar.gz: 4936d31724cfe88af332f0edf5c29faefdd8ed74
5
+ SHA512:
6
+ metadata.gz: 54ba0f9e9826cebeba55de7687e5215255a827e3765b360913cf460569d5f121657a9d0b6ecdab72149403ad2d2c15e594e55a508516b4bb9f1dd10c31568727
7
+ data.tar.gz: 0d91952a603ef98df693c9daa3603c52523a7fa045a9a6c7d9ba90c9a15c21706305a657ee0870a74977ffd51ea3596b2fff9148b74313eb7916b7f755e5a997
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,27 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jwt_sessions (0.0.0)
5
+ jwt (~> 1.4)
6
+ redis (~> 3)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ coderay (1.1.2)
12
+ jwt (1.5.6)
13
+ method_source (0.9.0)
14
+ pry (0.11.3)
15
+ coderay (~> 1.1.0)
16
+ method_source (~> 0.9.0)
17
+ redis (3.3.5)
18
+
19
+ PLATFORMS
20
+ ruby
21
+
22
+ DEPENDENCIES
23
+ jwt_sessions!
24
+ pry
25
+
26
+ BUNDLED WITH
27
+ 1.16.1
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Yulia Oletskaya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # jwt_sessions
2
+ XSS/CSRF safe JWT auth designed for SPA
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new do |task|
7
+ task.test_files = FileList['test/units/test_*.rb', 'test/units/**/test_*.rb']
8
+ end
9
+ desc 'Run tests'
10
+
11
+ task default: :test
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt_sessions/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'jwt_sessions'
7
+ s.version = JWTSessions::VERSION
8
+ s.date = '2018-03-08'
9
+ s.summary = 'JWT Sessions'
10
+ s.description = 'XSS/CSRF safe JWT auth designed for SPA'
11
+ s.authors = ['Yulia Oletskaya']
12
+ s.email = 'yulia.oletskaya@gmail.com'
13
+ s.homepage = 'http://rubygems.org/gems/jwt_sessions'
14
+ s.license = 'MIT'
15
+
16
+ s.files = Dir['*', 'lib/**/*', 'LICENSE', 'README.md']
17
+ s.test_files = Dir['test/units/*', 'test/units/**/*']
18
+ s.require_paths = ['lib']
19
+
20
+ s.add_dependency 'jwt', '~>1.4'
21
+ s.add_dependency 'redis', '~>3'
22
+
23
+ s.add_development_dependency 'pry', '~>0.11'
24
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ require 'jwt_sessions/errors'
6
+ require 'jwt_sessions/token'
7
+ require 'jwt_sessions/redis_token_store'
8
+ require 'jwt_sessions/refresh_token'
9
+ require 'jwt_sessions/csrf_token'
10
+ require 'jwt_sessions/access_token'
11
+ require 'jwt_sessions/session'
12
+ require 'jwt_sessions/authorization'
13
+ require 'jwt_sessions/version'
14
+
15
+ module JWTSessions
16
+ extend self
17
+
18
+ attr_writer :token_store
19
+
20
+ DEFAULT_SETTINGS_KEYS = %i[redis_host
21
+ redis_port
22
+ redis_db_name
23
+ token_prefix
24
+ algorithm
25
+ exp_time
26
+ refresh_exp_time].freeze
27
+ DEFAULT_REDIS_HOST = '127.0.0.1'
28
+ DEFAULT_REDIS_PORT = '6379'
29
+ DEFAULT_REDIS_DB_NAME = 'jwtokens'
30
+ DEFAULT_TOKEN_PREFIX = 'jwt_'
31
+ DEFAULT_ALGORITHM = 'HS256'
32
+ DEFAULT_EXP_TIME = 3600 # 1 hour in seconds
33
+ DEFAULT_REFRESH_EXP_TIME = 604800 # 1 week in seconds
34
+
35
+
36
+ DEFAULT_SETTINGS_KEYS.each do |setting|
37
+ var_name = :"@#{setting}"
38
+
39
+ define_method(setting) do
40
+ if instance_variables.include?(var_name)
41
+ instance_variable_get(var_name)
42
+ else
43
+ instance_variable_set(var_name,
44
+ const_get("DEFAULT_#{setting.upcase}"))
45
+ end
46
+ end
47
+
48
+ define_method("#{setting}=") do |val|
49
+ instance_variable_set(var_name, val)
50
+ end
51
+ end
52
+
53
+ def token_store
54
+ RedisTokenStore.instance(redis_host, redis_port, redis_db_name, token_prefix)
55
+ end
56
+
57
+ def encryption_key
58
+ raise Errors::Malconfigured, 'encryption_key is not specified' unless @encryption_key
59
+ @encryption_key
60
+ end
61
+
62
+ def access_expiration
63
+ Time.now.to_i + exp_time.to_i
64
+ end
65
+
66
+ def refresh_expiration
67
+ Time.now.to_i + refresh_exp_time.to_i
68
+ end
69
+
70
+ def encryption_key=(key)
71
+ @encryption_key = key
72
+ end
73
+ end
@@ -0,0 +1,29 @@
1
+ module JWTSessions
2
+ class AccessToken
3
+ attr_reader :token, :payload, :uid, :expiration, :csrf
4
+
5
+ def initialize(csrf, payload, store, uid = SecureRandom.uuid, expiration = JWTSessions.access_expiration)
6
+ @csrf = csrf
7
+ @uid = uid
8
+ @expiration = expiration
9
+ @payload = payload
10
+ @token = Token.encode(payload.merge(uid: uid, exp: expiration.to_i))
11
+ end
12
+
13
+ def destroy
14
+ store.destroy_access(uid)
15
+ end
16
+
17
+ class << self
18
+ def create(csrf, payload, store)
19
+ new(csrf, payload, store).tap do |inst|
20
+ store.persist_access(inst.uid, inst.csrf, inst.expiration)
21
+ end
22
+ end
23
+
24
+ def destroy(uid, store)
25
+ store.destroy_access(uid)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWTSessions
4
+ module Authorization
5
+ CSRF_SAFE_METHODS = ['GET', 'HEAD'].freeze
6
+
7
+ protected
8
+
9
+ def authenticate_request!
10
+ begin
11
+ cookieless_auth
12
+ rescue Errors::Unauthorized
13
+ cookie_based_auth
14
+ end
15
+
16
+ invalid_authentication unless Token.valid_payload?(payload)
17
+ check_csrf
18
+ end
19
+
20
+ def invalid_authentication
21
+ raise Errors::Unauthorized
22
+ end
23
+
24
+ def get_from_payload(key)
25
+ payload[key]
26
+ end
27
+
28
+ def check_csrf
29
+ invalid_authentication if should_check_csrf? && @_csrf_check && !valid_csrf_token?(retrieve_csrf)
30
+ end
31
+
32
+ def should_check_csrf?
33
+ !CSRF_SAFE_METHODS.include?(request_method)
34
+ end
35
+
36
+ def token_header
37
+ raise Errors::Malconfigured, 'token_header is not implemented'
38
+ end
39
+
40
+ def token_cookie
41
+ raise Errors::Malconfigured, 'token_cookie is not implemented'
42
+ end
43
+
44
+ def csrf_header
45
+ raise Errors::Malconfigured, 'csrf_header is not implemented'
46
+ end
47
+
48
+ def request_headers
49
+ raise Errors::Malconfigured, 'request_headers is not implemented'
50
+ end
51
+
52
+ def request_cookies
53
+ raise Errors::Malconfigured, 'request_cookies is not implemented'
54
+ end
55
+
56
+ def request_method
57
+ raise Errors::Malconfigured, 'request_method is not implemented'
58
+ end
59
+
60
+ def valid_csrf_token?(csrf_token)
61
+ JWTSessions::Session.new.valid_csrf?(@_raw_token, csrf_token)
62
+ end
63
+
64
+ def cookieless_auth
65
+ @_csrf_check = false
66
+ @_raw_token = token_from_headers
67
+ end
68
+
69
+ def cookie_based_auth
70
+ @_csrf_check = true
71
+ @_raw_token = token_from_cookies
72
+ end
73
+
74
+ private
75
+
76
+ def retrieve_csrf
77
+ token = requset_headers[csrf_header]
78
+ raise Errors::Unauthorized, 'CSRF token is not found' unless token
79
+ token
80
+ end
81
+
82
+ def token_from_headers
83
+ raw_token = request_headers[token_header]
84
+ token = raw_token.split(' ')[-1]
85
+ raise Errors::Unauthorized, 'Token is not found among request headers' unless token
86
+ token
87
+ end
88
+
89
+ def token_from_cookies
90
+ token = request_cookies[token_cookie]
91
+ raise Errors::Unauthorized, 'Token is not found among cookies' unless token
92
+ token
93
+ end
94
+
95
+ def payload
96
+ @_payload ||= Token.decode(@token).first
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWTSessions
4
+ class CSRFToken
5
+ CSRF_LENGTH = 32
6
+
7
+ attr_reader :encoded, :token
8
+
9
+ def initialize(csrf_token = nil)
10
+ @encoded = csrf_token || SecureRandom.base64(CSRF_LENGTH)
11
+ @token = masked_token
12
+ end
13
+
14
+ def valid_authenticity_token?(encoded_masked_token)
15
+ if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
16
+ return false
17
+ end
18
+
19
+ begin
20
+ masked_token = Base64.strict_decode64(encoded_masked_token)
21
+ rescue ArgumentError
22
+ return false
23
+ end
24
+
25
+ if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
26
+ secure_compare(masked_token, raw_token)
27
+ elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
28
+ csrf_token = unmask_token(masked_token)
29
+ secure_compare(csrf_token, raw_token)
30
+ else
31
+ false
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def secure_compare(a, b)
38
+ return false unless a.bytesize == b.bytesize
39
+
40
+ l = a.unpack "C#{a.bytesize}"
41
+
42
+ res = 0
43
+ b.each_byte { |byte| res |= byte ^ l.shift }
44
+ res == 0
45
+ end
46
+
47
+ def unmask_token(masked_token)
48
+ one_time_pad = masked_token[0...CSRF_LENGTH]
49
+ encrypted_csrf_token = masked_token[CSRF_LENGTH..-1]
50
+ xor_byte_strings(one_time_pad, encrypted_csrf_token)
51
+ end
52
+
53
+ def masked_token
54
+ one_time_pad = SecureRandom.random_bytes(CSRF_LENGTH)
55
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
56
+ masked_token = one_time_pad + encrypted_csrf_token
57
+ Base64.strict_encode64(masked_token)
58
+ end
59
+
60
+ def raw_token
61
+ Base64.strict_decode64(encoded)
62
+ end
63
+
64
+ def xor_byte_strings(s1, s2)
65
+ s2_bytes = s2.bytes
66
+ s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
67
+ s2_bytes.pack("C*")
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,8 @@
1
+ module JWTSessions
2
+ module Errors
3
+ class Error < RuntimeError; end
4
+ class Malconfigured < Error; end
5
+ class InvalidPayload < Error; end
6
+ class Unauthorized < Error; end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module JWTSessions
2
+ module RailsAuthorization
3
+ include Authorization
4
+
5
+ def request_headers
6
+ request.headers
7
+ end
8
+
9
+ def request_cookies
10
+ request.cookie_jar
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+
5
+ module JWTSessions
6
+ class RedisTokenStore
7
+ class << self
8
+ def instance(redis_host, redis_port, redis_db_name, prefix)
9
+ @_tokens_store ||= Redis.new(url: "redis://#{redis_host}:#{redis_port}/#{redis_db_name}")
10
+ @_token_prefix ||= prefix
11
+
12
+ new(@_tokens_store, @_token_prefix)
13
+ end
14
+
15
+ def clear
16
+ @_tokens_store = nil
17
+ @_token_prefix = nil
18
+ end
19
+
20
+ private
21
+
22
+ def new(store, prefix)
23
+ super(store, prefix)
24
+ end
25
+ end
26
+
27
+ attr_reader :store, :prefix
28
+
29
+ def initialize(store, prefix)
30
+ @store = store
31
+ @prefix = prefix
32
+ end
33
+
34
+ def fetch_access(uid)
35
+ csrf = store.get(access_key(uid))
36
+ return {} if csrf.nil?
37
+ { csrf: csrf }
38
+ end
39
+
40
+ def persist_access(uid, csrf, expiration)
41
+ key = access_key(uid)
42
+ store.set(key, csrf)
43
+ store.expireat(key, expiration)
44
+ end
45
+
46
+ def fetch_refresh(uid)
47
+ keys = [:csrf, :access_uid, :access_expiration, :expiration]
48
+ values = store.hmget(refresh_key(uid), *keys)
49
+ return {} if values.empty?
50
+ keys.each_with_index.inject({}) { |acc, (key, index)| acc[key] = values[index]; acc }
51
+ end
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)
56
+ store.hset(key, :expiration, expiration)
57
+ store.expireat(key, expiration)
58
+ end
59
+
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)
62
+ end
63
+
64
+ def destroy_refresh(uid)
65
+ store.del(refresh_key(uid))
66
+ end
67
+
68
+ def destroy_access(uid)
69
+ store.del(access_key(uid))
70
+ end
71
+
72
+ private
73
+
74
+ def access_key(uid)
75
+ "#{prefix}_access_#{uid}"
76
+ end
77
+
78
+ def refresh_key(uid)
79
+ "#{prefix}_refresh_#{uid}"
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWTSessions
4
+ class RefreshToken
5
+ attr_reader :expiration, :uid, :token, :csrf, :access_uid, :access_expiration, :store
6
+
7
+ def initialize(csrf, access_uid, access_expiration, store, uid = SecureRandom.uuid, expiration = JWTSessions.refresh_expiration)
8
+ @csrf = csrf
9
+ @access_uid = access_uid
10
+ @access_expiration = access_expiration
11
+ @uid = uid
12
+ @expiration = expiration
13
+ @store = store
14
+ @token = Token.encode(uid: uid, exp: expiration.to_i)
15
+ end
16
+
17
+ class << self
18
+ def create(csrf, access_uid, access_expiration, store)
19
+ inst = new(csrf, access_uid, access_expiration, store)
20
+ inst.send(:persist_in_store)
21
+ inst
22
+ end
23
+
24
+ def find(uid, store)
25
+ token_attrs = store.fetch_refresh(uid)
26
+ raise Errors::Unauthorized, 'Refresh token not found' if token_attrs.empty?
27
+ new(token_attrs[:csrf],
28
+ token_attrs[:access_uid],
29
+ token_attrs[:access_expiration],
30
+ store,
31
+ uid,
32
+ token_attrs[:expiration])
33
+ end
34
+
35
+ def destroy(uid, store)
36
+ store.destroy_refresh(uid)
37
+ end
38
+ end
39
+
40
+ def update(access_uid, access_expiration, csrf)
41
+ store.update_refresh(uid, access_uid, access_expiration, csrf)
42
+ end
43
+
44
+ def destroy
45
+ store.destroy_refresh(uid)
46
+ end
47
+
48
+ private
49
+
50
+ def persist_in_store
51
+ store.persist_refresh(uid, access_expiration, access_uid, csrf, expiration)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWTSessions
4
+ class Session
5
+ attr_reader :access_token, :refresh_token, :csrf_token
6
+ attr_accessor :payload, :store
7
+
8
+ def initialize(options = {})
9
+ @store = options.fetch(:store, JWTSessions.token_store)
10
+ @payload = options.fetch(:payload, {})
11
+ end
12
+
13
+ def login
14
+ create_csrf_token
15
+ create_access_token
16
+ create_refresh_token
17
+
18
+ tokens_hash
19
+ end
20
+
21
+ def valid_csrf?(access_token, csrf_token)
22
+ csrf(access_token).valid_authenticity_token?(csrf_token)
23
+ end
24
+
25
+ def masked_csrf(access_token)
26
+ csrf(access_token).token
27
+ end
28
+
29
+ def refresh(refresh_token, &block)
30
+ refresh_token_data(refresh_token)
31
+ refresh_by_uid(&block)
32
+ end
33
+
34
+ private
35
+
36
+ def refresh_by_uid(&block)
37
+ check_refresh_on_time(&block) if block_given?
38
+ AccessToken.destroy(@_refresh.access_uid, store)
39
+ issue_tokens_after_refresh
40
+ end
41
+
42
+ def csrf(access_token)
43
+ token_data = access_token_data(access_token)
44
+ raise Errors::Unauthorized, 'Access token not found' if token_data.empty?
45
+ CSRFToken.new(token_data[:csrf])
46
+ end
47
+
48
+ def access_token_data(token)
49
+ uid = token_uid(token, :access)
50
+ store.fetch_access(uid)
51
+ end
52
+
53
+ def refresh_token_data(token)
54
+ uid = token_uid(token, :refresh)
55
+ retrieve_refresh_token(uid)
56
+ end
57
+
58
+ def token_uid(token, type)
59
+ token_payload = JWTSessions::Token.decode(refresh_token).first
60
+ uid = token_payload.fetch('uid', nil)
61
+ if uid.nil?
62
+ message = "#{type.to_s.capitalize} token payload does not contain token uid"
63
+ raise Errors::InvalidPayload, message
64
+ end
65
+ uid
66
+ end
67
+
68
+ def retrieve_refresh_token(uid)
69
+ @_refresh = RefreshToken.find(uid, store)
70
+ end
71
+
72
+ def tokens_hash
73
+ { csrf: csrf_token, access: access_token, refresh: refresh_token }
74
+ end
75
+
76
+ def check_refresh_on_time
77
+ expiration = @_refresh.access_expiration
78
+ yield @_refresh.uid, expiration if expiration.to_i > Time.now.to_i
79
+ end
80
+
81
+ def issue_tokens_after_refresh
82
+ create_csrf_token
83
+ create_access_token
84
+ update_refresh_token
85
+
86
+ tokens_hash
87
+ end
88
+
89
+ def update_refresh_token
90
+ @_refresh.update(@_access.uid, @_access.expiration, @_csrf.encoded)
91
+ @refresh_token = @_refresh.token
92
+ end
93
+
94
+ def create_csrf_token
95
+ @_csrf = CSRFToken.new
96
+ @csrf_token = @_csrf.token
97
+ end
98
+
99
+ def create_refresh_token
100
+ @_refresh = RefreshToken.create(@_csrf.encoded, @_access.uid, @_access.expiration, store)
101
+ @refresh_token = @_refresh.token
102
+ end
103
+
104
+ def create_access_token
105
+ @_access = AccessToken.create(@_csrf.encoded, payload, store)
106
+ @access_token = @_access.token
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module JWTSessions
6
+ class Token
7
+ class << self
8
+ def encode(payload)
9
+ exp_payload = meta.merge(payload)
10
+ JWT.encode(exp_payload, JWTSessions.encryption_key, JWTSessions.algorithm)
11
+ end
12
+
13
+ def decode(token)
14
+ JWT.decode(token, JWTSessions.encryption_key, true, { algorithm: JWTSessions.algorithm, verify_expiration: false })
15
+ rescue JWT::DecodeError => e
16
+ raise Errors::Unauthorized, e.message
17
+ rescue StandardError
18
+ raise Errors::Unauthorized, 'could not decode a token'
19
+ end
20
+
21
+ def valid_payload?(payload)
22
+ !expired?(payload)
23
+ end
24
+
25
+ def meta
26
+ { exp: JWTSessions.access_expiration }
27
+ end
28
+
29
+ def expired?(payload)
30
+ Time.at(payload['exp']) < Time.now
31
+ rescue StandardError
32
+ raise Errors::Unauthorized, 'invalid payload expiration time'
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWTSessions
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'jwt_sessions'
5
+
6
+ class TestSession < Minitest::Test
7
+ attr_accessor :session, :payload, :tokens
8
+ EXPECTED_KEYS = [:access, :csrf, :refresh].freeze
9
+
10
+ def setup
11
+ JWTSessions.encryption_key = '65994c7b523a3232e7aba54d8cbf'
12
+ @payload = { test: 'test' }
13
+ @session = JWTSessions::Session.new(payload: payload)
14
+ @tokens = session.login
15
+ end
16
+
17
+ def test_login
18
+ decoded_access = JWTSessions::Token.decode(tokens[:access]).first
19
+ assert_equal EXPECTED_KEYS, tokens.keys.sort
20
+ assert_equal payload[:test], decoded_access['test']
21
+ end
22
+
23
+ def test_refresh
24
+ refreshed_tokens = session.refresh(tokens[:refresh])
25
+ decoded_access = JWTSessions::Token.decode(refreshed_tokens[:access]).first
26
+ assert_equal EXPECTED_KEYS, refreshed_tokens.keys.sort
27
+ assert_equal payload[:test], decoded_access['test']
28
+ end
29
+
30
+ def test_refresh_with_block_not_expired
31
+ assert_raises JWTSessions::Errors::Unauthorized do
32
+ session.refresh(tokens[:refresh]) do
33
+ raise JWTSessions::Errors::Unauthorized
34
+ end
35
+ end
36
+ end
37
+
38
+ def test_refresh_with_block_expired
39
+ JWTSessions.exp_time = 0
40
+ @session = JWTSessions::Session.new(payload: payload)
41
+ @tokens = session.login
42
+ refreshed_tokens = session.refresh(tokens[:refresh]) do
43
+ raise JWTSessions::Errors::Unauthorized
44
+ end
45
+ JWTSessions.exp_time = 3600
46
+ decoded_access = JWTSessions::Token.decode(refreshed_tokens[:access]).first
47
+ assert_equal EXPECTED_KEYS, refreshed_tokens.keys.sort
48
+ assert_equal payload[:test], decoded_access['test']
49
+ end
50
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'jwt_sessions'
5
+
6
+ class TestToken < Minitest::Test
7
+ attr_accessor :payload
8
+
9
+ def setup
10
+ JWTSessions.encryption_key = '65994c7b523a3232e7aba54d8cbf'
11
+ @payload = { 'user_id' => 1 }
12
+ end
13
+
14
+ def test_valid_token_decode
15
+ token = JWTSessions::Token.encode(payload)
16
+ assert_equal payload['user_id'], JWTSessions::Token.decode(token).first['user_id']
17
+ end
18
+
19
+ def test_invalid_token_decode
20
+ assert_raises JWTSessions::Errors::Unauthorized do
21
+ JWTSessions::Token.decode('abc')
22
+ end
23
+ assert_raises JWTSessions::Errors::Unauthorized do
24
+ JWTSessions::Token.decode('')
25
+ end
26
+ assert_raises JWTSessions::Errors::Unauthorized do
27
+ JWTSessions::Token.decode(nil)
28
+ end
29
+ end
30
+
31
+ def test_payload_exp_time
32
+ assert_raises JWTSessions::Errors::Unauthorized do
33
+ JWTSessions::Token.valid_payload?(payload)
34
+ end
35
+ payload['exp'] = Time.now - (3600 * 24)
36
+ assert_equal false, JWTSessions::Token.valid_payload?(payload)
37
+ payload['exp'] = Time.now + (3600 * 24)
38
+ assert_equal true, JWTSessions::Token.valid_payload?(payload)
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'jwt_sessions'
5
+
6
+ class TestJWTSessions < Minitest::Test
7
+ def test_default_settings
8
+ assert_equal JWTSessions::DEFAULT_REDIS_HOST, JWTSessions.redis_host
9
+ assert_equal JWTSessions::DEFAULT_REDIS_DB_NAME, JWTSessions.redis_db_name
10
+ assert_equal JWTSessions::DEFAULT_TOKEN_PREFIX, JWTSessions.token_prefix
11
+ assert_equal JWTSessions::DEFAULT_ALGORITHM, JWTSessions.algorithm
12
+ assert_equal JWTSessions::DEFAULT_EXP_TIME, JWTSessions.exp_time
13
+ assert_equal JWTSessions::DEFAULT_REFRESH_EXP_TIME, JWTSessions.refresh_exp_time
14
+ end
15
+
16
+ def test_encryption_key
17
+ JWTSessions.encryption_key = nil
18
+ assert_raises JWTSessions::Errors::Malconfigured do
19
+ JWTSessions.encryption_key
20
+ end
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jwt_sessions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Yulia Oletskaya
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.11'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.11'
55
+ description: XSS/CSRF safe JWT auth designed for SPA
56
+ email: yulia.oletskaya@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - Gemfile
62
+ - Gemfile.lock
63
+ - LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - jwt_sessions.gemspec
67
+ - lib/jwt_sessions.rb
68
+ - lib/jwt_sessions/access_token.rb
69
+ - lib/jwt_sessions/authorization.rb
70
+ - lib/jwt_sessions/csrf_token.rb
71
+ - lib/jwt_sessions/errors.rb
72
+ - lib/jwt_sessions/rails_authorization.rb
73
+ - lib/jwt_sessions/redis_token_store.rb
74
+ - lib/jwt_sessions/refresh_token.rb
75
+ - lib/jwt_sessions/session.rb
76
+ - lib/jwt_sessions/token.rb
77
+ - lib/jwt_sessions/version.rb
78
+ - test/units/jwt_sessions/test_session.rb
79
+ - test/units/jwt_sessions/test_token.rb
80
+ - test/units/test_jwt_sessions.rb
81
+ homepage: http://rubygems.org/gems/jwt_sessions
82
+ licenses:
83
+ - MIT
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.6.13
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: JWT Sessions
105
+ test_files:
106
+ - test/units/test_jwt_sessions.rb
107
+ - test/units/jwt_sessions/test_session.rb
108
+ - test/units/jwt_sessions/test_token.rb