slots-jwt 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|