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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +121 -6
  3. data/app/controllers/slots/jwt/sessions_controller.rb +38 -0
  4. data/app/models/slots/jwt/application_record.rb +9 -0
  5. data/app/models/slots/jwt/session.rb +44 -0
  6. data/config/initializers/inflections.rb +3 -0
  7. data/config/routes.rb +2 -2
  8. data/lib/generators/slots/install/USAGE +1 -1
  9. data/lib/generators/slots/install/install_generator.rb +1 -1
  10. data/lib/generators/slots/install/templates/create_slots_sessions.rb +2 -2
  11. data/lib/generators/slots/install/templates/slots.rb +1 -1
  12. data/lib/generators/slots/model/model_generator.rb +1 -1
  13. data/lib/slots.rb +1 -44
  14. data/lib/slots/jwt.rb +49 -0
  15. data/lib/slots/jwt/authentication_helper.rb +147 -0
  16. data/lib/slots/jwt/configuration.rb +84 -0
  17. data/lib/slots/jwt/database_authentication.rb +21 -0
  18. data/lib/slots/jwt/engine.rb +9 -0
  19. data/lib/slots/jwt/extra_classes.rb +14 -0
  20. data/lib/slots/jwt/generic_methods.rb +53 -0
  21. data/lib/slots/jwt/generic_validations.rb +53 -0
  22. data/lib/slots/jwt/permission_filter.rb +37 -0
  23. data/lib/slots/jwt/slokens.rb +115 -0
  24. data/lib/slots/jwt/tests.rb +37 -0
  25. data/lib/slots/jwt/tokens.rb +104 -0
  26. data/lib/slots/jwt/type_helper.rb +30 -0
  27. data/lib/slots/{version.rb → jwt/version.rb} +3 -1
  28. data/lib/tasks/slots_tasks.rake +5 -5
  29. metadata +23 -19
  30. data/app/controllers/slots/sessions_controller.rb +0 -36
  31. data/app/mailers/slots/application_mailer.rb +0 -8
  32. data/app/models/slots/application_record.rb +0 -7
  33. data/app/models/slots/session.rb +0 -42
  34. data/lib/slots/authentication_helper.rb +0 -144
  35. data/lib/slots/configuration.rb +0 -82
  36. data/lib/slots/database_authentication.rb +0 -19
  37. data/lib/slots/engine.rb +0 -7
  38. data/lib/slots/extra_classes.rb +0 -12
  39. data/lib/slots/generic_methods.rb +0 -51
  40. data/lib/slots/generic_validations.rb +0 -51
  41. data/lib/slots/slokens.rb +0 -113
  42. data/lib/slots/tests.rb +0 -35
  43. data/lib/slots/tokens.rb +0 -102
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ module AuthenticationHelper
6
+ ALL = Object.new
7
+
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include ActionController::HttpAuthentication::Token::ControllerMethods
12
+ end
13
+
14
+ def jw_token
15
+ return @_jw_token if instance_variable_defined?(:@_jw_token)
16
+ token = authenticate_with_http_token { |t, _| t }
17
+ @_jw_token = token ? Slots::JWT::Slokens.decode(token) : nil
18
+ end
19
+
20
+ def jw_token!
21
+ jw_token&.valid!
22
+ end
23
+
24
+ def update_expired_session_tokens
25
+ return false unless Slots::JWT.configuration.session_lifetime
26
+ return false unless jw_token&.expired? && jw_token.session.present?
27
+ new_session_token
28
+ end
29
+
30
+ def new_session_token
31
+ _current_user = Slots::JWT.configuration.authentication_model.from_sloken(@_jw_token)
32
+ return false unless _current_user&.update_session
33
+ @_current_user = _current_user
34
+ true
35
+ end
36
+
37
+ def current_user
38
+ return @_current_user if instance_variable_defined?(:@_current_user)
39
+ @_current_user = jw_token ? Slots::JWT.configuration.authentication_model.from_sloken(jw_token!) : nil
40
+ end
41
+ def load_user
42
+ current_user&.valid_in_database? && current_user.allowed_new_token?
43
+ end
44
+
45
+ def set_token_header!
46
+ # check if current user for logout
47
+ response.set_header('authorization', "Bearer token=#{current_user.token}") if current_user&.new_token?
48
+ end
49
+
50
+ def require_valid_user
51
+ # Load user will make sure it is in the database and valid in the database
52
+ raise Slots::JWT::InvalidToken, "User doesnt exist" if @_require_load_user && !load_user
53
+ access_denied! unless current_user && (@_ignore_callbacks || token_allowed?)
54
+ end
55
+ def require_load_user
56
+ # Use varaible so that if this action is prepended it will still only be called when checking for valid user,
57
+ # i.e. so its not called before update_expired_session_tokens if set
58
+ @_require_load_user = true
59
+ end
60
+ def ignore_callbacks
61
+ @_ignore_callbacks = true
62
+ end
63
+
64
+ def access_denied!
65
+ raise Slots::JWT::AccessDenied
66
+ end
67
+
68
+ def token_allowed?
69
+ !(self.class._reject_token?(self))
70
+ end
71
+
72
+ def new_token!(session)
73
+ current_user.create_token(session)
74
+ set_token_header!
75
+ end
76
+
77
+ def update_token!
78
+ current_user.update_token
79
+ end
80
+
81
+ module ClassMethods
82
+ def update_expired_session_tokens!(**options)
83
+ prepend_before_action :update_expired_session_tokens, **options
84
+ after_action :set_token_header!, **options
85
+ end
86
+
87
+ def require_login!(load_user: false, **options)
88
+ before_action :require_load_user, **options if load_user
89
+ before_action :require_valid_user, **options
90
+ end
91
+
92
+ def require_user_load!(**options)
93
+ prepend_before_action :require_load_user, **options
94
+ end
95
+
96
+ def skip_callback!(**options)
97
+ prepend_before_action :ignore_callbacks, **options
98
+ end
99
+
100
+ def ignore_login!(**options)
101
+ skip_before_action :require_valid_user, **options
102
+ skip_before_action :require_load_user, **options, raise: false
103
+ skip_before_action :update_expired_session_tokens, **options, raise: false
104
+ skip_after_action :set_token_header!, **options, raise: false
105
+ end
106
+
107
+ def catch_invalid_login(response: {errors: {authentication: ['login or password is invalid']}}, status: :unauthorized)
108
+ rescue_from Slots::JWT::AuthenticationFailed do |exception|
109
+ render json: response, status: status
110
+ end
111
+ end
112
+
113
+ def catch_invalid_token(response: {errors: {authentication: ['invalid or missing token']}}, status: :unauthorized)
114
+ rescue_from Slots::JWT::InvalidToken do |exception|
115
+ render json: response, status: status
116
+ end
117
+ end
118
+
119
+ def catch_access_denied(response: {errors: {authorization: ["can't access"]}}, status: :forbidden)
120
+ rescue_from Slots::JWT::AccessDenied do |exception|
121
+ render json: response, status: status
122
+ end
123
+ end
124
+
125
+ def reject_token(only: ALL, except: ALL, &block)
126
+ raise 'Cant pass both only and except' unless only == ALL || except == ALL
127
+ only = Array(only) if only != ALL
128
+ except = Array(except) if except != ALL
129
+
130
+ (@_reject_token ||= []).push([only, except, block])
131
+ end
132
+ def _reject_token?(con)
133
+ (@_reject_token ||= []).any? { |o, e, b| _check_to_reject?(con, o, e, b) } || _superclass_reject_token?(con)
134
+ end
135
+ def _check_to_reject?(con, only, except, block)
136
+ return false unless only == ALL || only.any? { |o| o.to_sym == con.action_name.to_sym }
137
+ return false if except != ALL && except.any? { |e| e.to_sym == con.action_name.to_sym }
138
+ con.instance_eval(&block)
139
+ end
140
+
141
+ def _superclass_reject_token?(con)
142
+ self.superclass.respond_to?('_reject_token?') && self.superclass._reject_token?(con)
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Slots
6
+ module JWT
7
+ class Configuration
8
+ attr_accessor :login_regex_validations, :token_lifetime, :session_lifetime, :previous_jwt_lifetime
9
+ attr_reader :logins
10
+ attr_writer :authentication_model
11
+
12
+ # raise_no_error is used for rake to load
13
+ def initialize
14
+ @logins = {email: //}
15
+ @login_regex_validations = true
16
+ @authentication_model = 'User'
17
+ @secret_keys = [{created_at: 0, secret: ENV['SLOT_SECRET']}]
18
+ @token_lifetime = 1.hour
19
+ @session_lifetime = 2.weeks # Set to nil if you dont want sessions
20
+ @previous_jwt_lifetime = 5.seconds # Set to nil if you dont want sessions
21
+ @manage_callbacks = Proc.new { }
22
+ end
23
+
24
+ def logins=(value)
25
+ if value.is_a? Symbol
26
+ @logins = {value => //}
27
+ elsif value.is_a?(Hash)
28
+ # Should do most inclusive regex last
29
+ raise 'must be hash of symbols => regex' unless value.length > 0 && value.all? { |k, v| k.is_a?(Symbol) && v.is_a?(Regexp) }
30
+ @logins = value
31
+ else
32
+ raise 'must be a symbol or hash'
33
+ end
34
+ end
35
+
36
+ def authentication_model
37
+ @authentication_model.to_s.constantize rescue nil
38
+ end
39
+
40
+ def secret=(v)
41
+ @secret_keys = [{created_at: 0, secret: v}]
42
+ end
43
+
44
+ def secret_yaml=(file_path_string)
45
+ secret_keys = YAML.load_file(Slots::JWT.secret_yaml_file)
46
+ @secret_keys = []
47
+ secret_keys.each do |secret_key|
48
+ raise ArgumentError, 'Need CREATED_AT' unless (created_at = secret_key['CREATED_AT']&.to_i)
49
+ raise ArgumentError, 'Need SECRET' unless (secret = secret_key['SECRET'])
50
+ previous_created_at = @secret_keys[-1]&.dig(:created_at) || Time.now.to_i
51
+
52
+ raise ArgumentError, 'CREATED_AT must be newest to latest' unless previous_created_at > created_at
53
+ @secret_keys.push(
54
+ created_at: created_at,
55
+ secret: secret
56
+ )
57
+ end
58
+ end
59
+
60
+ def secret(at = Time.now.to_i)
61
+ @secret_keys.each do |secret_hash|
62
+ return secret_hash[:secret] if at > secret_hash[:created_at]
63
+ end
64
+ raise InvalidSecret, 'Invalid Secret'
65
+ end
66
+ end
67
+
68
+ class << self
69
+ attr_writer :configuration
70
+
71
+ def configuration
72
+ @configuration ||= Configuration.new
73
+ end
74
+
75
+ def configure
76
+ yield configuration
77
+ end
78
+
79
+ def secret_yaml_file
80
+ Rails.root.join('config', 'slots_secrets.yml')
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ module DatabaseAuthentication
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ has_secure_password
10
+ end
11
+
12
+ # TODO allow super
13
+ def as_json(*)
14
+ super.except('password_digest')
15
+ end
16
+
17
+ module ClassMethods
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Slots::JWT
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ class AuthenticationFailed < StandardError
6
+ end
7
+ class InvalidToken < StandardError
8
+ end
9
+ class AccessDenied < StandardError
10
+ end
11
+ class InvalidSecret < StandardError
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ module GenericMethods
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ end
10
+
11
+ def allowed_new_token?
12
+ !(self.class._reject_new_token?(self))
13
+ end
14
+
15
+ def run_token_created_callback
16
+ self.class._token_created_callback(self)
17
+ end
18
+
19
+ def authenticate?(password)
20
+ password.present? && persisted? && respond_to?(:authenticate) && authenticate(password) && allowed_new_token?
21
+ end
22
+
23
+ def authenticate!(password)
24
+ raise Slots::JWT::AuthenticationFailed unless self.authenticate?(password)
25
+ true
26
+ end
27
+
28
+ module ClassMethods
29
+ def find_for_authentication(login)
30
+ Slots::JWT.configuration.logins.each do |k, v|
31
+ next unless login&.match(v)
32
+ return find_by(arel_table[k].lower.eq(login.downcase)) || new
33
+ end
34
+ new
35
+ end
36
+
37
+ def reject_new_token(&block)
38
+ (@_reject_new_token ||= []).push(block)
39
+ end
40
+ def _reject_new_token?(user)
41
+ (@_reject_new_token ||= []).any? { |b| user.instance_eval(&b) }
42
+ end
43
+
44
+ def token_created_callback(&block)
45
+ (@_token_created_callback ||= []).push(block)
46
+ end
47
+ def _token_created_callback(user)
48
+ (@_token_created_callback ||= []).each { |b| user.instance_eval(&b) }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ module GenericValidations
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ validate :unique_and_present, :logins_meets_criteria
10
+ end
11
+
12
+ def logins_meets_criteria
13
+ return if self.errors.any?
14
+ return unless Slots::JWT.configuration.login_regex_validations
15
+ logins = Slots::JWT.configuration.logins
16
+ login_c = logins.keys
17
+ logins.each do |col, reg|
18
+ login_c.delete(col) # Login columns left
19
+ column_match(reg, col)
20
+ column_dont_match(reg, col, login_c)
21
+ end
22
+ end
23
+
24
+ def unique_and_present
25
+ # Use this rather than validates because logins in configure might not be set yet on include
26
+ Slots::JWT.configuration.logins.each do |column, _|
27
+ value = self.send(column)
28
+ next self.errors.add(column, "can't be blank") unless value.present?
29
+
30
+ pk_value = self.send(self.class.primary_key)
31
+ lower_case = self.class.arel_table[column].lower.eq(value.downcase)
32
+ next unless self.class.where.not(self.class.primary_key => pk_value).where(lower_case).exists?
33
+ self.errors.add(column, "has already been taken")
34
+ end
35
+ end
36
+
37
+ def column_match(regex, column)
38
+ # TODO change error message to use locals? or something configurable
39
+ self.errors.add(column, "didn't match login criteria") unless self.send(column).match(regex)
40
+ end
41
+
42
+ def column_dont_match(regex, column_not_to_match, columns)
43
+ columns.each do |c|
44
+ # Since we check if any errors should be present
45
+ self.errors.add(c, "matched #{column_not_to_match} login criteria") if self.send(c).match(regex)
46
+ end
47
+ end
48
+
49
+ module ClassMethods
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ class PermissionFilter
6
+ def initialize(current_user)
7
+ @current_user = current_user
8
+ end
9
+
10
+ def call(schema_member, ctx)
11
+ @schema_member = schema_member
12
+ return true if _dont_check?
13
+ allowed?
14
+ end
15
+ protected
16
+ attr_reader :schema_member, :current_user
17
+
18
+ private
19
+ def _dont_check?
20
+ !schema_member.metadata[:has_required_permission]
21
+ end
22
+
23
+ def required_permission
24
+ schema_member.metadata[:required_permission]
25
+ end
26
+
27
+ def valid_loaded_user
28
+ return @valid_loaded_user if instance_variable_defined?(:@valid_loaded_user)
29
+ @valid_loaded_user = current_user&.valid_in_database? && current_user.allowed_new_token?
30
+ end
31
+
32
+ def is_admin
33
+ valid_loaded_user && current_user.admin
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+ module Slots
5
+ module JWT
6
+ class Slokens
7
+ attr_reader :token, :exp, :iat, :extra_payload, :authentication_model_values, :session
8
+ def initialize(decode: false, encode: false, token: nil, authentication_record: nil, extra_payload: nil, session: nil)
9
+ if decode
10
+ @token = token
11
+ decode()
12
+ elsif encode
13
+ @authentication_model_values = authentication_record.as_json
14
+ @extra_payload = extra_payload.as_json
15
+ @session = session
16
+ update_iat
17
+ update_exp
18
+ encode()
19
+ @valid = true
20
+ else
21
+ raise 'must encode or decode'
22
+ end
23
+ end
24
+ def self.decode(token)
25
+ self.new(decode: true, token: token)
26
+ end
27
+ def self.encode(authentication_record, session = '', extra_payload)
28
+ self.new(encode: true, authentication_record: authentication_record, session: session, extra_payload: extra_payload)
29
+ end
30
+
31
+ def expired?
32
+ @expired
33
+ end
34
+
35
+ def valid?
36
+ @valid
37
+ end
38
+
39
+ def valid!
40
+ raise InvalidToken, "Invalid Token" unless valid?
41
+ self
42
+ end
43
+
44
+ def update_token_data(authentication_record, extra_payload)
45
+ @authentication_model_values = authentication_record.as_json
46
+ @extra_payload = extra_payload.as_json
47
+ update_iat
48
+ encode
49
+ end
50
+
51
+ def update_token(authentication_record, extra_payload)
52
+ update_exp
53
+ update_token_data(authentication_record, extra_payload)
54
+ end
55
+
56
+ def payload
57
+ {
58
+ authentication_model_key => @authentication_model_values,
59
+ 'exp' => @exp,
60
+ 'iat' => @iat,
61
+ 'session' => @session,
62
+ 'extra_payload' => @extra_payload,
63
+ }
64
+ end
65
+
66
+ private
67
+ def authentication_model_key
68
+ Slots::JWT.configuration.authentication_model.name.underscore
69
+ end
70
+
71
+ def default_expected_keys
72
+ ['exp', 'iat', 'session', authentication_model_key]
73
+ end
74
+ def secret
75
+ Slots::JWT.configuration.secret(@iat)
76
+ end
77
+ def update_iat
78
+ @iat = Time.now.to_i
79
+ end
80
+ def update_exp
81
+ @exp = Slots::JWT.configuration.token_lifetime.from_now.to_i
82
+ end
83
+ def encode
84
+ @token = ::JWT.encode self.payload, secret, 'HS256'
85
+ @expired = false
86
+ @valid = true
87
+ end
88
+
89
+ def decode
90
+ begin
91
+ set_payload
92
+ ::JWT.decode @token, secret, true, verify_iat: true, algorithm: 'HS256'
93
+ rescue ::JWT::ExpiredSignature
94
+ @expired = true
95
+ rescue ::JWT::InvalidIatError, ::JWT::VerificationError, ::JWT::DecodeError, NoMethodError, JSON::ParserError, Slots::JWT::InvalidSecret
96
+ @valid = false
97
+ else
98
+ @valid = payload.slice(*default_expected_keys).compact.length == default_expected_keys.length
99
+ end
100
+ end
101
+
102
+ def set_payload
103
+ encoded64 = @token.split('.')[1] || ''
104
+ string_payload = Base64.decode64(encoded64)
105
+ local_payload = JSON.parse(string_payload)
106
+ raise JSON::ParserError unless local_payload.is_a?(Hash)
107
+ @exp = local_payload['exp']&.to_i
108
+ @iat = local_payload['iat']&.to_i
109
+ @session = local_payload['session']
110
+ @authentication_model_values = local_payload[authentication_model_key]
111
+ @extra_payload = local_payload['extra_payload']
112
+ end
113
+ end
114
+ end
115
+ end