oath 1.1.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 (94) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +165 -0
  7. data/LICENSE.txt +22 -0
  8. data/NEWS.rdoc +118 -0
  9. data/README.md +384 -0
  10. data/Rakefile +6 -0
  11. data/lib/oath.rb +132 -0
  12. data/lib/oath/back_door.rb +53 -0
  13. data/lib/oath/configuration.rb +149 -0
  14. data/lib/oath/constraints/signed_in.rb +14 -0
  15. data/lib/oath/constraints/signed_out.rb +14 -0
  16. data/lib/oath/controller_helpers.rb +161 -0
  17. data/lib/oath/failure_app.rb +48 -0
  18. data/lib/oath/field_map.rb +56 -0
  19. data/lib/oath/param_transformer.rb +38 -0
  20. data/lib/oath/railtie.rb +11 -0
  21. data/lib/oath/services.rb +5 -0
  22. data/lib/oath/services/authentication.rb +40 -0
  23. data/lib/oath/services/password_reset.rb +27 -0
  24. data/lib/oath/services/sign_in.rb +25 -0
  25. data/lib/oath/services/sign_out.rb +24 -0
  26. data/lib/oath/services/sign_up.rb +42 -0
  27. data/lib/oath/strategies/password_strategy.rb +42 -0
  28. data/lib/oath/test/controller_helpers.rb +43 -0
  29. data/lib/oath/test/helpers.rb +24 -0
  30. data/lib/oath/version.rb +4 -0
  31. data/lib/oath/warden_setup.rb +47 -0
  32. data/oath.gemspec +30 -0
  33. data/spec/features/user/user_signs_in_spec.rb +14 -0
  34. data/spec/features/user/user_signs_in_through_back_door_spec.rb +11 -0
  35. data/spec/features/user/user_tries_to_access_constrained_routes_spec.rb +18 -0
  36. data/spec/features/user/user_tries_to_access_http_auth_page_spec.rb +9 -0
  37. data/spec/features/visitor/visitor_fails_to_sign_up_spec.rb +10 -0
  38. data/spec/features/visitor/visitor_is_unauthorized_spec.rb +8 -0
  39. data/spec/features/visitor/visitor_signs_in_via_invalid_form_spec.rb +11 -0
  40. data/spec/features/visitor/visitor_signs_up_spec.rb +40 -0
  41. data/spec/features/visitor/visitor_tries_to_access_constrained_routes_spec.rb +14 -0
  42. data/spec/features/visitor/visitor_uses_remember_token_spec.rb +13 -0
  43. data/spec/oath/configuration_spec.rb +11 -0
  44. data/spec/oath/controller_helpers_spec.rb +180 -0
  45. data/spec/oath/field_map_spec.rb +19 -0
  46. data/spec/oath/services/authentication_spec.rb +25 -0
  47. data/spec/oath/services/password_reset_spec.rb +24 -0
  48. data/spec/oath/services/sign_in_spec.rb +13 -0
  49. data/spec/oath/services/sign_out_spec.rb +13 -0
  50. data/spec/oath/services/sign_up_spec.rb +49 -0
  51. data/spec/oath/strategies/password_strategy_spec.rb +23 -0
  52. data/spec/oath/test_controller_helpers_spec.rb +63 -0
  53. data/spec/oath/test_helpers_spec.rb +97 -0
  54. data/spec/oath_spec.rb +27 -0
  55. data/spec/rails_app/Rakefile +7 -0
  56. data/spec/rails_app/app/assets/images/rails.png +0 -0
  57. data/spec/rails_app/app/assets/javascripts/application.js +13 -0
  58. data/spec/rails_app/app/assets/stylesheets/application.css +13 -0
  59. data/spec/rails_app/app/controllers/application_controller.rb +4 -0
  60. data/spec/rails_app/app/controllers/basic_auth_controller.rb +7 -0
  61. data/spec/rails_app/app/controllers/constrained_to_users_controller.rb +5 -0
  62. data/spec/rails_app/app/controllers/constrained_to_visitors_controller.rb +5 -0
  63. data/spec/rails_app/app/controllers/failures_controller.rb +5 -0
  64. data/spec/rails_app/app/controllers/invalid_sessions_controller.rb +2 -0
  65. data/spec/rails_app/app/controllers/posts_controller.rb +6 -0
  66. data/spec/rails_app/app/controllers/sessions_controller.rb +26 -0
  67. data/spec/rails_app/app/controllers/users_controller.rb +23 -0
  68. data/spec/rails_app/app/helpers/application_helper.rb +2 -0
  69. data/spec/rails_app/app/models/user.rb +10 -0
  70. data/spec/rails_app/app/views/invalid_sessions/new.html.erb +4 -0
  71. data/spec/rails_app/app/views/layouts/application.html.erb +18 -0
  72. data/spec/rails_app/app/views/posts/index.html.erb +1 -0
  73. data/spec/rails_app/app/views/sessions/new.html.erb +5 -0
  74. data/spec/rails_app/app/views/users/new.html.erb +5 -0
  75. data/spec/rails_app/config.ru +4 -0
  76. data/spec/rails_app/config/application.rb +58 -0
  77. data/spec/rails_app/config/boot.rb +6 -0
  78. data/spec/rails_app/config/database.yml +25 -0
  79. data/spec/rails_app/config/environment.rb +5 -0
  80. data/spec/rails_app/config/environments/development.rb +29 -0
  81. data/spec/rails_app/config/environments/production.rb +54 -0
  82. data/spec/rails_app/config/environments/test.rb +29 -0
  83. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  84. data/spec/rails_app/config/initializers/inflections.rb +15 -0
  85. data/spec/rails_app/config/initializers/secret_token.rb +7 -0
  86. data/spec/rails_app/config/routes.rb +24 -0
  87. data/spec/rails_app/db/seeds.rb +7 -0
  88. data/spec/rails_app/public/404.html +26 -0
  89. data/spec/rails_app/public/422.html +26 -0
  90. data/spec/rails_app/public/500.html +25 -0
  91. data/spec/rails_app/public/favicon.ico +0 -0
  92. data/spec/rails_app/script/rails +6 -0
  93. data/spec/spec_helper.rb +37 -0
  94. metadata +325 -0
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,132 @@
1
+ require 'warden'
2
+ require "oath/version"
3
+ require "oath/configuration"
4
+ require "oath/services"
5
+ require "oath/controller_helpers"
6
+ require "oath/railtie"
7
+ require "oath/failure_app"
8
+ require "oath/back_door"
9
+ require "oath/warden_setup"
10
+ require "oath/field_map"
11
+ require "oath/param_transformer"
12
+ require "oath/strategies/password_strategy"
13
+ require "active_support/core_ext/module/attribute_accessors"
14
+
15
+ # Oath is an authentication toolkit designed to allow developers to create their own
16
+ # authentication solutions. If you're interested in a default implementation try
17
+ # {http://github.com/halogenandtoast/oath-generators Oath Generators}
18
+ # @since 0.0.15
19
+ module Oath
20
+ mattr_accessor :warden_config
21
+ mattr_accessor :config
22
+
23
+ module Test
24
+ autoload :Helpers, "oath/test/helpers"
25
+ autoload :ControllerHelpers, "oath/test/controller_helpers"
26
+ end
27
+
28
+ # initialize Oath. Sets up warden and the default configuration.
29
+ #
30
+ # @note This is used in {Oath::Railtie} in order to bootstrap Oath
31
+ # @param warden_config [Warden::Config] the configuration from warden
32
+ # @see Oath::Railtie
33
+ # @see Oath::Configuration
34
+ def self.initialize warden_config
35
+ setup_config
36
+ setup_warden_config(warden_config)
37
+ end
38
+
39
+ # compares the token (undigested password) to a digested password
40
+ #
41
+ # @param digest [String] A digested password
42
+ # @param token [String] An undigested password
43
+ # @see Oath::Configuration#default_token_comparison
44
+ # @return [Boolean] whether the token and digest match
45
+ def self.compare_token(digest, token)
46
+ config.token_comparison.call(digest, token)
47
+ end
48
+
49
+ # hashes a token
50
+ #
51
+ # @param token [String] the password in undigested form
52
+ # @see Oath::Configuration#default_hashing_method
53
+ # @return [String] a digest of the token
54
+ def self.hash_token(token)
55
+ config.hashing_method.call(token)
56
+ end
57
+
58
+ # performs transformations on params for signing up and
59
+ # signing in
60
+ #
61
+ # @param params [Hash] hash of parameters to transform
62
+ # @see Oath::Configuration#param_transofmrations
63
+ # @return [Hash] hash with transformed parameters
64
+ def self.transform_params(params)
65
+ ParamTransformer.new(params, config.param_transformations).to_h
66
+ end
67
+
68
+ # the user class
69
+ #
70
+ # @see Oath::Configuration#setup_class_defaults
71
+ # @deprecated Use Oath.config.user_class instead
72
+ # @return [Class] the User class
73
+ def self.user_class
74
+ warn "#{Kernel.caller.first}: [DEPRECATION] " +
75
+ 'Accessing the user class through the Oath module is deprecated. Use Oath.config.user_class instead.'
76
+ config.user_class
77
+ end
78
+
79
+ # finds a user based on their credentials
80
+ #
81
+ # @param params [Hash] a hash of user parameters
82
+ # @param field_map [FieldMap] a field map in order to allow multiple lookup fields
83
+ # @see Oath::Configuration#default_find_method
84
+ # @return [User] if user is found
85
+ # @return [nil] if no user is found
86
+ def self.lookup(params, field_map)
87
+ if params.present?
88
+ fields = FieldMap.new(params, field_map).to_fields
89
+ self.config.find_method.call(fields)
90
+ end
91
+ end
92
+
93
+ # Puts oath into test mode. This will disable hashing passwords
94
+ # @note You must call this if you want to use oath in your tests
95
+ def self.test_mode!
96
+ Warden.test_mode!
97
+ self.config ||= Oath::Configuration.new
98
+ config.hashing_method = ->(password) { password }
99
+ config.token_comparison = ->(digest, undigested_password) do
100
+ digest == undigested_password
101
+ end
102
+ end
103
+
104
+ # Configures oath
105
+ #
106
+ # @yield [configuration] Yield the current configuration
107
+ # @example A custom configuration
108
+ # Oath.configure do |config|
109
+ # config.user_lookup_field = :username
110
+ # config.user_token_store_field = :hashed_password
111
+ # end
112
+ def self.configure(&block)
113
+ self.config ||= Oath::Configuration.new
114
+ yield self.config
115
+ end
116
+
117
+ # Resets oath in between tests.
118
+ # @note You must call this between tests
119
+ def self.test_reset!
120
+ Warden.test_reset!
121
+ end
122
+
123
+ private
124
+
125
+ def self.setup_config
126
+ self.config ||= Oath::Configuration.new
127
+ end
128
+
129
+ def self.setup_warden_config(warden_config)
130
+ self.warden_config = WardenSetup.new(warden_config).call
131
+ end
132
+ end
@@ -0,0 +1,53 @@
1
+ module Oath
2
+ # Middleware used in tests to allow users to be signed in directly, without
3
+ # having to load and submit the sign in form. The user should be provided by
4
+ # using the key :as in a hash passed to the path.
5
+ #
6
+ # @note This should only be used for testing purposes
7
+ # @since 0.0.15
8
+ # @example Using the backdoor in an rspec feature spec
9
+ # feature "User dashboard" do
10
+ # scenario "user visits dashboard" do
11
+ # user = create(:user)
12
+ # visit dashboard_path(as: user)
13
+ # expect(page).to have_css("#dashboard")
14
+ # end
15
+ # end
16
+ class BackDoor
17
+ # Create the a new BackDoor middleware for test purposes
18
+ # @return [BackDoor]
19
+ def initialize(app, &block)
20
+ @app = app
21
+
22
+ if block
23
+ @sign_in_block = block
24
+ end
25
+ end
26
+
27
+ # Execute the BackDoor middleware signing in the user specified with :as
28
+ def call(env)
29
+ sign_in_through_the_back_door(env)
30
+ @app.call(env)
31
+ end
32
+
33
+ private
34
+
35
+ def sign_in_through_the_back_door(env)
36
+ params = Rack::Utils.parse_query(env['QUERY_STRING'])
37
+ user_id = params['as']
38
+
39
+ if user_id.present?
40
+ user = find_user(user_id)
41
+ env["warden"].set_user(user)
42
+ end
43
+ end
44
+
45
+ def find_user(user_id)
46
+ if @sign_in_block
47
+ @sign_in_block.call(user_id)
48
+ else
49
+ Oath.config.user_class.find(user_id)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,149 @@
1
+ module Oath
2
+ # Configuration options for Oath
3
+ # @since 0.0.15
4
+ class Configuration
5
+ attr_accessor :user_token_field, :user_token_store_field
6
+ attr_accessor :hashing_method, :token_comparison, :user_lookup_field
7
+ attr_accessor :sign_in_notice
8
+ attr_accessor :sign_in_service, :sign_up_service, :sign_out_service
9
+ attr_accessor :authentication_service, :password_reset_service
10
+ attr_accessor :failure_app
11
+ attr_accessor :creation_method, :find_method
12
+ attr_accessor :no_login_handler, :no_login_redirect
13
+ attr_accessor :authentication_strategy
14
+ attr_accessor :warden_serialize_into_session, :warden_serialize_from_session
15
+ attr_accessor :param_transformations
16
+
17
+ attr_writer :user_class
18
+
19
+ def initialize
20
+ setup_class_defaults
21
+ setup_token_hashing
22
+ setup_notices
23
+ setup_services
24
+ setup_warden
25
+ setup_param_transformations
26
+ end
27
+
28
+ # Default creation method. Can be overriden via {Oath.configure}
29
+ #
30
+ # @see #creation_method=
31
+ def default_creation_method
32
+ ->(params) do
33
+ updated_params = Oath.transform_params(params)
34
+ Oath.config.user_class.create(updated_params)
35
+ end
36
+ end
37
+
38
+ # Default hashing method. Can be overriden via {Oath.configure}
39
+ #
40
+ # @see #hashing_method=
41
+ def default_hashing_method
42
+ ->(token) do
43
+ if token.present?
44
+ BCrypt::Password.create(token)
45
+ else
46
+ token
47
+ end
48
+ end
49
+ end
50
+
51
+ # Default find method. Can be overriden via {Oath.configure}
52
+ #
53
+ # @see #find_method=
54
+ # @see Oath.config.user_class
55
+ def default_find_method
56
+ ->(params) do
57
+ updated_params = Oath.transform_params(params)
58
+ Oath.config.user_class.find_by(updated_params)
59
+ end
60
+ end
61
+
62
+ # Default token comparison method. Can be overriden via {Oath.configure}
63
+ #
64
+ # @see #token_comparison=
65
+ def default_token_comparison
66
+ ->(digest, undigested_token) do
67
+ BCrypt::Password.new(digest) == undigested_token
68
+ end
69
+ end
70
+
71
+ # Default handler when user is not logged in. Can be overriden via {Oath.configure}
72
+ #
73
+ # @see #no_login_handler=
74
+ # @see #sign_in_notice
75
+ # @see #no_login_redirect
76
+ def default_no_login_handler
77
+ ->(controller) do
78
+ notice = Oath.config.sign_in_notice
79
+
80
+ if notice.respond_to?(:call)
81
+ controller.flash.notice = notice.call
82
+ else
83
+ warn "[DEPRECATION] `Oath.config.sign_in_notice` should be a lambda instead of a string"
84
+ controller.flash.notice = notice
85
+ end
86
+
87
+ controller.redirect_to Oath.config.no_login_redirect
88
+ end
89
+ end
90
+
91
+ # User class. Can be overriden via {Oath.configure}
92
+ #
93
+ # @see #user_class=
94
+ def user_class
95
+ @user_class.constantize
96
+ end
97
+
98
+ private
99
+
100
+ def setup_token_hashing
101
+ @hashing_method = default_hashing_method
102
+ @token_comparison = default_token_comparison
103
+ end
104
+
105
+ def setup_notices
106
+ @sign_in_notice = -> { 'You must be signed in' }
107
+ end
108
+
109
+ def setup_class_defaults
110
+ @user_class = 'User'
111
+ @user_token_field = :password
112
+ @user_token_store_field = :password_digest
113
+ @user_lookup_field = :email
114
+ @creation_method = default_creation_method
115
+ @find_method = default_find_method
116
+ @no_login_redirect = { controller: '/sessions', action: 'new' }
117
+ @no_login_handler = default_no_login_handler
118
+ end
119
+
120
+ def setup_services
121
+ @authentication_service = Oath::Services::Authentication
122
+ @sign_in_service = Oath::Services::SignIn
123
+ @sign_up_service = Oath::Services::SignUp
124
+ @sign_out_service = Oath::Services::SignOut
125
+ @password_reset_service = Oath::Services::PasswordReset
126
+ end
127
+
128
+ def setup_warden
129
+ setup_warden_requirements
130
+ setup_warden_serialization
131
+ end
132
+
133
+ def setup_warden_requirements
134
+ @failure_app = Oath::FailureApp
135
+ @authentication_strategy = Oath::Strategies::PasswordStrategy
136
+ end
137
+
138
+ def setup_warden_serialization
139
+ @warden_serialize_into_session = -> (user) { user.id }
140
+ @warden_serialize_from_session = -> (id) { Oath.config.user_class.find_by(id: id) }
141
+ end
142
+
143
+ def setup_param_transformations
144
+ @param_transformations = {
145
+ "email" => ->(value) { value.downcase }
146
+ }
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,14 @@
1
+ module Oath
2
+ module Constraints
3
+ # Rails route constraint for signed in users
4
+ class SignedIn
5
+ # Checks to see if the constraint is matched by having a user signed in
6
+ #
7
+ # @param request [Rack::Request] A rack request
8
+ def matches?(request)
9
+ warden = request.env["warden"]
10
+ warden && warden.authenticated?
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Oath
2
+ module Constraints
3
+ # Rails route constraint for signed out users
4
+ class SignedOut
5
+ # Checks to see if the constraint is matched by not having a user signed in
6
+ #
7
+ # @param request [Rack::Request] A rack request
8
+ def matches?(request)
9
+ warden = request.env["warden"]
10
+ warden && warden.unauthenticated?
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,161 @@
1
+ require 'bcrypt'
2
+ require 'active_support/concern'
3
+
4
+ module Oath
5
+ # Mixin to be included in Rails controllers.
6
+ # @since 0.0.15
7
+ module ControllerHelpers
8
+ extend ActiveSupport::Concern
9
+ included do
10
+ if respond_to?(:helper_method)
11
+ helper_method :current_user, :signed_in?
12
+ end
13
+ end
14
+
15
+ # Sign in a user
16
+ #
17
+ # @note Uses the {Oath::Services::SignIn} service to create a session
18
+ #
19
+ # @param user [User] the user object to sign in
20
+ # @yield Yields to the block if the user is successfully signed in
21
+ # @return [Object] returns the value from calling perform on the {Oath::Services::SignIn} service
22
+ def sign_in user
23
+ Oath.config.sign_in_service.new(user, warden).perform.tap do |status|
24
+ if status && block_given?
25
+ yield
26
+ end
27
+ end
28
+ end
29
+
30
+ # Sign out the current session
31
+ #
32
+ # @note Uses the {Oath::Services::SignOut} service to destroy the session
33
+ #
34
+ # @return [Object] returns the value from calling perform on the {Oath::Services::SignOut} service
35
+ def sign_out
36
+ Oath.config.sign_out_service.new(warden).perform
37
+ end
38
+
39
+ # Sign up a user
40
+ #
41
+ # @note Uses the {Oath::Services::SignUp} service to create a user
42
+ #
43
+ # @param user_params [Hash] params containing lookup and token fields
44
+ # @yield Yields to the block if the user is signed up successfully
45
+ # @return [Object] returns the value from calling perform on the {Oath::Services::SignUp} service
46
+ def sign_up user_params
47
+ Oath.config.sign_up_service.new(user_params).perform.tap do |status|
48
+ if status && block_given?
49
+ yield
50
+ end
51
+ end
52
+ end
53
+
54
+ # Authenticates a session.
55
+ #
56
+ # @note Uses the {Oath::Services::Authentication} service to verify the user's details
57
+ #
58
+ # @param session_params [Hash] params containing lookup and token fields
59
+ # @param field_map [Hash] Field map used for allowing users to sign in with multiple fields e.g. email and username
60
+ # @return [User] if authentication succeeded
61
+ # @return [nil] if authentication failed
62
+ # @example Basic usage
63
+ # class SessionsController < ApplicationController
64
+ # def create
65
+ # user = authenticate_session(session_params)
66
+ #
67
+ # if sign_in(user)
68
+ # redirect_to(root_path)
69
+ # else
70
+ # render :new
71
+ # end
72
+ # end
73
+ #
74
+ # private
75
+ #
76
+ # def session_params
77
+ # params.require(:session).permit(:email, :password)
78
+ # end
79
+ #
80
+ # end
81
+ # @example Using the field map to authenticate using multiple lookup fields
82
+ # class SessionsController < ApplicationController
83
+ # def create
84
+ # user = authenticate_session(session_params, email_or_username: [:email, :username])
85
+ #
86
+ # if sign_in(user)
87
+ # redirect_to(root_path)
88
+ # else
89
+ # render :new
90
+ # end
91
+ # end
92
+ #
93
+ # private
94
+ #
95
+ # def session_params
96
+ # params.require(:session).permit(:email_or_username, :password)
97
+ # end
98
+ #
99
+ # end
100
+
101
+ def authenticate_session session_params, field_map = nil
102
+ token_field = Oath.config.user_token_field
103
+ params_hash = Oath.transform_params(session_params).symbolize_keys
104
+ password = params_hash.fetch(token_field)
105
+ user = Oath.lookup(params_hash.except(token_field), field_map)
106
+ authenticate(user, password)
107
+ end
108
+
109
+ # Authenticates a user given a password
110
+ #
111
+ # @note Uses the {Oath::Services::Authentication} service to verify the user's credentials
112
+ #
113
+ # @param user [User] the user
114
+ # @param password [String] the password
115
+ # @return [User] if authentication succeeded
116
+ # @return [nil] if authentication failed
117
+ def authenticate user, password
118
+ Oath.config.authentication_service.new(user, password).perform
119
+ end
120
+
121
+ # Resets a user's password
122
+ #
123
+ # @note Uses the {Oath::Services::PasswordReset} service to change a user's password
124
+ #
125
+ # @param user [User] the user
126
+ # @param password [String] the password
127
+ def reset_password user, password
128
+ Oath.config.password_reset_service.new(user, password).perform
129
+ end
130
+
131
+ # @api private
132
+ def warden
133
+ request.env['warden']
134
+ end
135
+
136
+ # helper_method that returns the current user
137
+ #
138
+ # @return [User] if user is signed in
139
+ # @return [nil] if user is not signed in
140
+ def current_user
141
+ @current_user ||= warden.user
142
+ end
143
+
144
+ # helper_method that checks if there is a user signed in
145
+ #
146
+ # @return [User] if user is signed in
147
+ # @return [nil] if user is not signed in
148
+ def signed_in?
149
+ warden.user
150
+ end
151
+
152
+ # before_action that determines what to do when the user is not signed in
153
+ #
154
+ # @note Uses the no login handler
155
+ def require_login
156
+ unless signed_in?
157
+ Oath.config.no_login_handler.call(self)
158
+ end
159
+ end
160
+ end
161
+ end