toker 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +120 -0
- data/Rakefile +35 -0
- data/app/controllers/toker/application_controller.rb +6 -0
- data/app/controllers/toker/sessions_controller.rb +51 -0
- data/app/controllers/toker/users_controller.rb +31 -0
- data/app/helpers/toker/application_helper.rb +4 -0
- data/app/models/toker/token.rb +30 -0
- data/app/models/toker/user.rb +11 -0
- data/app/serializers/toker/token_serializer.rb +9 -0
- data/app/serializers/toker/user_serializer.rb +5 -0
- data/config/initializers/active_model_serializers.rb +1 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20130822225749_create_toker_users.rb +10 -0
- data/db/migrate/20130908035917_create_toker_tokens.rb +11 -0
- data/lib/tasks/toker_tasks.rake +4 -0
- data/lib/toker.rb +37 -0
- data/lib/toker/engine.rb +5 -0
- data/lib/toker/version.rb +3 -0
- data/spec/controllers/toker/sessions_controller_spec.rb +7 -0
- data/spec/controllers/toker/users_controller_spec.rb +7 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/controllers/things_controller.rb +17 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/thing.rb +3 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +34 -0
- data/spec/dummy/bin/update +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +18 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +9 -0
- data/spec/dummy/config/database.yml +26 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +54 -0
- data/spec/dummy/config/environments/production.rb +86 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/new_framework_defaults.rb +23 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/puma.rb +47 -0
- data/spec/dummy/config/routes.rb +7 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/db/migrate/20161125174125_create_things.rb +10 -0
- data/spec/dummy/db/schema.rb +42 -0
- data/spec/dummy/log/RAILS_ENV=test.log +0 -0
- data/spec/dummy/log/development.log +936 -0
- data/spec/dummy/log/test.log +152724 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/toker/tokens.rb +4 -0
- data/spec/factories/toker/users.rb +7 -0
- data/spec/features/toker/session_spec.rb +159 -0
- data/spec/features/toker/token_auth_spec.rb +103 -0
- data/spec/features/toker/user_spec.rb +124 -0
- data/spec/models/toker/token_spec.rb +68 -0
- data/spec/models/toker/user_spec.rb +62 -0
- data/spec/rails_helper.rb +57 -0
- data/spec/spec_helper.rb +97 -0
- metadata +280 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 05f09692a0b7434bd2b9f5863d56d0bb1f7a095f
|
4
|
+
data.tar.gz: 59b9044130987a282cf44d0fa87d98aa1d4f8af9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 55394ef9108cda69194d6a282ad88ac2e1d383c6c9ec306e9b6272b6881f1f770d1fdfa2b9a9d98ae6bb390b2aa3d6cef250e99f151f7e0319d2af0a6071892b
|
7
|
+
data.tar.gz: 3129c5c317708647272eabcc8c289af3f057d29a62ffef15d62205ee89495ccb1acd77ca4559cb522eabf7b732e055ce5cbe2ed5a7195fbb74cac03de1cd18ee
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
Copyright © 2016 Jack Flannery
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
this software and associated documentation files (the “Software”), to deal in
|
6
|
+
the Software without restriction, including without limitation the rights to
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
12
|
+
copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# Toke
|
2
|
+
|
3
|
+
Toke is a Rails engine that is designed to be mounted in a Rails API and provides simple token based authentication. Toke provides a user model and restful JSON routes for registering new users, logging in, and logging out. Toke will return a JSON Web Token (JWT) upon successfull login, which will be required for any actions that you choose to secure by using the provided `before_action toke!`
|
4
|
+
|
5
|
+
**Warning: Toke uses an http header to pass the token, so use always https.**
|
6
|
+
|
7
|
+
### Install
|
8
|
+
|
9
|
+
gem install toke
|
10
|
+
|
11
|
+
or in a your Gemfile:
|
12
|
+
|
13
|
+
gem 'toke', '~> 0.1.0'
|
14
|
+
|
15
|
+
#### Install the migrations
|
16
|
+
|
17
|
+
Toke uses two Active Record models: `Toke::User` and `Toke::Token`. `Users` have one (`has_one`) `Token` and `Tokens` belong to (`belong_to`) `Users`. The tables for the models are `toke_users` and `toke_tokens`. To copy the two migrations into your application's `db/migrate` directory, that will create these tables, run the following rake command:
|
18
|
+
|
19
|
+
rake install:toke:migrations
|
20
|
+
|
21
|
+
Feel free to add additional fields to these migrations, but be sure not to remove anything, all are required. Migrate your datebase as usual:
|
22
|
+
|
23
|
+
rake db:migrate
|
24
|
+
|
25
|
+
#### Mount the Toke engine
|
26
|
+
|
27
|
+
Toke's routes need to be added to your application's routes. You can namespace the tokes routes by mounting Toke at a path such `/toke` by adding the following line to your routes file:
|
28
|
+
|
29
|
+
mount Toke::Engine => "/toke"
|
30
|
+
|
31
|
+
Or you can just mount Toke at `/` if that works for you:
|
32
|
+
|
33
|
+
mount Toke::Engine => "/"
|
34
|
+
|
35
|
+
Run `rake routes` to see the all of the routes that are added to your application.
|
36
|
+
|
37
|
+
Finally include the `Toke::TokenAuthentication` module in your ApplicationController, or any individual controller where you need the `toke!` and `current_user` methods available.
|
38
|
+
|
39
|
+
include Toke::TokenAuthentication
|
40
|
+
|
41
|
+
### Log In
|
42
|
+
|
43
|
+
If you have a user with email: jack@example.com and password "secret", login with a POST request using http basic authentication to pass the email and password in a header, with an empty body. This can be demonstrated with the curl: (all expamles assume that Toke is mounted at `/`)
|
44
|
+
|
45
|
+
curl -i -X POST example.com/login -u "jack@example.com:secret"
|
46
|
+
|
47
|
+
If authentication fails, status code `401 Unauthorized` will be returned.
|
48
|
+
|
49
|
+
If authentication is successfull, then status code `201 Created` will be returned, and the body will contain the JSON representation of the logged in user. Also returned will be an `Authorization` header containing the token, for example:
|
50
|
+
|
51
|
+
Authorization: Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl9pZCI6MSwiZXhwIjoxNTExODAzNzEzfQ.6nBJNPQ5jJsSOOCAWtAjaMnU3r6ofECsC9ckm4YbGrU
|
52
|
+
|
53
|
+
If you store the token on the client, such as in browser local storage, and need to check if it is still valid and not expired, send a `PUT` request to `/login` with the token in an `Authorization` request header. For example on a single page web app, you may want to validate the token is valid and get the `User` object back in response on every page load when the user is logged in. Done in curl that would look like this:
|
54
|
+
|
55
|
+
curl -i -X PUT example.com/login -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl9pZCI6MSwiZXhwIjoxNTExODAzNzEzfQ.6nBJNPQ5jJsSOOCAWtAjaMnU3r6ofECsC9ckm4YbGrU'
|
56
|
+
|
57
|
+
If successful `200 OK` will be returned along the `User` object in the body. If the token is expired or invalid a `401 Unauthorized` will be returned instead.
|
58
|
+
|
59
|
+
### Securing a controller action
|
60
|
+
|
61
|
+
The `toke!` method is provided to use as a before action to secure your API endpoints. Add the following to a controller that should require authentication
|
62
|
+
|
63
|
+
before_action toke!
|
64
|
+
|
65
|
+
Secured endpoints will now require an `Authorization` request header containing the token recieved in the response header of the login API call. If you have a the following `PostsController`
|
66
|
+
|
67
|
+
class PostsController < ApplicationController
|
68
|
+
before_action :toke!
|
69
|
+
|
70
|
+
# ...
|
71
|
+
end
|
72
|
+
|
73
|
+
and a routes file like this:
|
74
|
+
|
75
|
+
Rails.application.routes.draw do
|
76
|
+
mount Toke::Engine => "/"
|
77
|
+
resources :posts
|
78
|
+
end
|
79
|
+
|
80
|
+
A get request to the index action would look like this with curl:
|
81
|
+
|
82
|
+
curl -i example.com/posts -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl9pZCI6MSwiZXhwIjoxNTExODAzNzEzfQ.6nBJNPQ5jJsSOOCAWtAjaMnU3r6ofECsC9ckm4YbGrU'
|
83
|
+
|
84
|
+
If the token matches and is not expired, then your controller action will execute. Inside the contoller action, `current_user` will be available, giving you access to the `User` object that matched the given token.
|
85
|
+
|
86
|
+
If the token is expired, invalid or not given at all, by default a status of `401 Unauthorized` and an empty response body will be returned.
|
87
|
+
|
88
|
+
You can change the default behavior of returning `401` by passing a block to `toke!`. The block will be executed when authentication fails. For example, you may want your controller to have a limited functionality to anyone not logged in with a token. You may have published posts that you would permit anyone to access. However logged in users with a token should have access to all of their own posts, published or not. You can achieve this with something like the following:
|
89
|
+
|
90
|
+
class PostsController < ApplicationController
|
91
|
+
before_action :toke!, only: [:create, :update, :destroy]
|
92
|
+
|
93
|
+
before_action only: :index do
|
94
|
+
toke! do |errors|
|
95
|
+
render json: Post.published
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
before_action only: :show do
|
100
|
+
toke! do |errors|
|
101
|
+
render json: Post.published.find(params[:id])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def index
|
106
|
+
render json: current_user.posts
|
107
|
+
end
|
108
|
+
|
109
|
+
def show
|
110
|
+
render json: current_user.posts.find(params[:id])
|
111
|
+
end
|
112
|
+
|
113
|
+
# ...
|
114
|
+
end
|
115
|
+
|
116
|
+
### Log Out
|
117
|
+
|
118
|
+
To logout send a `delete` request to `/logout` passing the token as you would on any other secured endpoint. In curl:
|
119
|
+
|
120
|
+
curl -i -X DELETE example.com/logout -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl9pZCI6MSwiZXhwIjoxNTExODAzNzEzfQ.6nBJNPQ5jJsSOOCAWtAjaMnU3r6ofECsC9ckm4YbGrU'
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env rake
|
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 = 'Toke'
|
14
|
+
rdoc.options << '--line-numbers'
|
15
|
+
rdoc.rdoc_files.include('README.rdoc')
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
17
|
+
end
|
18
|
+
|
19
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
20
|
+
load 'rails/tasks/engine.rake'
|
21
|
+
|
22
|
+
Bundler::GemHelper.install_tasks
|
23
|
+
|
24
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f }
|
25
|
+
|
26
|
+
require 'rake/testtask'
|
27
|
+
|
28
|
+
Rake::TestTask.new(:test) do |t|
|
29
|
+
t.libs << 'lib'
|
30
|
+
t.libs << 'test'
|
31
|
+
t.pattern = 'test/**/*_test.rb'
|
32
|
+
t.verbose = false
|
33
|
+
end
|
34
|
+
|
35
|
+
task default: :test
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Toker
|
2
|
+
class SessionsController < ApplicationController
|
3
|
+
before_action :toke!, only: :destroy
|
4
|
+
|
5
|
+
def create
|
6
|
+
@user = authenticate_with_http_basic do |email, password|
|
7
|
+
user = User.find_by email: email
|
8
|
+
if user && user.authenticate(password)
|
9
|
+
user.token.destroy if user.token
|
10
|
+
user.token = Token.create expires_at: 1.year.from_now
|
11
|
+
user.token.generate_key!
|
12
|
+
user.token.save
|
13
|
+
user
|
14
|
+
end
|
15
|
+
end
|
16
|
+
if @user
|
17
|
+
response.headers['Authorization'] = "Token #{@user.token.key}"
|
18
|
+
render json: @user, status: :created
|
19
|
+
else
|
20
|
+
render json: { Unauthorized: 'Invalid email or password' }, status: :unauthorized
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def update
|
25
|
+
token = authenticate_with_http_token do |jwt, options|
|
26
|
+
Token.decode(jwt)[0]
|
27
|
+
end
|
28
|
+
user = token.user if token
|
29
|
+
if user
|
30
|
+
response.headers['Authorization'] = "Token #{token.key}"
|
31
|
+
render json: user
|
32
|
+
else
|
33
|
+
render json: { Unauthorized: 'Invalid session' }, status: :unauthorized
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def destroy
|
38
|
+
@user.token.destroy
|
39
|
+
head :no_content
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def payload
|
45
|
+
{
|
46
|
+
user_id: @user.id,
|
47
|
+
exp: 1.year.from_now.to_i
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Toker
|
2
|
+
|
3
|
+
class UsersController < ApplicationController
|
4
|
+
|
5
|
+
before_action :toke!
|
6
|
+
|
7
|
+
def create
|
8
|
+
user = User.new(person_params)
|
9
|
+
if user.save
|
10
|
+
render json: user, status: 201
|
11
|
+
else
|
12
|
+
head 500
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def index
|
17
|
+
render json: User.all
|
18
|
+
end
|
19
|
+
|
20
|
+
def show
|
21
|
+
user = User.find(params[:id])
|
22
|
+
render json: user
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def person_params
|
28
|
+
params.require(:user).permit(:email, :password, :password_confirmation)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Toker
|
2
|
+
class Token < ActiveRecord::Base
|
3
|
+
belongs_to :user
|
4
|
+
|
5
|
+
def generate_key!
|
6
|
+
self.key = JWT.encode payload, Rails.application.secrets.secret_key_base, 'HS256'
|
7
|
+
return self.key
|
8
|
+
end
|
9
|
+
|
10
|
+
def payload
|
11
|
+
{
|
12
|
+
token_id: id,
|
13
|
+
exp: expires_at.to_i
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.decode(jwt)
|
18
|
+
begin
|
19
|
+
decoded_token = JWT.decode jwt, Rails.application.secrets.secret_key_base, 'HS256'
|
20
|
+
token_id = decoded_token[0]['token_id']
|
21
|
+
token = Token.find(token_id)
|
22
|
+
rescue JWT::ExpiredSignature
|
23
|
+
error = { Unauthorized: 'Token expired' }
|
24
|
+
rescue JWT::DecodeError, JWT::VerificationError, ActiveRecord::RecordNotFound
|
25
|
+
error = { Unauthorized: 'Token invalid' }
|
26
|
+
end
|
27
|
+
[token, error]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Toker
|
2
|
+
class User < ActiveRecord::Base
|
3
|
+
has_secure_password
|
4
|
+
|
5
|
+
validates :email, presence: true, uniqueness: true
|
6
|
+
validates :password, confirmation: true, length: { in: 6..50 }
|
7
|
+
validates :password_confirmation, presence: true
|
8
|
+
|
9
|
+
has_one :token
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
ActiveModelSerializers.config.adapter = :json
|
data/config/routes.rb
ADDED
data/lib/toker.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "toker/engine"
|
2
|
+
require "active_model_serializers"
|
3
|
+
require "jwt"
|
4
|
+
|
5
|
+
module Toker
|
6
|
+
module TokenAuthentication
|
7
|
+
include ActionController::HttpAuthentication::Token::ControllerMethods
|
8
|
+
|
9
|
+
def toke!(&unauthorized_handler)
|
10
|
+
token = authenticate_with_http_token do |jwt, options|
|
11
|
+
token, error = Token.decode(jwt)
|
12
|
+
errors.merge!(error) if error
|
13
|
+
token
|
14
|
+
end
|
15
|
+
|
16
|
+
@user = token.user if token
|
17
|
+
|
18
|
+
unless @user
|
19
|
+
if block_given?
|
20
|
+
unauthorized_handler.call(errors)
|
21
|
+
else
|
22
|
+
render json: errors, status: :unauthorized
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def current_user
|
28
|
+
@user
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def errors
|
34
|
+
@errors ||= { 'Unauthorized' => 'Token required' }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/toker/engine.rb
ADDED