slots-jwt 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +214 -0
- data/Rakefile +34 -0
- data/app/controllers/slots/sessions_controller.rb +36 -0
- data/app/mailers/slots/application_mailer.rb +8 -0
- data/app/models/slots/application_record.rb +7 -0
- data/app/models/slots/session.rb +42 -0
- data/config/routes.rb +10 -0
- data/lib/generators/slots/install/USAGE +13 -0
- data/lib/generators/slots/install/install_generator.rb +16 -0
- data/lib/generators/slots/install/templates/create_slots_sessions.rb +13 -0
- data/lib/generators/slots/install/templates/slots.rb +10 -0
- data/lib/generators/slots/model/USAGE +9 -0
- data/lib/generators/slots/model/model_generator.rb +24 -0
- data/lib/generators/slots/model/templates/create_models.rb +12 -0
- data/lib/generators/slots/model/templates/model.rb +5 -0
- data/lib/generators/slots/model/templates/model_test.rb +5 -0
- data/lib/slots.rb +46 -0
- data/lib/slots/authentication_helper.rb +144 -0
- data/lib/slots/configuration.rb +82 -0
- data/lib/slots/database_authentication.rb +19 -0
- data/lib/slots/engine.rb +7 -0
- data/lib/slots/extra_classes.rb +12 -0
- data/lib/slots/generic_methods.rb +51 -0
- data/lib/slots/generic_validations.rb +51 -0
- data/lib/slots/slokens.rb +113 -0
- data/lib/slots/tests.rb +35 -0
- data/lib/slots/tokens.rb +102 -0
- data/lib/slots/version.rb +5 -0
- data/lib/tasks/slots_tasks.rake +28 -0
- metadata +134 -0
@@ -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
|
data/lib/slots.rb
ADDED
@@ -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
|
data/lib/slots/engine.rb
ADDED
@@ -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
|