slots-jwt 0.0.4

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.
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Create the authentication model
3
+
4
+ Example:
5
+ rails generate model MODEL
6
+
7
+ This will create:
8
+ app/models/model.rb
9
+ db/migrate/YYYYMMDDHHMMSS_create_models.rb
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ class ModelGenerator < Rails::Generators::NamedBase
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ def copy_model
8
+ template "model.rb", "app/models/#{name.underscore}.rb"
9
+ template "model_test.rb", "test/models/#{name.underscore}_test.rb"
10
+ template "create_models.rb", "db/migrate/#{Time.now.strftime("%Y%m%d%H%M%S")}_create_#{name.underscore.pluralize}.rb"
11
+ end
12
+
13
+ def set_config
14
+ if name.underscore != 'user'
15
+ file = 'config/initializers/slots.rb'
16
+ config = /\n.+config\.authentication_model = .+/
17
+ gsub_file(file, config, "", verbose: false)
18
+ inject_into_file(file, after: /Slots.configure do .+\n/) do
19
+ " config.authentication_model = '#{name.classify}'\n"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ class Create<%= name.classify.pluralize %> < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :users do |t|
4
+ t.string :email, index: true, unique: true
5
+
6
+ # database_authentication
7
+ t.string :password_digest
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= name.classify %> < ApplicationRecord
4
+ slots :database_authentication
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ class <%= name.classify %>Test < ActiveSupport::TestCase
5
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slots/configuration"
4
+ require "slots/database_authentication"
5
+ require "slots/engine"
6
+ require "slots/extra_classes"
7
+ require "slots/generic_methods"
8
+ require "slots/generic_validations"
9
+ require "slots/slokens"
10
+ require "slots/tests"
11
+ require "slots/tokens"
12
+ require "slots/authentication_helper"
13
+
14
+ module Slots
15
+ # Your code goes here...
16
+ module Model
17
+ def session_assocaition
18
+ {foreign_key: "#{Slots.configuration.authentication_model.to_s.underscore}_id", class_name: Slots.configuration.authentication_model.to_s}
19
+ end
20
+
21
+ def slots(*extensions)
22
+ to_include = [GenericMethods, GenericValidations, Tokens]
23
+ extensions.each do |e|
24
+ extension = e.to_sym
25
+ case extension
26
+ when :database_authentication
27
+ to_include.push(DatabaseAuthentication)
28
+ else
29
+ raise "The following slot extension was not found: #{extension}\nThe following are allows :database_authentication, :approvable, :confirmable"
30
+ end
31
+ end
32
+ define_method(:slots?) { |v| extensions.include?(v) }
33
+
34
+ include(*to_include)
35
+ has_many :sessions, session_assocaition.merge(class_name: 'Slots::Session')
36
+ end
37
+
38
+ # module Controller
39
+ # extended do
40
+ # include Slots::AuthenticationHelper
41
+ # end
42
+ # end
43
+ end
44
+ ActiveRecord::Base.extend Slots::Model
45
+ ActionController::API.include Slots::AuthenticationHelper
46
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module AuthenticationHelper
5
+ ALL = Object.new
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include ActionController::HttpAuthentication::Token::ControllerMethods
11
+ end
12
+
13
+ def jw_token
14
+ return @_jw_token if @_jw_token&.valid!
15
+ @_jw_token = Slots::Slokens.decode(authenticate_with_http_token { |t, _| t })
16
+ @_jw_token.valid!
17
+ end
18
+
19
+ def update_expired_session_tokens
20
+ return false unless Slots.configuration.session_lifetime
21
+ @_jw_token = Slots::Slokens.decode(authenticate_with_http_token { |t, _| t })
22
+ return false unless @_jw_token.expired? && @_jw_token.session.present?
23
+ new_session_token
24
+ end
25
+
26
+ def new_session_token
27
+ _current_user = Slots.configuration.authentication_model.from_sloken(@_jw_token)
28
+ return false unless _current_user&.update_session
29
+ @_current_user = _current_user
30
+ true
31
+ end
32
+
33
+ def current_user
34
+ return @_current_user if instance_variable_defined?(:@_current_user)
35
+ current_user = Slots.configuration.authentication_model.from_sloken(jw_token)
36
+ # So if jw_token initalize current_user if expired
37
+ @_current_user ||= current_user
38
+ end
39
+ def load_user
40
+ current_user&.valid_in_database? && current_user.allowed_new_token?
41
+ end
42
+
43
+ def set_token_header!
44
+ # check if current user for logout
45
+ response.set_header('authorization', "Bearer token=#{current_user.token}") if current_user&.new_token?
46
+ end
47
+
48
+ def require_valid_user
49
+ # Load user will make sure it is in the database and valid in the database
50
+ raise Slots::InvalidToken, "User doesnt exist" if @_require_load_user && !load_user
51
+ access_denied! unless current_user && (@_ignore_callbacks || token_allowed?)
52
+ end
53
+ def require_load_user
54
+ # Use varaible so that if this action is prepended it will still only be called when checking for valid user,
55
+ # i.e. so its not called before update_expired_session_tokens if set
56
+ @_require_load_user = true
57
+ end
58
+ def ignore_callbacks
59
+ @_ignore_callbacks = true
60
+ end
61
+
62
+ def access_denied!
63
+ raise Slots::AccessDenied
64
+ end
65
+
66
+ def token_allowed?
67
+ !(self.class._reject_token?(self))
68
+ end
69
+
70
+ def new_token!(session)
71
+ current_user.create_token(session)
72
+ set_token_header!
73
+ end
74
+
75
+ def update_token!
76
+ current_user.update_token
77
+ end
78
+
79
+ module ClassMethods
80
+ def update_expired_session_tokens!(**options)
81
+ prepend_before_action :update_expired_session_tokens, **options
82
+ after_action :set_token_header!, **options
83
+ end
84
+
85
+ def require_login!(load_user: false, **options)
86
+ before_action :require_load_user, **options if load_user
87
+ before_action :require_valid_user, **options
88
+ end
89
+
90
+ def require_user_load!(**options)
91
+ prepend_before_action :require_load_user, **options
92
+ end
93
+
94
+ def skip_callback!(**options)
95
+ prepend_before_action :ignore_callbacks, **options
96
+ end
97
+
98
+ def ignore_login!(**options)
99
+ skip_before_action :require_valid_user, **options
100
+ skip_before_action :require_load_user, **options, raise: false
101
+ skip_before_action :update_expired_session_tokens, **options, raise: false
102
+ skip_after_action :set_token_header!, **options, raise: false
103
+ end
104
+
105
+ def catch_invalid_login(response: {errors: {authentication: ['login or password is invalid']}}, status: :unauthorized)
106
+ rescue_from Slots::AuthenticationFailed do |exception|
107
+ render json: response, status: status
108
+ end
109
+ end
110
+
111
+ def catch_invalid_token(response: {errors: {authentication: ['invalid or missing token']}}, status: :unauthorized)
112
+ rescue_from Slots::InvalidToken do |exception|
113
+ render json: response, status: status
114
+ end
115
+ end
116
+
117
+ def catch_access_denied(response: {errors: {authorization: ["can't access"]}}, status: :forbidden)
118
+ rescue_from Slots::AccessDenied do |exception|
119
+ render json: response, status: status
120
+ end
121
+ end
122
+
123
+ def reject_token(only: ALL, except: ALL, &block)
124
+ raise 'Cant pass both only and except' unless only == ALL || except == ALL
125
+ only = Array(only) if only != ALL
126
+ except = Array(except) if except != ALL
127
+
128
+ (@_reject_token ||= []).push([only, except, block])
129
+ end
130
+ def _reject_token?(con)
131
+ (@_reject_token ||= []).any? { |o, e, b| _check_to_reject?(con, o, e, b) } || _superclass_reject_token?(con)
132
+ end
133
+ def _check_to_reject?(con, only, except, block)
134
+ return false unless only == ALL || only.any? { |o| o.to_sym == con.action_name.to_sym }
135
+ return false if except != ALL && except.any? { |e| e.to_sym == con.action_name.to_sym }
136
+ (con.instance_eval &block)
137
+ end
138
+
139
+ def _superclass_reject_token?(con)
140
+ self.superclass.respond_to?('_reject_token?') && self.superclass._reject_token?(con)
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,82 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Slots
6
+ end
7
+ end
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,51 @@
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
@@ -0,0 +1,51 @@
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