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.
- checksums.yaml +4 -4
- data/README.md +121 -6
- data/app/controllers/slots/jwt/sessions_controller.rb +38 -0
- data/app/models/slots/jwt/application_record.rb +9 -0
- data/app/models/slots/jwt/session.rb +44 -0
- data/config/initializers/inflections.rb +3 -0
- data/config/routes.rb +2 -2
- data/lib/generators/slots/install/USAGE +1 -1
- data/lib/generators/slots/install/install_generator.rb +1 -1
- data/lib/generators/slots/install/templates/create_slots_sessions.rb +2 -2
- data/lib/generators/slots/install/templates/slots.rb +1 -1
- data/lib/generators/slots/model/model_generator.rb +1 -1
- data/lib/slots.rb +1 -44
- data/lib/slots/jwt.rb +49 -0
- data/lib/slots/jwt/authentication_helper.rb +147 -0
- data/lib/slots/jwt/configuration.rb +84 -0
- data/lib/slots/jwt/database_authentication.rb +21 -0
- data/lib/slots/jwt/engine.rb +9 -0
- data/lib/slots/jwt/extra_classes.rb +14 -0
- data/lib/slots/jwt/generic_methods.rb +53 -0
- data/lib/slots/jwt/generic_validations.rb +53 -0
- data/lib/slots/jwt/permission_filter.rb +37 -0
- data/lib/slots/jwt/slokens.rb +115 -0
- data/lib/slots/jwt/tests.rb +37 -0
- data/lib/slots/jwt/tokens.rb +104 -0
- data/lib/slots/jwt/type_helper.rb +30 -0
- data/lib/slots/{version.rb → jwt/version.rb} +3 -1
- data/lib/tasks/slots_tasks.rake +5 -5
- metadata +23 -19
- data/app/controllers/slots/sessions_controller.rb +0 -36
- data/app/mailers/slots/application_mailer.rb +0 -8
- data/app/models/slots/application_record.rb +0 -7
- data/app/models/slots/session.rb +0 -42
- data/lib/slots/authentication_helper.rb +0 -144
- data/lib/slots/configuration.rb +0 -82
- data/lib/slots/database_authentication.rb +0 -19
- data/lib/slots/engine.rb +0 -7
- data/lib/slots/extra_classes.rb +0 -12
- data/lib/slots/generic_methods.rb +0 -51
- data/lib/slots/generic_validations.rb +0 -51
- data/lib/slots/slokens.rb +0 -113
- data/lib/slots/tests.rb +0 -35
- data/lib/slots/tokens.rb +0 -102
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 839d8b81d5e72a4e4034aa14fcbf8ba479f7137d35e3ac31ff3f853a780f8925
|
4
|
+
data.tar.gz: 1c0b98884bb29be89fa2900ae627c08f7a9aa17871ec161643388b3a22ed0e16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,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
|
data/config/routes.rb
CHANGED
@@ -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'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class CreateSlotsSessions < ActiveRecord::Migration[5.2]
|
2
2
|
def change
|
3
|
-
create_table :
|
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 :
|
11
|
+
add_index :slots_jwt_sessions, :session, unique: true
|
12
12
|
end
|
13
13
|
end
|
@@ -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
|
data/lib/slots.rb
CHANGED
@@ -1,46 +1,3 @@
|
|
1
|
-
|
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
|
data/lib/slots/jwt.rb
ADDED
@@ -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
|