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.
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
@@ -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
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Slots
4
- class Engine < ::Rails::Engine
5
- isolate_namespace Slots
6
- end
7
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Slots
4
- class AuthenticationFailed < StandardError
5
- end
6
- class InvalidToken < StandardError
7
- end
8
- class AccessDenied < StandardError
9
- end
10
- class InvalidSecret < StandardError
11
- end
12
- end
@@ -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
@@ -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
@@ -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
@@ -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