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
@@ -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,5 @@
1
+ module TokenAuthenticateMe
2
+ class Engine < Rails::Engine
3
+ # engine_name :token_authenticate_me
4
+ end
5
+ 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,3 @@
1
+ module TokenAuthenticateMe
2
+ VERSION = '0.2.0'
3
+ 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