token_authenticate_me 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +8 -0
- data/Gemfile +7 -0
- data/LICENSE +20 -0
- data/README.md +47 -0
- data/Rakefile +17 -0
- data/app/mailers/token_authenticate_me_mailer.rb +28 -0
- data/app/views/token_authenticate_me_mailer/invalid_user_reset_password_email.html.erb +20 -0
- data/app/views/token_authenticate_me_mailer/invalid_user_reset_password_email.text.erb +13 -0
- data/app/views/token_authenticate_me_mailer/valid_user_reset_password_email.html.erb +16 -0
- data/app/views/token_authenticate_me_mailer/valid_user_reset_password_email.text.erb +9 -0
- data/config.ru +7 -0
- data/lib/generators/token_authenticate_me/controllers/controllers_generator.rb +37 -0
- data/lib/generators/token_authenticate_me/controllers/templates/password_reset.rb +6 -0
- data/lib/generators/token_authenticate_me/controllers/templates/sessions.rb +6 -0
- data/lib/generators/token_authenticate_me/install/install_generator.rb +12 -0
- data/lib/generators/token_authenticate_me/models/models_generator.rb +55 -0
- data/lib/generators/token_authenticate_me/models/templates/authentication_migration.rb +20 -0
- data/lib/generators/token_authenticate_me/models/templates/authentication_model.rb +6 -0
- data/lib/generators/token_authenticate_me/models/templates/session_migration.rb +17 -0
- data/lib/generators/token_authenticate_me/models/templates/session_model.rb +6 -0
- data/lib/token_authenticate_me.rb +6 -0
- data/lib/token_authenticate_me/controllers/password_resetable.rb +90 -0
- data/lib/token_authenticate_me/controllers/sessionable.rb +63 -0
- data/lib/token_authenticate_me/controllers/token_authenticateable.rb +43 -0
- data/lib/token_authenticate_me/engine.rb +5 -0
- data/lib/token_authenticate_me/models/authenticatable.rb +40 -0
- data/lib/token_authenticate_me/models/sessionable.rb +27 -0
- data/lib/token_authenticate_me/version.rb +3 -0
- data/spec/acceptance/password_reset_api_spec.rb +111 -0
- data/spec/acceptance/session_api_spec.rb +95 -0
- data/spec/acceptance/users_api_spec.rb +56 -0
- data/spec/internal/app/controllers/application_controller.rb +5 -0
- data/spec/internal/app/controllers/password_resets_controller.rb +5 -0
- data/spec/internal/app/controllers/sessions_controller.rb +5 -0
- data/spec/internal/app/controllers/users_controller.rb +7 -0
- data/spec/internal/app/models/session.rb +5 -0
- data/spec/internal/app/models/user.rb +5 -0
- data/spec/internal/app/policies/user_policy.rb +25 -0
- data/spec/internal/app/serializers/user_serializer.rb +3 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +13 -0
- data/spec/internal/db/fixtures/users.rb +11 -0
- data/spec/internal/db/schema.rb +19 -0
- data/spec/spec_helper.rb +38 -0
- data/token_authenticate_me.gemspec +32 -0
- 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
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
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,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,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,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,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
|