slots-jwt 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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