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.
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