slots-jwt 0.0.4 → 0.1.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 +4 -4
- data/README.md +121 -6
- data/app/controllers/slots/jwt/sessions_controller.rb +38 -0
- data/app/models/slots/jwt/application_record.rb +9 -0
- data/app/models/slots/jwt/session.rb +44 -0
- data/config/initializers/inflections.rb +3 -0
- data/config/routes.rb +2 -2
- data/lib/generators/slots/install/USAGE +1 -1
- data/lib/generators/slots/install/install_generator.rb +1 -1
- data/lib/generators/slots/install/templates/create_slots_sessions.rb +2 -2
- data/lib/generators/slots/install/templates/slots.rb +1 -1
- data/lib/generators/slots/model/model_generator.rb +1 -1
- data/lib/slots.rb +1 -44
- data/lib/slots/jwt.rb +49 -0
- data/lib/slots/jwt/authentication_helper.rb +147 -0
- data/lib/slots/jwt/configuration.rb +84 -0
- data/lib/slots/jwt/database_authentication.rb +21 -0
- data/lib/slots/jwt/engine.rb +9 -0
- data/lib/slots/jwt/extra_classes.rb +14 -0
- data/lib/slots/jwt/generic_methods.rb +53 -0
- data/lib/slots/jwt/generic_validations.rb +53 -0
- data/lib/slots/jwt/permission_filter.rb +37 -0
- data/lib/slots/jwt/slokens.rb +115 -0
- data/lib/slots/jwt/tests.rb +37 -0
- data/lib/slots/jwt/tokens.rb +104 -0
- data/lib/slots/jwt/type_helper.rb +30 -0
- data/lib/slots/{version.rb → jwt/version.rb} +3 -1
- data/lib/tasks/slots_tasks.rake +5 -5
- metadata +23 -19
- data/app/controllers/slots/sessions_controller.rb +0 -36
- data/app/mailers/slots/application_mailer.rb +0 -8
- data/app/models/slots/application_record.rb +0 -7
- data/app/models/slots/session.rb +0 -42
- data/lib/slots/authentication_helper.rb +0 -144
- data/lib/slots/configuration.rb +0 -82
- data/lib/slots/database_authentication.rb +0 -19
- data/lib/slots/engine.rb +0 -7
- data/lib/slots/extra_classes.rb +0 -12
- data/lib/slots/generic_methods.rb +0 -51
- data/lib/slots/generic_validations.rb +0 -51
- data/lib/slots/slokens.rb +0 -113
- data/lib/slots/tests.rb +0 -35
- data/lib/slots/tokens.rb +0 -102
data/lib/slots/configuration.rb
DELETED
@@ -1,82 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'yaml'
|
4
|
-
|
5
|
-
module Slots
|
6
|
-
class Configuration
|
7
|
-
attr_accessor :login_regex_validations, :token_lifetime, :session_lifetime, :previous_jwt_lifetime
|
8
|
-
attr_reader :logins
|
9
|
-
attr_writer :authentication_model
|
10
|
-
|
11
|
-
# raise_no_error is used for rake to load
|
12
|
-
def initialize
|
13
|
-
@logins = {email: //}
|
14
|
-
@login_regex_validations = true
|
15
|
-
@authentication_model = 'User'
|
16
|
-
@secret_keys = [{created_at: 0, secret: ENV['SLOT_SECRET']}]
|
17
|
-
@token_lifetime = 1.hour
|
18
|
-
@session_lifetime = 2.weeks # Set to nil if you dont want sessions
|
19
|
-
@previous_jwt_lifetime = 5.seconds # Set to nil if you dont want sessions
|
20
|
-
@manage_callbacks = Proc.new { }
|
21
|
-
end
|
22
|
-
|
23
|
-
def logins=(value)
|
24
|
-
if value.is_a? Symbol
|
25
|
-
@logins = {value => //}
|
26
|
-
elsif value.is_a?(Hash)
|
27
|
-
# Should do most inclusive regex last
|
28
|
-
raise 'must be hash of symbols => regex' unless value.length > 0 && value.all? { |k, v| k.is_a?(Symbol) && v.is_a?(Regexp) }
|
29
|
-
@logins = value
|
30
|
-
else
|
31
|
-
raise 'must be a symbol or hash'
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def authentication_model
|
36
|
-
@authentication_model.to_s.constantize rescue nil
|
37
|
-
end
|
38
|
-
|
39
|
-
def secret=(v)
|
40
|
-
@secret_keys = [{created_at: 0, secret: v}]
|
41
|
-
end
|
42
|
-
|
43
|
-
def secret_yaml=(file_path_string)
|
44
|
-
secret_keys = YAML.load_file(Slots.secret_yaml_file)
|
45
|
-
@secret_keys = []
|
46
|
-
secret_keys.each do |secret_key|
|
47
|
-
raise ArgumentError, 'Need CREATED_AT' unless (created_at = secret_key['CREATED_AT']&.to_i)
|
48
|
-
raise ArgumentError, 'Need SECRET' unless (secret = secret_key['SECRET'])
|
49
|
-
previous_created_at = @secret_keys[-1]&.dig(:created_at) || Time.now.to_i
|
50
|
-
|
51
|
-
raise ArgumentError, 'CREATED_AT must be newest to latest' unless previous_created_at > created_at
|
52
|
-
@secret_keys.push(
|
53
|
-
created_at: created_at,
|
54
|
-
secret: secret
|
55
|
-
)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def secret(at = Time.now.to_i)
|
60
|
-
@secret_keys.each do |secret_hash|
|
61
|
-
return secret_hash[:secret] if at > secret_hash[:created_at]
|
62
|
-
end
|
63
|
-
raise InvalidSecret, 'Invalid Secret'
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
class << self
|
68
|
-
attr_writer :configuration
|
69
|
-
|
70
|
-
def configuration
|
71
|
-
@configuration ||= Configuration.new
|
72
|
-
end
|
73
|
-
|
74
|
-
def configure
|
75
|
-
yield configuration
|
76
|
-
end
|
77
|
-
|
78
|
-
def secret_yaml_file
|
79
|
-
Rails.root.join('config', 'slots_secrets.yml')
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Slots
|
4
|
-
module DatabaseAuthentication
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
included do
|
8
|
-
has_secure_password
|
9
|
-
end
|
10
|
-
|
11
|
-
# TODO allow super
|
12
|
-
def as_json(*)
|
13
|
-
super.except('password_digest')
|
14
|
-
end
|
15
|
-
|
16
|
-
module ClassMethods
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
data/lib/slots/engine.rb
DELETED
data/lib/slots/extra_classes.rb
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Slots
|
4
|
-
module GenericMethods
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
included do
|
8
|
-
end
|
9
|
-
|
10
|
-
def allowed_new_token?
|
11
|
-
!(self.class._reject_new_token?(self))
|
12
|
-
end
|
13
|
-
|
14
|
-
def run_token_created_callback
|
15
|
-
self.class._token_created_callback(self)
|
16
|
-
end
|
17
|
-
|
18
|
-
def authenticate?(password)
|
19
|
-
password.present? && persisted? && respond_to?(:authenticate) && authenticate(password) && allowed_new_token?
|
20
|
-
end
|
21
|
-
|
22
|
-
def authenticate!(password)
|
23
|
-
raise Slots::AuthenticationFailed unless self.authenticate?(password)
|
24
|
-
true
|
25
|
-
end
|
26
|
-
|
27
|
-
module ClassMethods
|
28
|
-
def find_for_authentication(login)
|
29
|
-
Slots.configuration.logins.each do |k, v|
|
30
|
-
next unless login&.match(v)
|
31
|
-
return find_by(arel_table[k].lower.eq(login.downcase)) || new
|
32
|
-
end
|
33
|
-
new
|
34
|
-
end
|
35
|
-
|
36
|
-
def reject_new_token(&block)
|
37
|
-
(@_reject_new_token ||= []).push(block)
|
38
|
-
end
|
39
|
-
def _reject_new_token?(user)
|
40
|
-
(@_reject_new_token ||= []).any? { |b| user.instance_eval &b }
|
41
|
-
end
|
42
|
-
|
43
|
-
def token_created_callback(&block)
|
44
|
-
(@_token_created_callback ||= []).push(block)
|
45
|
-
end
|
46
|
-
def _token_created_callback(user)
|
47
|
-
(@_token_created_callback ||= []).each { |b| user.instance_eval &b }
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Slots
|
4
|
-
module GenericValidations
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
included do
|
8
|
-
validate :unique_and_present, :logins_meets_criteria
|
9
|
-
end
|
10
|
-
|
11
|
-
def logins_meets_criteria
|
12
|
-
return if self.errors.any?
|
13
|
-
return unless Slots.configuration.login_regex_validations
|
14
|
-
logins = Slots.configuration.logins
|
15
|
-
login_c = logins.keys
|
16
|
-
logins.each do |col, reg|
|
17
|
-
login_c.delete(col) # Login columns left
|
18
|
-
column_match(reg, col)
|
19
|
-
column_dont_match(reg, col, login_c)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def unique_and_present
|
24
|
-
# Use this rather than validates because logins in configure might not be set yet on include
|
25
|
-
Slots.configuration.logins.each do |column, _|
|
26
|
-
value = self.send(column)
|
27
|
-
next self.errors.add(column, "can't be blank") unless value.present?
|
28
|
-
|
29
|
-
pk_value = self.send(self.class.primary_key)
|
30
|
-
lower_case = self.class.arel_table[column].lower.eq(value.downcase)
|
31
|
-
next unless self.class.where.not(self.class.primary_key => pk_value).where(lower_case).exists?
|
32
|
-
self.errors.add(column, "has already been taken")
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def column_match(regex, column)
|
37
|
-
# TODO change error message to use locals? or something configurable
|
38
|
-
self.errors.add(column, "didn't match login criteria") unless self.send(column).match(regex)
|
39
|
-
end
|
40
|
-
|
41
|
-
def column_dont_match(regex, column_not_to_match, columns)
|
42
|
-
columns.each do |c|
|
43
|
-
# Since we check if any errors should be present
|
44
|
-
self.errors.add(c, "matched #{column_not_to_match} login criteria") if self.send(c).match(regex)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
module ClassMethods
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
data/lib/slots/slokens.rb
DELETED
@@ -1,113 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'jwt'
|
4
|
-
module Slots
|
5
|
-
class Slokens
|
6
|
-
attr_reader :token, :exp, :iat, :extra_payload, :authentication_model_values, :session
|
7
|
-
def initialize(decode: false, encode: false, token: nil, authentication_record: nil, extra_payload: nil, session: nil)
|
8
|
-
if decode
|
9
|
-
@token = token
|
10
|
-
decode()
|
11
|
-
elsif encode
|
12
|
-
@authentication_model_values = authentication_record.as_json
|
13
|
-
@extra_payload = extra_payload.as_json
|
14
|
-
@session = session
|
15
|
-
update_iat
|
16
|
-
update_exp
|
17
|
-
encode()
|
18
|
-
@valid = true
|
19
|
-
else
|
20
|
-
raise 'must encode or decode'
|
21
|
-
end
|
22
|
-
end
|
23
|
-
def self.decode(token)
|
24
|
-
self.new(decode: true, token: token)
|
25
|
-
end
|
26
|
-
def self.encode(authentication_record, session = '', extra_payload)
|
27
|
-
self.new(encode: true, authentication_record: authentication_record, session: session, extra_payload: extra_payload)
|
28
|
-
end
|
29
|
-
|
30
|
-
def expired?
|
31
|
-
@expired
|
32
|
-
end
|
33
|
-
|
34
|
-
def valid?
|
35
|
-
@valid
|
36
|
-
end
|
37
|
-
|
38
|
-
def valid!
|
39
|
-
raise InvalidToken, "Invalid Token" unless valid?
|
40
|
-
self
|
41
|
-
end
|
42
|
-
|
43
|
-
def update_token_data(authentication_record, extra_payload)
|
44
|
-
@authentication_model_values = authentication_record.as_json
|
45
|
-
@extra_payload = extra_payload.as_json
|
46
|
-
update_iat
|
47
|
-
encode
|
48
|
-
end
|
49
|
-
|
50
|
-
def update_token(authentication_record, extra_payload)
|
51
|
-
update_exp
|
52
|
-
update_token_data(authentication_record, extra_payload)
|
53
|
-
end
|
54
|
-
|
55
|
-
def payload
|
56
|
-
{
|
57
|
-
authentication_model_key => @authentication_model_values,
|
58
|
-
'exp' => @exp,
|
59
|
-
'iat' => @iat,
|
60
|
-
'session' => @session,
|
61
|
-
'extra_payload' => @extra_payload,
|
62
|
-
}
|
63
|
-
end
|
64
|
-
|
65
|
-
private
|
66
|
-
def authentication_model_key
|
67
|
-
Slots.configuration.authentication_model.name.underscore
|
68
|
-
end
|
69
|
-
|
70
|
-
def default_expected_keys
|
71
|
-
['exp', 'iat', 'session', authentication_model_key]
|
72
|
-
end
|
73
|
-
def secret
|
74
|
-
Slots.configuration.secret(@iat)
|
75
|
-
end
|
76
|
-
def update_iat
|
77
|
-
@iat = Time.now.to_i
|
78
|
-
end
|
79
|
-
def update_exp
|
80
|
-
@exp = Slots.configuration.token_lifetime.from_now.to_i
|
81
|
-
end
|
82
|
-
def encode
|
83
|
-
@token = JWT.encode self.payload, secret, 'HS256'
|
84
|
-
@expired = false
|
85
|
-
@valid = true
|
86
|
-
end
|
87
|
-
|
88
|
-
def decode
|
89
|
-
begin
|
90
|
-
set_payload
|
91
|
-
JWT.decode @token, secret, true, verify_iat: true, algorithm: 'HS256'
|
92
|
-
rescue JWT::ExpiredSignature
|
93
|
-
@expired = true
|
94
|
-
rescue JWT::InvalidIatError, JWT::VerificationError, JWT::DecodeError, Slots::InvalidSecret, NoMethodError, JSON::ParserError
|
95
|
-
@valid = false
|
96
|
-
else
|
97
|
-
@valid = payload.slice(*default_expected_keys).compact.length == default_expected_keys.length
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def set_payload
|
102
|
-
encoded64 = @token.split('.')[1] || ''
|
103
|
-
string_payload = Base64.decode64(encoded64)
|
104
|
-
local_payload = JSON.parse(string_payload)
|
105
|
-
raise JSON::ParserError unless local_payload.is_a?(Hash)
|
106
|
-
@exp = local_payload['exp']&.to_i
|
107
|
-
@iat = local_payload['iat']&.to_i
|
108
|
-
@session = local_payload['session']
|
109
|
-
@authentication_model_values = local_payload[authentication_model_key]
|
110
|
-
@extra_payload = local_payload['extra_payload']
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
data/lib/slots/tests.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Slots
|
4
|
-
module Tests
|
5
|
-
def authorized_get(current_user, url, headers: {}, **options)
|
6
|
-
authorized_protocal :get, current_user, url, headers: headers, **options
|
7
|
-
end
|
8
|
-
def authorized_post(current_user, url, headers: {}, **options)
|
9
|
-
authorized_protocal :post, current_user, url, headers: headers, **options
|
10
|
-
end
|
11
|
-
def authorized_patch(current_user, url, headers: {}, **options)
|
12
|
-
authorized_protocal :patch, current_user, url, headers: headers, **options
|
13
|
-
end
|
14
|
-
def authorized_put(current_user, url, headers: {}, **options)
|
15
|
-
authorized_protocal :put, current_user, url, headers: headers, **options
|
16
|
-
end
|
17
|
-
def authorized_delete(current_user, url, headers: {}, **options)
|
18
|
-
authorized_protocal :delete, current_user, url, headers: headers, **options
|
19
|
-
end
|
20
|
-
|
21
|
-
def authorized_protocal(type, current_user, url, headers: {}, session: false, **options)
|
22
|
-
@token = current_user&.create_token(session)
|
23
|
-
headers = headers.merge(token_header(@token)) if @token
|
24
|
-
send(type, url, headers: headers, **options)
|
25
|
-
end
|
26
|
-
|
27
|
-
def current_token
|
28
|
-
@token
|
29
|
-
end
|
30
|
-
|
31
|
-
def token_header(token)
|
32
|
-
{'authorization' => %{Bearer token="#{token}"}}
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
data/lib/slots/tokens.rb
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Slots
|
4
|
-
module Tokens
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
included do
|
8
|
-
end
|
9
|
-
|
10
|
-
def jwt_identifier
|
11
|
-
send(self.class.jwt_identifier_column)
|
12
|
-
end
|
13
|
-
|
14
|
-
def create_token(have_session)
|
15
|
-
session = ''
|
16
|
-
if have_session && Slots.configuration.session_lifetime
|
17
|
-
@new_session = self.sessions.new(jwt_iat: 0)
|
18
|
-
# Session should never be invalid since its all programmed
|
19
|
-
raise 'Session not valid' unless @new_session.valid?
|
20
|
-
session = @new_session.session
|
21
|
-
end
|
22
|
-
@slots_jwt = Slots::Slokens.encode(self, session, extra_payload)
|
23
|
-
if @new_session
|
24
|
-
@new_session.jwt_iat = @slots_jwt.iat
|
25
|
-
@new_session.save!
|
26
|
-
end
|
27
|
-
@new_token = true
|
28
|
-
run_token_created_callback
|
29
|
-
token
|
30
|
-
end
|
31
|
-
|
32
|
-
def extra_payload
|
33
|
-
@extra_payload || {}
|
34
|
-
end
|
35
|
-
|
36
|
-
def token
|
37
|
-
@slots_jwt&.token
|
38
|
-
end
|
39
|
-
|
40
|
-
def jwt
|
41
|
-
@slots_jwt
|
42
|
-
end
|
43
|
-
def set_token!(slots_jwt)
|
44
|
-
@slots_jwt = slots_jwt
|
45
|
-
self
|
46
|
-
end
|
47
|
-
|
48
|
-
def update_session
|
49
|
-
return false unless valid_in_database?
|
50
|
-
return false unless allowed_new_token?
|
51
|
-
# Need to check if allowed new token after loading
|
52
|
-
session = self.sessions.matches_jwt(jwt)
|
53
|
-
return false unless session
|
54
|
-
old_iat = jwt.iat
|
55
|
-
jwt.update_token(self, extra_payload)
|
56
|
-
if session.jwt_iat == old_iat
|
57
|
-
# if old_iat == previous_jwt_iat dont update and return token
|
58
|
-
session.update(previous_jwt_iat: old_iat, jwt_iat: jwt.iat)
|
59
|
-
@new_token = true
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def update_token
|
64
|
-
# This will only update the data in the token
|
65
|
-
# not the experation data or anything else
|
66
|
-
return false unless valid_in_database?
|
67
|
-
return false unless allowed_new_token?
|
68
|
-
|
69
|
-
session = self.sessions.matches_jwt(jwt)
|
70
|
-
old_iat = jwt.iat
|
71
|
-
jwt.update_token_data(self, extra_payload)
|
72
|
-
# Dont worry if session isnt there because exp not updated
|
73
|
-
session&.update(previous_jwt_iat: old_iat, jwt_iat: jwt.iat)
|
74
|
-
@new_token = true
|
75
|
-
end
|
76
|
-
|
77
|
-
def new_token?
|
78
|
-
@new_token
|
79
|
-
end
|
80
|
-
|
81
|
-
def valid_in_database?
|
82
|
-
begin
|
83
|
-
jwt_identifier_was = self.jwt_identifier
|
84
|
-
self.reload
|
85
|
-
return false if jwt_identifier_was != self.jwt_identifier
|
86
|
-
rescue ActiveRecord::RecordNotFound
|
87
|
-
return false
|
88
|
-
end
|
89
|
-
true
|
90
|
-
end
|
91
|
-
|
92
|
-
module ClassMethods
|
93
|
-
def from_sloken(slots_jwt)
|
94
|
-
self.new(slots_jwt.authentication_model_values).set_token!(slots_jwt)
|
95
|
-
end
|
96
|
-
|
97
|
-
def jwt_identifier_column
|
98
|
-
Slots.configuration.logins.keys.first
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|