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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d6628ed470d9b4cec56322b2f5fb64ec3680eea78f133f7914390415e610492
4
- data.tar.gz: 9f6e1a6276ebbaa24db8332423d47ecb9f630b3d68fe0a0d034bcd3140d2ecd2
3
+ metadata.gz: 839d8b81d5e72a4e4034aa14fcbf8ba479f7137d35e3ac31ff3f853a780f8925
4
+ data.tar.gz: 1c0b98884bb29be89fa2900ae627c08f7a9aa17871ec161643388b3a22ed0e16
5
5
  SHA512:
6
- metadata.gz: 54d442b78eb5450335975c2b913029f236255e1adbb8a796afb261a3f67fe99c3838f69d5fa72cfe65b4eecf94d666c3ff797d81770072edad33270a5e63e698
7
- data.tar.gz: 15aafdcc9d1e8d6a20e4d7bbf4911d44aa60baa9b73b96dc03cb05f36738c65a64ed1b3e51e3e616ffe64f8617b4016a9685e034ed9b3cd293718638a0b6f5e0
6
+ metadata.gz: 5073aed2a69fae5b4b03bd531030943e8c31c0d908a69ae3843db4c91b0531c68135ff7096865edcf6c33875a4eff86738871d2e21443cd35109a4683fd9b9de
7
+ data.tar.gz: 42d073d5a20f7aab3e066825b9e058f90a9342fb19093170a7853190ac8c18193c14f6db1a0b636ddfd9abd2e8186583f5fe844e4aed470c65d216593a1d5037
data/README.md CHANGED
@@ -1,11 +1,25 @@
1
1
  # Slots
2
2
  Token authentication solution for rails 5 API. Slots use JSON Web Tokens for authentication and database session for remembering signed in users.
3
3
 
4
+ ## Table of Contents
5
+
6
+ - [Getting started](#getting-started)
7
+ - [Secrets](#secrets)
8
+ - [Usage](#usage)
9
+ - [Authorization](#authorization)
10
+ - [Sessions](#sessions)
11
+ - [Using with GraphQL](#graphql)
12
+ - [Testing](#testing)
13
+ - [Configurations](#configurations)
14
+ - [Routes](#routes)
15
+ - [Contributing](#contributing)
16
+ - [License](#license)
17
+
4
18
  ## Getting started
5
19
  Slots 0.0.4 works with Rails 5. Add this line to your application's Gemfile:
6
20
 
7
21
  ```ruby
8
- gem 'slots'
22
+ gem 'slots-jwt'
9
23
  ```
10
24
  Then run `bundle install`.
11
25
 
@@ -15,7 +29,7 @@ $ rails generate slots:install
15
29
  ```
16
30
  This will create `config/initializers/slots.rb` and add the following line to `config/routes.rb`
17
31
  ```ruby
18
- mount Slots::Engine => "/auth"
32
+ mount Slots::JWT::Engine => "/auth"
19
33
  ```
20
34
  This will mount all slot routes to `auth/*`.
21
35
 
@@ -26,7 +40,7 @@ $ rails generate slots:model User
26
40
  Any rails accepted name can be used for the model but `User` is the expected default. If a different name is used for the authentication model than it must be defined in the config file for slots (this will automatically be done if the `generate slots` is used).
27
41
  config/initializers/slots.rb
28
42
  ```ruby
29
- Slots.configure do |config|
43
+ Slots::JWT.configure do |config|
30
44
  ...
31
45
  config.authentication_model = 'AnotherModel'
32
46
  ...
@@ -49,6 +63,9 @@ Tokens are expected to be in the header of the request in the following format:
49
63
  ```
50
64
  They are also returned in the header in the same way.
51
65
 
66
+ ## Secret
67
+ To sign JSON web tokens, a (secret) key is needed. By default Slots will look in the `ENV['SLOT_SECRET']`. This can be changed in the slots config file.
68
+
52
69
  ## Usage
53
70
  To require a user to be authenticated the following methods can be used in the controller.
54
71
  ```ruby
@@ -132,9 +149,105 @@ If sessions are allowed (`session_lifetime` is not nil) `session: true` can be p
132
149
  1. The first is by sending the token to `MOUNT_LOCATION/update_session_token`. This method will always return a new token even if the token has not expired. This will return the same information as `sign_in` (user information and with the token in the header).
133
150
  2. The second is by adding `update_expired_session_tokens!` (which takes the usual options of a `before_action` `only`, `except`, etc). This method will allow any route to take a valid expired token and it will return a new token in the headers with usual route information in the body. A token will only be returned in the header if the token passed is expired. When using this method a problem can arise were two request are made at the same time with the same expired token. The first request processed would return a new token but the second request would fail because the expired token does not match the information of the session anymore (since it was just updated) and would therefore return unauthorized. To fix this there is a previous jwt lifetime (which defaults to 5 seconds and can be changed in the config). This will allow the previous token to be valid for 5 seconds (or whatever is set in config). If a previous token is sent that is within the previous lifetime it will be a valid token but it will not return a new token (since one was already returned in the earlier request).
134
151
 
152
+ ## GraphQL
153
+ Using graphql-ruby??? Slots has helper modules and classes! It uses the following two feature of graphql-ruby to help with authorization/authentication, [extension](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/type_definitions/extensions.md) and [limiting visibility](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/schema/limiting_visibility.md). An example can be seen in [graph_test](https://github.com/jonathongardner/graph_test).
154
+ ### Authorizing fields
155
+ ```ruby
156
+ class Types
157
+ AuthorizedField < GraphQL::Schema::Field
158
+ include Slots::JWT::TypeHelper
159
+ end
160
+ end
161
+ ```
162
+
163
+ ```ruby
164
+ module Types
165
+ class BaseObject < GraphQL::Schema::Object
166
+ field_class AuthorizedField
167
+ end
168
+ end
169
+ ```
170
+
171
+ ```ruby
172
+ module Types
173
+ class QueryType < Types::BaseObject
174
+ ...
175
+
176
+ field :authorized_field, [Types::AuthorizedFieldType], null: false, description: "An authorized field", required_permission: :admin
177
+ def authorized_field
178
+ ...
179
+ end
180
+ end
181
+ end
182
+ ```
183
+
184
+ ### Authorizing Types
185
+
186
+ ```ruby
187
+ module Types
188
+ class BaseObject < GraphQL::Schema::Object
189
+ # field_class AuthorizedField # can be used together
190
+ extend Slots::JWT::TypeHelper
191
+ # required_permission(:default_type)
192
+ end
193
+ end
194
+ ```
195
+
196
+ ```ruby
197
+ module Types
198
+ class AuthorizedType < Types::BaseObject
199
+ required_permission(:admin)
200
+ ...
201
+
202
+ field :field, String, null: false, description: "A string on an authorized field"
203
+ def field
204
+ ...
205
+ end
206
+ end
207
+ end
208
+ ```
209
+
210
+ ### Filter
211
+ Filter must be used with at least one of the above.
212
+ ```ruby
213
+ class PermissionFilter < Slots::PermissionFilter
214
+ def allowed?
215
+ # available methods schema_member, current_user, required_permission, valid_loaded_user
216
+ return true if required_permission == :anyone
217
+ # loaded user gets it from the DB to help ensure user info is current
218
+ return valid_loaded_user if required_permission == :loaded_user
219
+
220
+ return is_admin if required_permission == :admin
221
+ # default to a valid user
222
+ current_user.present?
223
+ end
224
+
225
+ def is_admin
226
+ valid_loaded_user && current_user.admin
227
+ end
228
+ end
229
+ ```
230
+
231
+ ```ruby
232
+ class GraphqlController < ApplicationController
233
+ def execute
234
+ ...
235
+ context = {
236
+ current_user: current_user,# can be nil if current_user bad token or no token
237
+ }
238
+ filter = PermissionFilter.new(current_user)
239
+ result = GraphTestSchema.execute(query, only: filter, variables: variables, context: context, operation_name: operation_name)
240
+ ...
241
+ end
242
+ ...
243
+ end
244
+
245
+ ```
246
+
247
+
135
248
  ## Testing
136
249
 
137
- By adding `include Slots::Tests` the following methods can be used within minitest, `authorized_get`, `authorized_post`, `authorized_put`, `authorized_patch` and `authorized_delete`. These methods are the same as the usual `get`, ... `delete` but the first param in the method must be the user. For example:
250
+ By adding `include Slots::JWT::Tests` the following methods can be used within minitest, `authorized_get`, `authorized_post`, `authorized_put`, `authorized_patch` and `authorized_delete`. These methods are the same as the usual `get`, ... `delete` but the first param in the method must be the user. For example:
138
251
  ```ruby
139
252
  authorized_get users(:some_user), some_route_url, params: {one: 'something', ...}, headers: {'info' => 'someInfo', ...}
140
253
  ```
@@ -142,7 +255,7 @@ authorized_get users(:some_user), some_route_url, params: {one: 'something', ...
142
255
  ## Configurations
143
256
  Default configuration:
144
257
  ```ruby
145
- Slots.configure do |config|
258
+ Slots::JWT.configure do |config|
146
259
  config.logins = :email
147
260
  config.login_regex_validations = true
148
261
  config.authentication_model = 'User'
@@ -180,7 +293,7 @@ The order should be newer to older secrets. This file can be created/updated man
180
293
 
181
294
  ## Routes
182
295
 
183
- All these routes will be mounted at the route used above in `mount Slots::Engine =>`.
296
+ All these routes will be mounted at the route used above in `mount Slots::JWT::Engine =>`.
184
297
 
185
298
  | Route Helper | Route | Token | |
186
299
  | ------------ | ----- | ----- | --- |
@@ -206,6 +319,8 @@ Some of the problems with JWS:
206
319
 
207
320
  The last solution I feel is the best because for most API calls (within the expiration time) the token remains stateless. The downsides can be negligible by setting the expiration time to something small (less than an hour). .
208
321
 
322
+ ### Why the name???
323
+ Last but not least the most important question of them all... why slots??? or better yet slots-jwt??? well I'll start with the first, a slot machine takes tokens... yep that's it, all other authentication names had been taken so this is it. So why slots-jwt? Well hopefully it helps clarify a little what it does but most of all rubygems wouldn't let me name it slots because it was to close to another name..?..? so I added `-jwt`.
209
324
 
210
325
  ## Contributing
211
326
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ class SessionsController < ApplicationController
6
+ update_expired_session_tokens! only: :update_session_token # needed if token is expired
7
+ require_user_load! only: :update_session_token
8
+ require_login! only: [:update_session_token, :sign_out]
9
+ skip_callback!
10
+
11
+ def sign_in
12
+ @_current_user = _authentication_model.find_for_authentication(params[:login])
13
+
14
+ current_user.authenticate!(params[:password])
15
+
16
+ new_token!(ActiveModel::Type::Boolean.new.cast(params[:session]))
17
+ render json: current_user.as_json, status: :accepted
18
+ end
19
+
20
+ def sign_out
21
+ Slots::JWT::Session.find_by(session: jw_token.session)&.delete if jw_token.session.present?
22
+ head :ok
23
+ end
24
+
25
+ def update_session_token
26
+ # TODO think about not allowing user to get new token here because then there
27
+ current_user.update_token unless current_user.new_token?
28
+ render json: current_user.as_json, status: :accepted
29
+ end
30
+
31
+ private
32
+
33
+ def _authentication_model
34
+ Slots::JWT.configuration.authentication_model
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ class ApplicationRecord < ActiveRecord::Base
6
+ self.abstract_class = true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ module JWT
5
+ class Session < ApplicationRecord
6
+ belongs_to :user, session_assocaition
7
+ before_validation :create_random_session, on: :create
8
+ validates :session, :jwt_iat, presence: true
9
+ validates :session, uniqueness: true
10
+
11
+ def update_random_session
12
+ self.session = SecureRandom.hex(32)
13
+ end
14
+
15
+ def self.expired
16
+ self.where(self.arel_table[:created_at].lte(Slots::JWT.configuration.session_lifetime.ago))
17
+ end
18
+
19
+ def self.not_expired
20
+ self.where(self.arel_table[:created_at].gt(Slots::JWT.configuration.session_lifetime.ago))
21
+ end
22
+
23
+ def self.matches_jwt(sloken_jws)
24
+ jwt_where = self.arel_table[:jwt_iat].eq(sloken_jws.iat)
25
+ if Slots::JWT.configuration.previous_jwt_lifetime
26
+ jwt_where = jwt_where.or(
27
+ Arel::Nodes::Grouping.new(
28
+ self.arel_table[:previous_jwt_iat].eq(sloken_jws.iat)
29
+ .and(self.arel_table[:jwt_iat].gt(Slots::JWT.configuration.previous_jwt_lifetime.ago.to_i))
30
+ )
31
+ )
32
+ end
33
+
34
+ self.not_expired
35
+ .where(jwt_where)
36
+ .find_by(session: sloken_jws.session)
37
+ end
38
+ private
39
+ def create_random_session
40
+ update_random_session unless self.session
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
2
+ inflect.acronym 'JWT'
3
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- user_model = Slots.configuration.authentication_model&.name&.underscore || ""
4
- Slots::Engine.routes.draw do
3
+ user_model = Slots::JWT.configuration.authentication_model&.name&.underscore || ""
4
+ Slots::JWT::Engine.routes.draw do
5
5
  get 'sign_in', to: 'sessions#sign_in'
6
6
  post 'sign_in', to: 'sessions#sign_in'
7
7
  delete 'sign_out', to: 'sessions#sign_out'
@@ -10,4 +10,4 @@ Example:
10
10
 
11
11
  This will add:
12
12
  TO: config/routes.rb
13
- mount Slots::Engine => "/slots"
13
+ mount Slots::JWT::Engine => "/slots"
@@ -10,7 +10,7 @@ module Slots
10
10
  end
11
11
 
12
12
  def add_route
13
- route "mount Slots::Engine => '/auth'"
13
+ route "mount Slots::JWT::Engine => '/auth'"
14
14
  end
15
15
  end
16
16
  end
@@ -1,6 +1,6 @@
1
1
  class CreateSlotsSessions < ActiveRecord::Migration[5.2]
2
2
  def change
3
- create_table :slots_sessions do |t|
3
+ create_table :slots_jwt_sessions do |t|
4
4
  t.string :session, length: 128
5
5
  t.bigint :jwt_iat
6
6
  t.bigint :previous_jwt_iat
@@ -8,6 +8,6 @@ class CreateSlotsSessions < ActiveRecord::Migration[5.2]
8
8
 
9
9
  t.timestamps
10
10
  end
11
- add_index :slots_sessions, :session, unique: true
11
+ add_index :slots_jwt_sessions, :session, unique: true
12
12
  end
13
13
  end
@@ -1,4 +1,4 @@
1
- Slots.configure do |config|
1
+ Slots::JWT.configure do |config|
2
2
  # config.logins = :email || {email: /@/, username: //}
3
3
  # config.login_regex_validations = true
4
4
  # config.authentication_model = 'User'
@@ -15,7 +15,7 @@ module Slots
15
15
  file = 'config/initializers/slots.rb'
16
16
  config = /\n.+config\.authentication_model = .+/
17
17
  gsub_file(file, config, "", verbose: false)
18
- inject_into_file(file, after: /Slots.configure do .+\n/) do
18
+ inject_into_file(file, after: /Slots::JWT.configure do .+\n/) do
19
19
  " config.authentication_model = '#{name.classify}'\n"
20
20
  end
21
21
  end
@@ -1,46 +1,3 @@
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
-
1
+ require 'slots/jwt'
14
2
  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
3
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slots/jwt/configuration"
4
+ require "slots/jwt/database_authentication"
5
+ require "slots/jwt/engine"
6
+ require "slots/jwt/extra_classes"
7
+ require "slots/jwt/generic_methods"
8
+ require "slots/jwt/generic_validations"
9
+ require "slots/jwt/slokens"
10
+ require "slots/jwt/tests"
11
+ require "slots/jwt/tokens"
12
+ require "slots/jwt/authentication_helper"
13
+ require "slots/jwt/permission_filter"
14
+ require "slots/jwt/type_helper"
15
+
16
+ module Slots
17
+ module JWT
18
+ module Model
19
+ def session_assocaition
20
+ {foreign_key: "#{Slots::JWT.configuration.authentication_model.to_s.underscore}_id", class_name: Slots::JWT.configuration.authentication_model.to_s}
21
+ end
22
+
23
+ def slots(*extensions)
24
+ to_include = [GenericMethods, GenericValidations, Tokens]
25
+ extensions.each do |e|
26
+ extension = e.to_sym
27
+ case extension
28
+ when :database_authentication
29
+ to_include.push(DatabaseAuthentication)
30
+ else
31
+ raise "The following slot extension was not found: #{extension}\nThe following are allows :database_authentication, :approvable, :confirmable"
32
+ end
33
+ end
34
+ define_method(:slots?) { |v| extensions.include?(v) }
35
+
36
+ include(*to_include)
37
+ has_many :sessions, session_assocaition.merge(class_name: 'Slots::JWT::Session')
38
+ end
39
+
40
+ # module Controller
41
+ # extended do
42
+ # include Slots::AuthenticationHelper
43
+ # end
44
+ # end
45
+ end
46
+ ActiveRecord::Base.extend Slots::JWT::Model
47
+ ActionController::API.include Slots::JWT::AuthenticationHelper
48
+ end
49
+ end