token_authenticate_me 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|