toker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +120 -0
  4. data/Rakefile +35 -0
  5. data/app/controllers/toker/application_controller.rb +6 -0
  6. data/app/controllers/toker/sessions_controller.rb +51 -0
  7. data/app/controllers/toker/users_controller.rb +31 -0
  8. data/app/helpers/toker/application_helper.rb +4 -0
  9. data/app/models/toker/token.rb +30 -0
  10. data/app/models/toker/user.rb +11 -0
  11. data/app/serializers/toker/token_serializer.rb +9 -0
  12. data/app/serializers/toker/user_serializer.rb +5 -0
  13. data/config/initializers/active_model_serializers.rb +1 -0
  14. data/config/routes.rb +9 -0
  15. data/db/migrate/20130822225749_create_toker_users.rb +10 -0
  16. data/db/migrate/20130908035917_create_toker_tokens.rb +11 -0
  17. data/lib/tasks/toker_tasks.rake +4 -0
  18. data/lib/toker.rb +37 -0
  19. data/lib/toker/engine.rb +5 -0
  20. data/lib/toker/version.rb +3 -0
  21. data/spec/controllers/toker/sessions_controller_spec.rb +7 -0
  22. data/spec/controllers/toker/users_controller_spec.rb +7 -0
  23. data/spec/dummy/README.rdoc +28 -0
  24. data/spec/dummy/Rakefile +6 -0
  25. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  26. data/spec/dummy/app/controllers/things_controller.rb +17 -0
  27. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  28. data/spec/dummy/app/models/thing.rb +3 -0
  29. data/spec/dummy/bin/bundle +3 -0
  30. data/spec/dummy/bin/rails +4 -0
  31. data/spec/dummy/bin/rake +4 -0
  32. data/spec/dummy/bin/setup +34 -0
  33. data/spec/dummy/bin/update +29 -0
  34. data/spec/dummy/config.ru +4 -0
  35. data/spec/dummy/config/application.rb +18 -0
  36. data/spec/dummy/config/boot.rb +5 -0
  37. data/spec/dummy/config/cable.yml +9 -0
  38. data/spec/dummy/config/database.yml +26 -0
  39. data/spec/dummy/config/environment.rb +5 -0
  40. data/spec/dummy/config/environments/development.rb +54 -0
  41. data/spec/dummy/config/environments/production.rb +86 -0
  42. data/spec/dummy/config/environments/test.rb +42 -0
  43. data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
  44. data/spec/dummy/config/initializers/assets.rb +11 -0
  45. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  46. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  47. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  48. data/spec/dummy/config/initializers/inflections.rb +16 -0
  49. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  50. data/spec/dummy/config/initializers/new_framework_defaults.rb +23 -0
  51. data/spec/dummy/config/initializers/session_store.rb +3 -0
  52. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  53. data/spec/dummy/config/locales/en.yml +23 -0
  54. data/spec/dummy/config/puma.rb +47 -0
  55. data/spec/dummy/config/routes.rb +7 -0
  56. data/spec/dummy/config/secrets.yml +22 -0
  57. data/spec/dummy/config/spring.rb +6 -0
  58. data/spec/dummy/db/migrate/20161125174125_create_things.rb +10 -0
  59. data/spec/dummy/db/schema.rb +42 -0
  60. data/spec/dummy/log/RAILS_ENV=test.log +0 -0
  61. data/spec/dummy/log/development.log +936 -0
  62. data/spec/dummy/log/test.log +152724 -0
  63. data/spec/dummy/public/favicon.ico +0 -0
  64. data/spec/factories/toker/tokens.rb +4 -0
  65. data/spec/factories/toker/users.rb +7 -0
  66. data/spec/features/toker/session_spec.rb +159 -0
  67. data/spec/features/toker/token_auth_spec.rb +103 -0
  68. data/spec/features/toker/user_spec.rb +124 -0
  69. data/spec/models/toker/token_spec.rb +68 -0
  70. data/spec/models/toker/user_spec.rb +62 -0
  71. data/spec/rails_helper.rb +57 -0
  72. data/spec/spec_helper.rb +97 -0
  73. metadata +280 -0
@@ -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.
@@ -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'
@@ -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,6 @@
1
+ module Toker
2
+ class ApplicationController < ActionController::API
3
+ include ActionController::HttpAuthentication::Basic::ControllerMethods
4
+ include Toker::TokenAuthentication
5
+ end
6
+ end
@@ -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,4 @@
1
+ module Toker
2
+ module ApplicationHelper
3
+ end
4
+ 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,9 @@
1
+ module Toker
2
+ class TokenSerializer < ActiveModel::Serializer
3
+ attributes :id, :key, :expires_at, :user_id
4
+
5
+ def expires_at
6
+ object.expires_at.to_s(:db)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Toker
2
+ class UserSerializer < ActiveModel::Serializer
3
+ attributes :id, :email
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ ActiveModelSerializers.config.adapter = :json
@@ -0,0 +1,9 @@
1
+ Toker::Engine.routes.draw do
2
+ post 'register', to: 'users#create'
3
+
4
+ post 'login', to: 'sessions#create'
5
+ put 'login', to: 'sessions#update'
6
+ delete 'logout', to: 'sessions#destroy'
7
+
8
+ resources :users, only: [:index, :show]
9
+ end
@@ -0,0 +1,10 @@
1
+ class CreateTokerUsers < ActiveRecord::Migration
2
+ def change
3
+ create_table :toker_users do |t|
4
+ t.string :email, index: true
5
+ t.string :password_digest
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ class CreateTokerTokens < ActiveRecord::Migration
2
+ def change
3
+ create_table :toker_tokens do |t|
4
+ t.references :user, index: true
5
+ t.string :key, limit: 256
6
+ t.timestamp :expires_at
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :toker do
3
+ # # Task goes here
4
+ # end
@@ -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
@@ -0,0 +1,5 @@
1
+ module Toker
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Toker
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Toker
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'rails_helper'
2
+
3
+ module Toker
4
+
5
+ describe SessionsController do
6
+ end
7
+ end