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,113 @@
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
@@ -0,0 +1,35 @@
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
@@ -0,0 +1,102 @@
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ VERSION = "0.0.4"
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :slots do
4
+ desc "Creates or prepends secret in config/lots_secrets.yml"
5
+ task :new_secret do
6
+ FileUtils.touch(Slots.secret_yaml_file)
7
+ secret_keys = YAML.load_file(Slots.secret_yaml_file) || []
8
+ # DO 1 minute from now so has time to restart server
9
+ secret_keys.prepend('SECRET' => SecureRandom.hex(64), 'CREATED_AT' => 1.minute.from_now.to_i)
10
+
11
+ # TODO this might not always work
12
+ slots_initilizer = Rails.root.join('config', 'initializers', 'slots.rb')
13
+ require slots_initilizer if File.file?(slots_initilizer)
14
+
15
+ remove_old_secrets = Slots.configuration.session_lifetime.ago.to_i
16
+ secret_keys.reject! { |value| remove_old_secrets > value['CREATED_AT'] }
17
+ File.open(Slots.secret_yaml_file, "w") do |file|
18
+ file.write secret_keys.to_yaml
19
+ end
20
+ Rake::Task["restart"].invoke
21
+ end
22
+ desc "Clears config/lots_secrets.yml"
23
+ task :clear_secrets do
24
+ File.open(Slots.secret_yaml_file, "w") do |file|
25
+ file.write [].to_yaml
26
+ end
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slots-jwt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Jonathon Gardner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bcrypt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.1.7
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.1.7
41
+ - !ruby/object:Gem::Dependency
42
+ name: jwt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.1.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Token Authentication for Rails using JWT. Slots is designed to keep JWT
70
+ stateless and minimize database calls. This is done by storing (none sensitive)
71
+ data in the JWT and populating current_user with the JWT data. This allows for things
72
+ like `current_user.teams` or other assocations to be called on the user. Unless
73
+ explicitly told slots will only load the user from the database when creating (or
74
+ updating an expired) token.
75
+ email:
76
+ - TheAppGardner@gmail.com
77
+ executables: []
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - MIT-LICENSE
82
+ - README.md
83
+ - Rakefile
84
+ - app/controllers/slots/sessions_controller.rb
85
+ - app/mailers/slots/application_mailer.rb
86
+ - app/models/slots/application_record.rb
87
+ - app/models/slots/session.rb
88
+ - config/routes.rb
89
+ - lib/generators/slots/install/USAGE
90
+ - lib/generators/slots/install/install_generator.rb
91
+ - lib/generators/slots/install/templates/create_slots_sessions.rb
92
+ - lib/generators/slots/install/templates/slots.rb
93
+ - lib/generators/slots/model/USAGE
94
+ - lib/generators/slots/model/model_generator.rb
95
+ - lib/generators/slots/model/templates/create_models.rb
96
+ - lib/generators/slots/model/templates/model.rb
97
+ - lib/generators/slots/model/templates/model_test.rb
98
+ - lib/slots.rb
99
+ - lib/slots/authentication_helper.rb
100
+ - lib/slots/configuration.rb
101
+ - lib/slots/database_authentication.rb
102
+ - lib/slots/engine.rb
103
+ - lib/slots/extra_classes.rb
104
+ - lib/slots/generic_methods.rb
105
+ - lib/slots/generic_validations.rb
106
+ - lib/slots/slokens.rb
107
+ - lib/slots/tests.rb
108
+ - lib/slots/tokens.rb
109
+ - lib/slots/version.rb
110
+ - lib/tasks/slots_tasks.rake
111
+ homepage: https://github.com/jonathongardner/slots
112
+ licenses:
113
+ - MIT
114
+ metadata: {}
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubygems_version: 3.0.1
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: Token Authentication for Rails using JWT.
134
+ test_files: []