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
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
require 'token_authenticate_me/controllers/token_authenticateable'
|
4
|
+
|
5
|
+
module TokenAuthenticateMe
|
6
|
+
module Controllers
|
7
|
+
module Sessionable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
include TokenAuthenticateMe::Controllers::TokenAuthenticateable
|
11
|
+
|
12
|
+
included do
|
13
|
+
skip_before_action :authenticate, only: [:create]
|
14
|
+
after_action :cleanup_sessions, only: [:destroy]
|
15
|
+
|
16
|
+
def create
|
17
|
+
resource = User.where('username=? OR email=?', params[:username], params[:username]).first
|
18
|
+
if resource && resource.authenticate(params[:password])
|
19
|
+
@session = Session.create(user_id: resource.id)
|
20
|
+
render json: serialize_session(@session), status: 201
|
21
|
+
else
|
22
|
+
render json: { message: 'Bad credentials' }, status: 401
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def show
|
27
|
+
@session = authenticate_token
|
28
|
+
render json: serialize_session(@session)
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy
|
32
|
+
authenticate_token.destroy
|
33
|
+
|
34
|
+
render status: 204, nothing: true
|
35
|
+
rescue
|
36
|
+
render_unauthorized
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def serialize_session(session)
|
42
|
+
{
|
43
|
+
session: {
|
44
|
+
key: session.key,
|
45
|
+
user_id: session.user_id,
|
46
|
+
expiration: session.expiration
|
47
|
+
}
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def session_params
|
52
|
+
params.permit(:username, :email, :password)
|
53
|
+
end
|
54
|
+
|
55
|
+
def cleanup_sessions
|
56
|
+
ApiSession.where('expiration < ?', DateTime.now).delete_all
|
57
|
+
rescue
|
58
|
+
Rails.logger.warn 'Error cleaning up old authentication sessions'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module TokenAuthenticateMe
|
4
|
+
module Controllers
|
5
|
+
module TokenAuthenticateable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
before_action :authenticate
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def authenticate
|
15
|
+
authenticate_token || render_unauthorized
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_user
|
19
|
+
if authenticate_token
|
20
|
+
@current_user ||= User.find_by_id(authenticate_token.user_id)
|
21
|
+
else
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def authenticate_token
|
27
|
+
@session ||= authenticate_with_http_token do |token, _options|
|
28
|
+
session = Session.find_by_key(token)
|
29
|
+
if session && session.expiration > DateTime.now
|
30
|
+
session
|
31
|
+
else
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_unauthorized
|
38
|
+
headers['WWW-Authenticate'] = 'Token realm="Application"'
|
39
|
+
render json: 'Bad credentials', status: 401
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module TokenAuthenticateMe
|
4
|
+
module Models
|
5
|
+
module Authenticatable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
has_secure_password
|
10
|
+
|
11
|
+
validates(
|
12
|
+
:email,
|
13
|
+
presence: true,
|
14
|
+
uniqueness: { case_sensitive: false }
|
15
|
+
)
|
16
|
+
|
17
|
+
validates(
|
18
|
+
:username,
|
19
|
+
format: { with: /\A[a-zA-Z0-9]+\Z/ },
|
20
|
+
presence: true,
|
21
|
+
uniqueness: { case_sensitive: false }
|
22
|
+
)
|
23
|
+
|
24
|
+
def create_reset_token!
|
25
|
+
# rubocop:disable Lint/Loop
|
26
|
+
begin
|
27
|
+
self.reset_password_token = SecureRandom.hex
|
28
|
+
end while self.class.exists?(reset_password_token: reset_password_token)
|
29
|
+
|
30
|
+
self.reset_password_token_exp = password_expiration_hours.hours.from_now
|
31
|
+
self.save!
|
32
|
+
end
|
33
|
+
|
34
|
+
def password_expiration_hours
|
35
|
+
8
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module TokenAuthenticateMe
|
4
|
+
module Models
|
5
|
+
module Sessionable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
before_create :generate_unique_key
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def generate_unique_key
|
14
|
+
begin
|
15
|
+
self.key = SecureRandom.hex
|
16
|
+
end while self.class.exists?(key: key) # rubocop:disable Lint/Loop
|
17
|
+
|
18
|
+
self.expiration = expiration_hours.hours.from_now
|
19
|
+
end
|
20
|
+
|
21
|
+
def expiration_hours
|
22
|
+
24
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Password Reset API' do
|
4
|
+
it 'resets the users password when called with the correct token' do
|
5
|
+
user = create_user
|
6
|
+
|
7
|
+
user.create_reset_token!
|
8
|
+
encrypted_pw = user.password_digest
|
9
|
+
reset_token = user.reset_password_token.to_s
|
10
|
+
|
11
|
+
put '/password_resets/' + reset_token + '/',
|
12
|
+
password: 'test', password_confirmation: 'test'
|
13
|
+
|
14
|
+
expect(last_response.status).to eq(204)
|
15
|
+
expect(User.find(user.id).password_digest).not_to eq(encrypted_pw)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'does not allow replay attacks' do
|
19
|
+
user = create_user
|
20
|
+
|
21
|
+
user.create_reset_token!
|
22
|
+
encrypted_pw = user.password_digest
|
23
|
+
reset_token = user.reset_password_token.to_s
|
24
|
+
|
25
|
+
put '/password_resets/' + reset_token + '/',
|
26
|
+
password: 'test', password_confirmation: 'test'
|
27
|
+
|
28
|
+
expect(last_response.status).to eq(204)
|
29
|
+
expect(User.find(user.id).password_digest).not_to eq(encrypted_pw)
|
30
|
+
|
31
|
+
put '/password_resets/' + reset_token + '/',
|
32
|
+
password: 'test', password_confirmation: 'test'
|
33
|
+
expect(last_response.status).to eq(404)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'fails to reset the users password when the confirmation does not match' do
|
37
|
+
user = create_user
|
38
|
+
|
39
|
+
user.create_reset_token!
|
40
|
+
encrypted_pw = user.password_digest
|
41
|
+
reset_token = user.reset_password_token.to_s
|
42
|
+
|
43
|
+
put '/password_resets/' + reset_token + '/',
|
44
|
+
password: 'test', password_confirmation: 'test_ops'
|
45
|
+
|
46
|
+
expect(last_response.status).to eq(422)
|
47
|
+
expect(User.find(user.id).password_digest).to eq(encrypted_pw)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'raises a routing error when called with an empty token' do
|
51
|
+
user = create_user
|
52
|
+
|
53
|
+
user.create_reset_token!
|
54
|
+
encrypted_pw = user.password_digest
|
55
|
+
|
56
|
+
expect do
|
57
|
+
put '/password_resets//', password: 'test', password_confirmation: 'test'
|
58
|
+
end.to raise_error(ActionController::RoutingError)
|
59
|
+
expect(User.find(user.id).password_digest).to eq(encrypted_pw)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns a 404 when reset is requested with a bad token' do
|
63
|
+
user = create_user
|
64
|
+
|
65
|
+
user.create_reset_token!
|
66
|
+
|
67
|
+
put '/password_resets/' + SecureRandom.hex.to_s + '/',
|
68
|
+
password: 'test', password_confirmation: 'test'
|
69
|
+
|
70
|
+
expect(last_response.status).to eq(404)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'returns a 204 when a password reset is requested with a valid e-mail' do
|
74
|
+
user = create_user
|
75
|
+
|
76
|
+
post '/password_resets/', email: user.email
|
77
|
+
|
78
|
+
expect(last_response.status).to eq(204)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns a 204 when a password reset is requested with a invalid e-mail' do
|
82
|
+
user = create_user # rubocop:disable Lint/UselessAssignment
|
83
|
+
|
84
|
+
post '/password_resets/', email: 'foo@bar.com'
|
85
|
+
|
86
|
+
expect(last_response.status).to eq(204)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'sends a valid e-mail when a password reset is requested with a valid e-mail' do
|
90
|
+
user = create_user
|
91
|
+
|
92
|
+
post '/password_resets/', email: user.email
|
93
|
+
|
94
|
+
mail = ActionMailer::Base.deliveries.last
|
95
|
+
|
96
|
+
expect(mail['to'].to_s).to eq(user.email)
|
97
|
+
expect(mail['subject'].to_s).to eq('Password Reset')
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'sends a invalid e-mail when a password reset is requested with a invalid e-mail' do
|
101
|
+
user = create_user # rubocop:disable Lint/UselessAssignment
|
102
|
+
email = 'foo@bar.com'
|
103
|
+
|
104
|
+
post '/password_resets/', email: email
|
105
|
+
|
106
|
+
mail = ActionMailer::Base.deliveries.last
|
107
|
+
|
108
|
+
expect(mail['to'].to_s).to eq(email)
|
109
|
+
expect(mail['subject'].to_s).to eq('Password Reset Error')
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Session API' do
|
4
|
+
it 'creates a new session when authenticating with a username and password' do
|
5
|
+
password = 'text'
|
6
|
+
user = create_user(password: password)
|
7
|
+
|
8
|
+
post '/session/',
|
9
|
+
username: user.username, password: password
|
10
|
+
|
11
|
+
expect(last_response.status).to eq(201)
|
12
|
+
json = JSON.parse(last_response.body)
|
13
|
+
|
14
|
+
expect(json['session']).not_to be_nil
|
15
|
+
expect(json['session']['key']).not_to be_nil
|
16
|
+
expect(json['session']['expiration']).not_to be_nil
|
17
|
+
expect(user.id).to eq(json['session']['user_id'])
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'creates a new session when authenticating with a email and password' do
|
21
|
+
password = 'text'
|
22
|
+
user = create_user(password: password)
|
23
|
+
|
24
|
+
post '/session/',
|
25
|
+
username: user.email, password: password
|
26
|
+
|
27
|
+
expect(last_response.status).to eq(201)
|
28
|
+
json = JSON.parse(last_response.body)
|
29
|
+
|
30
|
+
expect(json['session']).not_to be_nil
|
31
|
+
expect(json['session']['key']).not_to be_nil
|
32
|
+
expect(json['session']['expiration']).not_to be_nil
|
33
|
+
expect(user.id).to eq(json['session']['user_id'])
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'fails to create a new session when authenticating with an invalid password' do
|
37
|
+
password = 'text'
|
38
|
+
user = create_user(password: password)
|
39
|
+
|
40
|
+
post '/session/',
|
41
|
+
username: user.email, password: 'not_test'
|
42
|
+
|
43
|
+
expect(last_response.status).to eq(401)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'fetches an existing session when authenticated' do
|
47
|
+
password = 'text'
|
48
|
+
user = create_user(password: password)
|
49
|
+
|
50
|
+
post '/session/',
|
51
|
+
username: user.email, password: password
|
52
|
+
expect(last_response.status).to eq(201)
|
53
|
+
json = JSON.parse(last_response.body)
|
54
|
+
|
55
|
+
header 'Authorization', 'Token token=' + json['session']['key']
|
56
|
+
get '/session/'
|
57
|
+
expect(last_response.status).to eq(200)
|
58
|
+
|
59
|
+
json = JSON.parse(last_response.body)
|
60
|
+
|
61
|
+
expect(json['session']).not_to be_nil
|
62
|
+
expect(json['session']['key']).not_to be_nil
|
63
|
+
expect(json['session']['expiration']).not_to be_nil
|
64
|
+
expect(user.id).to eq(json['session']['user_id'])
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'fetching an expired session fails' do
|
68
|
+
user = create_user
|
69
|
+
session = Session.create!(user_id: user.id)
|
70
|
+
session.update!(expiration: 5.minutes.ago)
|
71
|
+
|
72
|
+
header 'Authorization', 'Token token=' + session.key
|
73
|
+
get '/session/'
|
74
|
+
expect(last_response.status).to eq(401)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'destroying an existing session succeeds' do
|
78
|
+
user = create_user
|
79
|
+
session = Session.create!(user_id: user.id)
|
80
|
+
|
81
|
+
header 'Authorization', 'Token token=' + session.key
|
82
|
+
delete '/session/'
|
83
|
+
expect(last_response.status).to eq(204)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'destroying an expired session fails' do
|
87
|
+
user = create_user
|
88
|
+
session = Session.create!(user_id: user.id)
|
89
|
+
session.update!(expiration: 5.minutes.ago)
|
90
|
+
|
91
|
+
header 'Authorization', 'Token token=' + session.key
|
92
|
+
delete '/session/'
|
93
|
+
expect(last_response.status).to eq(401)
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Users API' do
|
4
|
+
it 'creates a new user when unauthenticated' do
|
5
|
+
username = 'test'
|
6
|
+
password = 'test'
|
7
|
+
email = 'test'
|
8
|
+
|
9
|
+
post '/users/',
|
10
|
+
user: {
|
11
|
+
username: username,
|
12
|
+
password: password,
|
13
|
+
password_confirmation: password,
|
14
|
+
email: email
|
15
|
+
}
|
16
|
+
|
17
|
+
expect(last_response.status).to eq(201)
|
18
|
+
json = JSON.parse(last_response.body)
|
19
|
+
|
20
|
+
expect(json['user']).not_to be_nil
|
21
|
+
expect(json['user']['username']).to eq(username)
|
22
|
+
expect(json['user']['email']).to eq(email)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'fails to create a new user when the password confirmation does not match' do
|
26
|
+
username = 'test'
|
27
|
+
password = 'test'
|
28
|
+
email = 'test'
|
29
|
+
|
30
|
+
post '/users/',
|
31
|
+
user: {
|
32
|
+
username: username,
|
33
|
+
password: password,
|
34
|
+
password_confirmation: 'invalid',
|
35
|
+
email: email
|
36
|
+
}
|
37
|
+
|
38
|
+
expect(last_response.status).to eq(422)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'succeeds to list users when authenticated' do
|
42
|
+
user = create_user
|
43
|
+
session = Session.create!(user_id: user.id)
|
44
|
+
|
45
|
+
header 'Authorization', 'Token token=' + session.key
|
46
|
+
get '/users/'
|
47
|
+
|
48
|
+
expect(last_response.status).to eq(200)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'fails to list users without being authenticated' do
|
52
|
+
get '/users/'
|
53
|
+
|
54
|
+
expect(last_response.status).to eq(401)
|
55
|
+
end
|
56
|
+
end
|