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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3d6628ed470d9b4cec56322b2f5fb64ec3680eea78f133f7914390415e610492
4
+ data.tar.gz: 9f6e1a6276ebbaa24db8332423d47ecb9f630b3d68fe0a0d034bcd3140d2ecd2
5
+ SHA512:
6
+ metadata.gz: 54d442b78eb5450335975c2b913029f236255e1adbb8a796afb261a3f67fe99c3838f69d5fa72cfe65b4eecf94d666c3ff797d81770072edad33270a5e63e698
7
+ data.tar.gz: 15aafdcc9d1e8d6a20e4d7bbf4911d44aa60baa9b73b96dc03cb05f36738c65a64ed1b3e51e3e616ffe64f8617b4016a9685e034ed9b3cd293718638a0b6f5e0
@@ -0,0 +1,20 @@
1
+ Copyright 2018 Jonathon Gardner
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,214 @@
1
+ # Slots
2
+ Token authentication solution for rails 5 API. Slots use JSON Web Tokens for authentication and database session for remembering signed in users.
3
+
4
+ ## Getting started
5
+ Slots 0.0.4 works with Rails 5. Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'slots'
9
+ ```
10
+ Then run `bundle install`.
11
+
12
+ Next create the slots config file and add the routes using:
13
+ ```console
14
+ $ rails generate slots:install
15
+ ```
16
+ This will create `config/initializers/slots.rb` and add the following line to `config/routes.rb`
17
+ ```ruby
18
+ mount Slots::Engine => "/auth"
19
+ ```
20
+ This will mount all slot routes to `auth/*`.
21
+
22
+ Next, the following command can be used to generate the authentication model.
23
+ ```console
24
+ $ rails generate slots:model User
25
+ ```
26
+ 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
+ config/initializers/slots.rb
28
+ ```ruby
29
+ Slots.configure do |config|
30
+ ...
31
+ config.authentication_model = 'AnotherModel'
32
+ ...
33
+ end
34
+ ```
35
+ If you are using a model that has already been created than just add the following with the desired plugins:
36
+ ```ruby
37
+ class MyModel < ApplicationRecord
38
+ ...
39
+ slots :database_authentication
40
+ end
41
+ ...
42
+ end
43
+ ```
44
+ And make sure the table has the necessary columns (if using the default setup, that would be email and password_digest). If other methods are desired for authentication like LDAP do not pass `:database_authentication` to slots and add a method `authenticate(password)` in the model. Database Authentication stores a password in the database using Secure Password.
45
+
46
+ Tokens are expected to be in the header of the request in the following format:
47
+ ```ruby
48
+ 'authorization' => 'Bearer token=TOKEN'
49
+ ```
50
+ They are also returned in the header in the same way.
51
+
52
+ ## Usage
53
+ To require a user to be authenticated the following methods can be used in the controller.
54
+ ```ruby
55
+ require_login!
56
+ ```
57
+
58
+ `require_login!` takes the usual options of a `before_action` (`only`, `except`) and also `load_user`.
59
+ The `current_user` is populated with the information from JWT. This can be a problem because the info in the JWT could become out of date; it would not update until the token has expired. If you want to force the user to be reloaded from the database you can call `require_user_load!` or pass `load_user: true` to `require_login!`. Default is not to load the user to help keep the JWT stateless.
60
+
61
+ NOTE: Before changes can be made to `current_user` user must be reloaded. This can be done using the above method or by `current_user.valid_in_database?`.
62
+
63
+ WARNING: do not call `require_login!` twice in one controller. For example if one route, you want with load_user and one without don't do the following, because only the last one will be done.
64
+ ```ruby
65
+ require_login! only: [:action1]
66
+ require_login! load_user: true, only: [:action2]
67
+ ```
68
+ This is a limitation on rails `before_action`. In the example above only `action2` will require a login. Instead use the following:
69
+ ```ruby
70
+ require_login! only: [:action1, :action2]
71
+ require_user_load! only: [:action2]
72
+ ```
73
+
74
+ These method will raise a `Slots::InvalidToken` Error. This error can be caught using the helper method `catch_invalid_token`. If nothing is passed the following will be returned with a unauthorized status:
75
+ ```
76
+ 'errors' => {
77
+ 'authentication' => ['invalid or missing token']
78
+ }
79
+ ```
80
+ A custom message or status can be returned using the following:
81
+ ```ruby
82
+ catch_invalid_token(response: {my_message: 'Some custom message'}, status: :im_a_teapot)
83
+ ```
84
+ It is sometimes easier to always require login and explicitly ignore it when needed. To do this add `require_login!` and `catch_invalid_token` to the `ApplicationController`. Then on routes that you do not want to require authentication use the following method.
85
+ ```ruby
86
+ ignore_login!
87
+ ```
88
+ This takes all the same options as `require_login!`.
89
+
90
+ To not allow a user to sign in the following can be used in the authentication model:
91
+ ```ruby
92
+ class User < ApplicationRecord
93
+ slots :database_authentication
94
+
95
+ reject_new_token do
96
+ !self.approved # Return true if they cannot get a new token
97
+ end
98
+ end
99
+ ```
100
+ This will not allow unapproved users to get a new token (login or update_session_token).
101
+
102
+ ## Authorization
103
+ Sometimes when dealing with authentication you also need authorization. While in most cases you should use another gem to handle this, if it is simple (like an admin or approved user) slots can handle it. Just add the following:
104
+ ```ruby
105
+ class SomeController < ApplicationController
106
+ ...
107
+
108
+ reject_token do
109
+ !current_user.admin # Return true to not allow to see resource
110
+ end
111
+
112
+ def some_special_action_that_you_must_be_admin_for
113
+ end
114
+
115
+ ...
116
+ end
117
+ ```
118
+ `reject_token` take the same params as rails `before_action`. This will raise a `Slots::AccessDenied` Error for users not approved for the routes in this controller. To catch this error you can use the helper method `catch_access_denied`. If nothing is passed the following will be returned with a forbidden status:
119
+ ```
120
+ 'errors' => {
121
+ 'authorization' => ["can't access"]
122
+ }
123
+ ```
124
+ A custom message or status can be returned using the following:
125
+ ```ruby
126
+ catch_invalid_token(response: {my_message: 'Some custom message'}, status: :im_a_teapot)
127
+ ```
128
+ NOTE: If you want the token to be rejected for all tokens (i.e. require all routes to have an approved user) add the above to the `ApplicationController`. You can then also add more specific requirements to a controller by also adding it in the controller like requiring an admin. To ignore a `reject_token` use `skip_callback!` which again takes the same params as `before_action`.
129
+
130
+ ## Sessions
131
+ If sessions are allowed (`session_lifetime` is not nil) `session: true` can be passed along when signing in to receive a session token. A session tokens has a the session id in the payload of the JWT. This is kept in the JWT so the front-end only has to track one token. There are two ways to get a new token after a session token has expired.
132
+ 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
+ 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
+
135
+ ## Testing
136
+
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:
138
+ ```ruby
139
+ authorized_get users(:some_user), some_route_url, params: {one: 'something', ...}, headers: {'info' => 'someInfo', ...}
140
+ ```
141
+
142
+ ## Configurations
143
+ Default configuration:
144
+ ```ruby
145
+ Slots.configure do |config|
146
+ config.logins = :email
147
+ config.login_regex_validations = true
148
+ config.authentication_model = 'User'
149
+ config.secret = ENV['SLOT_SECRET']
150
+ config.token_lifetime = 1.hour
151
+ config.session_lifetime = 2.weeks
152
+ config.previous_jwt_lifetime = 5.seconds
153
+ config.secret_yaml = false
154
+ end
155
+ ```
156
+ - `logins`: this is the column to use for logins. It must be a symbol or a hash with symbol regex pair where the symbol is the column and the regex is when to use it (hash order matters). An example might is
157
+ ```ruby
158
+ config.logins = {email: /@/, username: //}
159
+ ```
160
+ This would make it if a value for login is passed and it has an @ symbol than check the email column otherwise check the username column.
161
+ - `login_regex_validations`: This will require the column for login to match the regex passed and no others before it. So for the example above it would not allow username to contain '@'.
162
+ - `authentication_model`: The model used for authentication.
163
+ - `secret`: This is the secret used to encode the JWS.
164
+ - `token_lifetime`: This is the lifetime of the token, it should be kept short (less than one hour).
165
+ - `session_lifetime`: This is the session lifetime, set to nil if you do not want to use sessions.
166
+ - `previous_jwt_lifetime`: This is the lifetime of the previous_jwt, for example if two request are sent with an expired token the first one will update the session making the second one invalid (because the iat doesn't match the session). Therefore this is to gives time for all following request to use the new token.
167
+ - `secret_yaml`: Set to true to load secret from `config/slots_secrets.yml`. [More](Secret Yaml)
168
+
169
+ ### Secret Yaml
170
+ `config/slots_secrets.yml` can be used to store multiple secrets with a date (this way secrets can be updated without invalidating current tokens). The format for the file is:
171
+ ```yaml
172
+ ---
173
+ - CREATED_AT: EPOCH TIME IN SECONDS
174
+ SECRET: new_secret
175
+ - CREATED_AT: EPOCH TIME IN SECONDS
176
+ SECRET: old_secret
177
+ ...
178
+ ```
179
+ The order should be newer to older secrets. This file can be created/updated manually or using `rake slots:new_secret`. If using `rake slots:new_secret` secrets that are older than session_lifetime will be removed. When updating manually remember to restart the server `rake restart`.
180
+
181
+ ## Routes
182
+
183
+ All these routes will be mounted at the route used above in `mount Slots::Engine =>`.
184
+
185
+ | Route Helper | Route | Token | |
186
+ | ------------ | ----- | ----- | --- |
187
+ | `slots.sign_in` | GET/POST `/sign_in` | Does not require Token | This is used to sign in. login and password are expected as params. If the credentials are valid the user is returned with the token in header in the following format: `'authorization' => 'Bearer token=TOKEN'` (same as sending) |
188
+ | `slots.sign_out` | DELETE `/sign_out` | Requires Token | This is used to sign out. This will delete the session if one exist for the token. |
189
+ | `slots.update_session_token` | GET `/update_session_token` | Requires Token (token can be expired). | This is used to force a new token to be returned from an expired token using the session in the JWT. The token is returned in the same way as sign_in. |
190
+
191
+
192
+ ### Why use session token inside a JWS?
193
+
194
+ Good question, first it's important to talk about some of the reasons (well maybe just one of the reasons) for using JWS:
195
+ - They are stateless. The nice thing is you don't have to go query a database to see if the session exist. Also if you have two different services that don't share a database they can validate the request by having the same secret.
196
+
197
+ Some of the problems with JWS:
198
+ - Since the tokens are stateless its hard to revoke a token before it expires. In the case of this gem revoking a token is important for signing out. Some solutions suggested are:
199
+
200
+ | Solutions | Problems |
201
+ | ---------- | -------- |
202
+ | Set long expatriation and ignore signing out (Have front end handle it by saving the token if the user wants to stay signed in) | If the token is compromised the token is still valid for X time. You can only revoke it by creating a new secret, which would require all users to get a new tokens. |
203
+ | Set long expiration and Store JWS in database | Not stateless. |
204
+ | Set long expiration and Blacklist JWS to revoke (or when a user signs out) | Better... but still not stateless. |
205
+ | Set short expiration and have a refresher/session token | When user signs out tokens are still valid for X time (which should be short). If the user info is changed (like a user is deactivated) the token is still valid until it expires. If a token with a session is compromised it can be revoked by removing that session (or all sessions if needed). |
206
+
207
+ 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
+
209
+
210
+ ## Contributing
211
+
212
+
213
+ ## License
214
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'Slots'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
20
+ load 'rails/tasks/engine.rake'
21
+
22
+ load 'rails/tasks/statistics.rake'
23
+
24
+ require 'bundler/gem_tasks'
25
+
26
+ require 'rake/testtask'
27
+
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.libs << 'test'
30
+ t.pattern = 'test/**/*_test.rb'
31
+ t.verbose = false
32
+ end
33
+
34
+ task default: :test
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ class SessionsController < ApplicationController
5
+ update_expired_session_tokens! only: :update_session_token # needed if token is expired
6
+ require_user_load! only: :update_session_token
7
+ require_login! only: [:update_session_token, :sign_out]
8
+ skip_callback!
9
+
10
+ def sign_in
11
+ @_current_user = _authentication_model.find_for_authentication(params[:login])
12
+
13
+ current_user.authenticate!(params[:password])
14
+
15
+ new_token!(ActiveModel::Type::Boolean.new.cast(params[:session]))
16
+ render json: current_user.as_json, status: :accepted
17
+ end
18
+
19
+ def sign_out
20
+ Slots::Session.find_by(session: jw_token.session)&.delete if jw_token.session.present?
21
+ head :ok
22
+ end
23
+
24
+ def update_session_token
25
+ # TODO think about not allowing user to get new token here because then there
26
+ current_user.update_token unless current_user.new_token?
27
+ render json: current_user.as_json, status: :accepted
28
+ end
29
+
30
+ private
31
+
32
+ def _authentication_model
33
+ Slots.configuration.authentication_model
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: "from@example.com"
6
+ layout "mailer"
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ class Session < ApplicationRecord
5
+ belongs_to :user, session_assocaition
6
+ before_validation :create_random_session, on: :create
7
+ validates :session, :jwt_iat, presence: true
8
+ validates :session, uniqueness: true
9
+
10
+ def update_random_session
11
+ self.session = SecureRandom.hex(32)
12
+ end
13
+
14
+ def self.expired
15
+ self.where(self.arel_table[:created_at].lte(Slots.configuration.session_lifetime.ago))
16
+ end
17
+
18
+ def self.not_expired
19
+ self.where(self.arel_table[:created_at].gt(Slots.configuration.session_lifetime.ago))
20
+ end
21
+
22
+ def self.matches_jwt(sloken_jws)
23
+ jwt_where = self.arel_table[:jwt_iat].eq(sloken_jws.iat)
24
+ if Slots.configuration.previous_jwt_lifetime
25
+ jwt_where = jwt_where.or(
26
+ Arel::Nodes::Grouping.new(
27
+ self.arel_table[:previous_jwt_iat].eq(sloken_jws.iat)
28
+ .and(self.arel_table[:jwt_iat].gt(Slots.configuration.previous_jwt_lifetime.ago.to_i))
29
+ )
30
+ )
31
+ end
32
+
33
+ self.not_expired
34
+ .where(jwt_where)
35
+ .find_by(session: sloken_jws.session)
36
+ end
37
+ private
38
+ def create_random_session
39
+ update_random_session unless self.session
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ user_model = Slots.configuration.authentication_model&.name&.underscore || ""
4
+ Slots::Engine.routes.draw do
5
+ get 'sign_in', to: 'sessions#sign_in'
6
+ post 'sign_in', to: 'sessions#sign_in'
7
+ delete 'sign_out', to: 'sessions#sign_out'
8
+ get 'update_session_token', to: 'sessions#update_session_token'
9
+ # get 'valid_token', to: 'sessions#valid_token' TODO not sure if valid token is needed
10
+ end
@@ -0,0 +1,13 @@
1
+ Description:
2
+ This is used for creating slots config file and adding the routes to config/routes.rb
3
+
4
+ Example:
5
+ rails generate slots:install
6
+
7
+ This will create:
8
+ config/initializers.slots.rb
9
+ db/migrate/YYYYMMDDHHMMSS_create_slots_sessions.rb
10
+
11
+ This will add:
12
+ TO: config/routes.rb
13
+ mount Slots::Engine => "/slots"
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slots
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ def copy_initializer
8
+ template "slots.rb", "config/initializers/slots.rb"
9
+ template "create_slots_sessions.rb", "db/migrate/#{Time.now.strftime("%Y%m%d%H%M%S")}_create_slots_sessions.rb"
10
+ end
11
+
12
+ def add_route
13
+ route "mount Slots::Engine => '/auth'"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ class CreateSlotsSessions < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :slots_sessions do |t|
4
+ t.string :session, length: 128
5
+ t.bigint :jwt_iat
6
+ t.bigint :previous_jwt_iat
7
+ t.bigint :user_id, index: true
8
+
9
+ t.timestamps
10
+ end
11
+ add_index :slots_sessions, :session, unique: true
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ Slots.configure do |config|
2
+ # config.logins = :email || {email: /@/, username: //}
3
+ # config.login_regex_validations = true
4
+ # config.authentication_model = 'User'
5
+ # config.secret = ENV['SLOT_SECRET']
6
+ # config.token_lifetime = 1.hour
7
+ # config.session_lifetime = 2.weeks
8
+ # config.previous_jwt_lifetime = 5.seconds # Set to nil if you dont want previous_jwt_lifetime
9
+ # config.secret_yaml = false # Set to true to read secret from config/slots_secrets.yml
10
+ end