token_authenticate_me 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rubocop.yml +8 -0
  4. data/Gemfile +7 -0
  5. data/LICENSE +20 -0
  6. data/README.md +47 -0
  7. data/Rakefile +17 -0
  8. data/app/mailers/token_authenticate_me_mailer.rb +28 -0
  9. data/app/views/token_authenticate_me_mailer/invalid_user_reset_password_email.html.erb +20 -0
  10. data/app/views/token_authenticate_me_mailer/invalid_user_reset_password_email.text.erb +13 -0
  11. data/app/views/token_authenticate_me_mailer/valid_user_reset_password_email.html.erb +16 -0
  12. data/app/views/token_authenticate_me_mailer/valid_user_reset_password_email.text.erb +9 -0
  13. data/config.ru +7 -0
  14. data/lib/generators/token_authenticate_me/controllers/controllers_generator.rb +37 -0
  15. data/lib/generators/token_authenticate_me/controllers/templates/password_reset.rb +6 -0
  16. data/lib/generators/token_authenticate_me/controllers/templates/sessions.rb +6 -0
  17. data/lib/generators/token_authenticate_me/install/install_generator.rb +12 -0
  18. data/lib/generators/token_authenticate_me/models/models_generator.rb +55 -0
  19. data/lib/generators/token_authenticate_me/models/templates/authentication_migration.rb +20 -0
  20. data/lib/generators/token_authenticate_me/models/templates/authentication_model.rb +6 -0
  21. data/lib/generators/token_authenticate_me/models/templates/session_migration.rb +17 -0
  22. data/lib/generators/token_authenticate_me/models/templates/session_model.rb +6 -0
  23. data/lib/token_authenticate_me.rb +6 -0
  24. data/lib/token_authenticate_me/controllers/password_resetable.rb +90 -0
  25. data/lib/token_authenticate_me/controllers/sessionable.rb +63 -0
  26. data/lib/token_authenticate_me/controllers/token_authenticateable.rb +43 -0
  27. data/lib/token_authenticate_me/engine.rb +5 -0
  28. data/lib/token_authenticate_me/models/authenticatable.rb +40 -0
  29. data/lib/token_authenticate_me/models/sessionable.rb +27 -0
  30. data/lib/token_authenticate_me/version.rb +3 -0
  31. data/spec/acceptance/password_reset_api_spec.rb +111 -0
  32. data/spec/acceptance/session_api_spec.rb +95 -0
  33. data/spec/acceptance/users_api_spec.rb +56 -0
  34. data/spec/internal/app/controllers/application_controller.rb +5 -0
  35. data/spec/internal/app/controllers/password_resets_controller.rb +5 -0
  36. data/spec/internal/app/controllers/sessions_controller.rb +5 -0
  37. data/spec/internal/app/controllers/users_controller.rb +7 -0
  38. data/spec/internal/app/models/session.rb +5 -0
  39. data/spec/internal/app/models/user.rb +5 -0
  40. data/spec/internal/app/policies/user_policy.rb +25 -0
  41. data/spec/internal/app/serializers/user_serializer.rb +3 -0
  42. data/spec/internal/config/database.yml +3 -0
  43. data/spec/internal/config/routes.rb +13 -0
  44. data/spec/internal/db/fixtures/users.rb +11 -0
  45. data/spec/internal/db/schema.rb +19 -0
  46. data/spec/spec_helper.rb +38 -0
  47. data/token_authenticate_me.gemspec +32 -0
  48. metadata +245 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6bf685e7c83d225932af75831c2adf88c38057ce
4
+ data.tar.gz: 153e157ba981b72d510a6bfc5c0eb6cc2bf6f2d6
5
+ SHA512:
6
+ metadata.gz: 4ec02dc8c34693f6330f4f987d4dcbeaa6b4535e3e0f9917c0b729094ef3b85d6a6111ca5c0f4e3421dec673759cdd7b7e642a71a75e257c725fd79d3be88d5f
7
+ data.tar.gz: 1e1c50ffd2dc2cc1c1be0e9e90e71b09b64b3dbdd522035e2bbd5acc5aa1606f02e92a468f83c91a5083ff2c3af80b2ce762186e8c2ce1adc18fe6f4ee1777b7
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ spec/internal/db/*sqlite
3
+ pkg/
4
+ spec/internal/log/*
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ Exclude:
3
+ - '**/templates/*'
4
+ - '*.gemspec'
5
+ LineLength:
6
+ Max: 100
7
+ Documentation:
8
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'active_model_serializers', '~> 0.8.0'
7
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 Sam Clopton
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.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ TokenAuthenticateMe
2
+ =====================
3
+
4
+ This gem adds simple API based token authentication. We at [inigo](http://inigo.io) wanted to be able to handle our entire authentication process -- including account creation and logging in -- through a RESTful API over JSON using token authentication, and found that solutions like Devise required too much hand holding due to its complexity to ultimately get the functionality that we wanted. Unfortunately we were unable to find a satisfactory existing solution -- though I'm sure one does exist, this isn't a new problem -- so we set out to create our own. After using internally on one project, we decided to roll it out into a gem to use on another.
5
+
6
+ ## Getting started
7
+
8
+ Add the gem to your Gemfile:
9
+ `gem token_authenticate_me`
10
+
11
+ Run `bundle install` to install it.
12
+
13
+ To add or create a user with token authentication run:
14
+ `rails generate token_authenticate_me:install <model>`
15
+
16
+ Replace `<model>` with the class name used for users. This will create the necessary migration files, and optionally create the model file if it does not exist.
17
+
18
+ **Right now this gem only supports creating the authentication model `User`, so it is recommended to call `rails generate token_authenticate_me:install user`**
19
+
20
+ Include TokenAuthenticateMe::TokenAuthentication into the application controller or any controllers that require authorization:
21
+ ````rb
22
+ require 'token_authenticate_me/token_authentication'
23
+
24
+ class ApplicationController < ActionController::Base
25
+ force_ssl if Rails.env.production?
26
+
27
+ # Prevent CSRF attacks by raising an exception.
28
+ # For APIs, you may want to use :null_session instead.
29
+ protect_from_forgery with: :exception
30
+
31
+ include TokenAuthenticateMe::TokenAuthentication
32
+
33
+ #...
34
+ end
35
+ ````
36
+
37
+ To skip authentication in a controller, just skip the authenticate before action:
38
+ ````rb
39
+ class Api::V1::UsersController < Api::BaseController
40
+
41
+ # Allow new users to create an account
42
+ skip_before_action :authenticate, only: [:create]
43
+ end
44
+ ````
45
+
46
+ ### TODO:
47
+ [ ] - Make it so any resource name can be used for authentication (initial thought is either specify the default or pass resource name in token string?).
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'bundler'
2
+
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ task :console do
6
+ require 'irb'
7
+ require 'irb/completion'
8
+ require 'token_authenticate_me' # You know what to do.
9
+ ARGV.clear
10
+ IRB.start
11
+ end
12
+
13
+ task default: [:rubocop]
14
+
15
+ task :rubocop do
16
+ system 'bundle exec rubocop -RD'
17
+ end
@@ -0,0 +1,28 @@
1
+ class TokenAuthenticateMeMailer < ActionMailer::Base
2
+ SIGNUP_PATH = 'sign-up'
3
+ RESET_PATH = 'reset-password/:token/'
4
+
5
+ def valid_user_reset_password_email(root_url, user)
6
+ @root_url = root_url
7
+ @user = user
8
+ @reset_path = RESET_PATH
9
+
10
+ @token_reset_path = token_reset_path
11
+
12
+ mail(to: user.email, subject: 'Password Reset')
13
+ end
14
+
15
+ def invalid_user_reset_password_email(root_url, email)
16
+ @root_url = root_url
17
+ @email = email
18
+ @signup_path = SIGNUP_PATH
19
+
20
+ mail(to: email, subject: 'Password Reset Error')
21
+ end
22
+
23
+ private
24
+
25
+ def token_reset_path
26
+ @reset_path.sub(/:token/, @user.reset_password_token)
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
5
+ </head>
6
+ <body>
7
+ <h1>Hi <%= @email %></h1>
8
+
9
+ <p>
10
+ Someone has requested a link to change your password, but we
11
+ do not have account associated with this email.
12
+ </p>
13
+
14
+ <p>To create a new account, please click the link below.</p>
15
+
16
+ <p><a href="<%= "#{@root_url}#{@signup_path}" %>">Create new account.</a></p>
17
+
18
+ <p>If you didn't request this password reset, please ignore this email.</p>
19
+ </body>
20
+ </html>
@@ -0,0 +1,13 @@
1
+ Hi <%= @email %>
2
+ =================================
3
+
4
+ <p>
5
+ Someone has requested a link to change your password, but we
6
+ do not have account associated with this email.
7
+ </p>
8
+
9
+ To create a new account, please go to the url below to create a new account.
10
+
11
+ <%= "#{@root_url}#{@signup_path}" %>
12
+
13
+ If you didn't request this password reset, please ignore this email.
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
5
+ </head>
6
+ <body>
7
+ <h1>Hi <%= @user.username %></h1>
8
+
9
+ <p>Someone has requested a link to change your password. You can do this through the link below.</p>
10
+
11
+ <p><a href="<%= "#{@root_url}#{@token_reset_path}" %>">Change my password</a></p>
12
+
13
+ <p>If you didn't request this, please ignore this email.</p>
14
+ <p>Your password won't change until you access the link above and create a new one.</p>
15
+ </body>
16
+ </html>
@@ -0,0 +1,9 @@
1
+ Hi <%= @user.username %>
2
+ ==================================
3
+
4
+ Someone has requested to change your password. You can do this through the link below.
5
+
6
+ Please go to <%= "#{@root_url}#{@token_reset_path}" %> to change your password.
7
+
8
+ If you didn't request this, please ignore this email.
9
+ Your password won't change until you access the link above and create a new one.
data/config.ru ADDED
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize!
7
+ run Combustion::Application
@@ -0,0 +1,37 @@
1
+ # TODO: Update so path (/api) isn't fixed
2
+ module TokenAuthenticateMe
3
+ module Generators
4
+ class ControllersGenerator < ::Rails::Generators::NamedBase
5
+ source_root File.expand_path('../templates', __FILE__)
6
+ check_class_collision suffix: 'Controller'
7
+
8
+ def create_sessions_controller
9
+ template 'sessions.rb', 'app/controllers/api/sessions_controller.rb'
10
+
11
+ # Inject /api/sesssion route into routes file
12
+ route <<-ROUTE
13
+ namespace :api do
14
+ resource :session, only: [:create, :show, :destroy]
15
+ end
16
+ ROUTE
17
+ end
18
+
19
+ def create_password_reset_controller # rubocop:disable Metrics/MethodLength
20
+ template 'password_reset.rb', 'app/controllers/api/password_resets_controller.rb'
21
+
22
+ # Inject /api/password_resets route into routes file
23
+ route <<-ROUTE
24
+ namespace :api do
25
+ resources(
26
+ :password_resets,
27
+ only: [:create, :update],
28
+ constraints: {
29
+ id: TokenAuthenticateMe::UUID_REGEX
30
+ }
31
+ )
32
+ end
33
+ ROUTE
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ require 'token_authenticate_me/controllers/password_resetable'
2
+
3
+ class PasswordResetsController < ApplicationController
4
+ include TokenAuthenticateMe::Controllers::PasswordResetable
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'token_authenticate_me/controllers/sessionable'
2
+
3
+ class SessionsController < ApplicationController
4
+ include TokenAuthenticateMe::Controllers::Sessionable
5
+
6
+ end
@@ -0,0 +1,12 @@
1
+ module TokenAuthenticateMe
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ def run_generators
5
+ params = @_initializer[0]
6
+
7
+ invoke 'token_authenticate_me:models', params
8
+ invoke 'token_authenticate_me:controllers', params
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,55 @@
1
+ module TokenAuthenticateMe
2
+ module Generators
3
+ class ModelsGenerator < ::Rails::Generators::NamedBase
4
+ include Rails::Generators::Migration
5
+
6
+ source_root File.expand_path('../templates', __FILE__)
7
+ check_class_collision suffix: ''
8
+
9
+ def self.next_migration_number(dirname)
10
+ next_migration_number = current_migration_number(dirname) + 1
11
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
12
+ end
13
+
14
+ def create_authentication_model_file
15
+ template 'authentication_model.rb', File.join('app/models', 'user.rb')
16
+ end
17
+
18
+ def create_authentication_migration_file
19
+ # When the switch is made to allow resource names to be specified, could use something like:
20
+ # migration_file_name = "#{next_migration_number}_#{plural_name}.rb"
21
+ # migration_template(
22
+ # 'authentication_migration.rb',
23
+ # File.join('db/migrations', migration_file_name)
24
+ # )
25
+ migration_template(
26
+ 'authentication_migration.rb',
27
+ File.join('db/migrations', "create_users.rb")
28
+ )
29
+ end
30
+
31
+ def create_session_model_file
32
+ template 'session_model.rb', File.join('app/models', 'session.rb')
33
+ end
34
+
35
+ def create_session_migration_file
36
+ # When the switch is made to allow resource names to be specified, could use something like:
37
+ # migration_file_name = "#{next_migration_number}_#{singular_name}_sessions.rb"
38
+ # migration_template(
39
+ # 'authentication_migration.rb',
40
+ # File.join('db/migrations', migration_file_name)
41
+ # )
42
+ migration_template(
43
+ 'session_migration.rb',
44
+ File.join('db/migrations', "create_sessions.rb")
45
+ )
46
+ end
47
+
48
+ private
49
+
50
+ def next_migration_number
51
+ self.class.next_migration_number('db/migrations')
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,20 @@
1
+ class UserMigration < ActiveRecord::Migration
2
+ def up
3
+ create_table :users do |t|
4
+ t.string :username, null: false
5
+ t.string :email, null: false
6
+ t.string :password_digest, null: false
7
+ t.string :reset_password_token
8
+ t.datetime :reset_password_token_exp
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :users, :email, unique: true
13
+ add_index :users, :username, unique: true
14
+ add_index :users, :reset_password_token, unique: true
15
+ end
16
+
17
+ def down
18
+ drop_table :users
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ require 'token_authenticate_me/models/authenticatable'
2
+
3
+ class User < ActiveRecord::Base
4
+ include TokenAuthenticateMe::Models::Authenticatable
5
+
6
+ end
@@ -0,0 +1,17 @@
1
+ class SessionMigration < ActiveRecord::Migration
2
+ def up
3
+ create_table :sessions do |t|
4
+ t.string :key, null: false
5
+ t.datetime :expiration
6
+ t.integer :user_id
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :key, unique: true
12
+ end
13
+
14
+ def down
15
+ drop_table :sessions
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ require 'token_authenticate_me/models/sessionable'
2
+
3
+ class Session < ActiveRecord::Base
4
+ include TokenAuthenticateMe::Models::Sessionable
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'token_authenticate_me/engine'
2
+ require 'token_authenticate_me/version'
3
+
4
+ module TokenAuthenticateMe
5
+ UUID_REGEX = /([a-f0-9]){32}/
6
+ end
@@ -0,0 +1,90 @@
1
+ require 'active_support/concern'
2
+
3
+ require 'token_authenticate_me/controllers/token_authenticateable'
4
+
5
+ module TokenAuthenticateMe
6
+ module Controllers
7
+ module PasswordResetable
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include TokenAuthenticateMe::Controllers::TokenAuthenticateable
12
+
13
+ skip_before_action :authenticate, only: [:create, :update]
14
+ before_action :validate_reset_token, only: [:update]
15
+
16
+ # Send reset token to user with e-mail address
17
+ def create
18
+ @user = User.find_by_email(params[:email])
19
+
20
+ if @user
21
+ send_valid_reset_email(@user)
22
+ else
23
+ send_invalid_reset_email(params[:email])
24
+ end
25
+
26
+ render status: 204, nothing: true
27
+ end
28
+
29
+ # Allow user to reset password when the token is valid
30
+ # and not expired
31
+ def update
32
+ @user.update!(
33
+ password: params[:password],
34
+ password_confirmation: params[:password_confirmation],
35
+ reset_password_token: nil,
36
+ reset_password_token_exp: nil
37
+ )
38
+
39
+ render status: 204, nothing: true
40
+ rescue ActiveRecord::RecordInvalid => e
41
+ handle_errors(e)
42
+ end
43
+
44
+ private
45
+
46
+ def send_valid_reset_email(user)
47
+ user.create_reset_token!
48
+
49
+ TokenAuthenticateMeMailer.valid_user_reset_password_email(
50
+ request.base_url,
51
+ user
52
+ ).deliver
53
+ end
54
+
55
+ def send_invalid_reset_email(email)
56
+ TokenAuthenticateMeMailer.invalid_user_reset_password_email(
57
+ request.base_url,
58
+ email
59
+ ).deliver
60
+ end
61
+
62
+ def session_params
63
+ params.permit(:password, :password_confirmation)
64
+ end
65
+
66
+ def render_errors(errors, status = 422)
67
+ render(json: { errors: errors }, status: status)
68
+ end
69
+
70
+ def handle_errors(e)
71
+ render_errors(e.record.errors.messages)
72
+ end
73
+
74
+ def validate_reset_token
75
+ valid_reset_token? || render_not_found
76
+ end
77
+
78
+ def render_not_found
79
+ render status: 404, nothing: true
80
+ end
81
+
82
+ def valid_reset_token?
83
+ @user = User.find_by_reset_password_token(params[:id])
84
+
85
+ @user && @user.reset_password_token_exp > DateTime.now
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end