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
@@ -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