janus 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/README.rdoc +65 -29
  2. data/lib/janus.rb +1 -13
  3. data/lib/janus/config.rb +9 -5
  4. data/lib/janus/controllers/confirmations_controller.rb +2 -0
  5. data/lib/janus/controllers/helpers.rb +17 -0
  6. data/lib/janus/controllers/internal_helpers.rb +12 -0
  7. data/lib/janus/controllers/passwords_controller.rb +3 -0
  8. data/lib/janus/controllers/sessions_controller.rb +58 -21
  9. data/lib/janus/manager.rb +7 -2
  10. data/lib/janus/models/database_authenticatable.rb +22 -6
  11. data/lib/janus/rails.rb +17 -0
  12. data/lib/janus/routes.rb +1 -2
  13. data/lib/janus/sinatra.rb +51 -0
  14. metadata +63 -169
  15. data/test/functional/home_controller_test.rb +0 -8
  16. data/test/functional/janus/mailer_test.rb +0 -14
  17. data/test/functional/janus/manager_test.rb +0 -94
  18. data/test/functional/users/confirmations_controller_test.rb +0 -59
  19. data/test/functional/users/passwords_controller_test.rb +0 -101
  20. data/test/functional/users/registrations_controller_test.rb +0 -112
  21. data/test/functional/users/sessions_controller_test.rb +0 -100
  22. data/test/functional/users_controller_test.rb +0 -22
  23. data/test/integration/users/rememberable_test.rb +0 -32
  24. data/test/integration/users/remote_test.rb +0 -72
  25. data/test/integration/users/sessions_test.rb +0 -18
  26. data/test/integration/users/trackable_test.rb +0 -22
  27. data/test/rails_app/app/controllers/application_controller.rb +0 -9
  28. data/test/rails_app/app/controllers/blogs_controller.rb +0 -6
  29. data/test/rails_app/app/controllers/home_controller.rb +0 -4
  30. data/test/rails_app/app/controllers/users/confirmations_controller.rb +0 -3
  31. data/test/rails_app/app/controllers/users/passwords_controller.rb +0 -3
  32. data/test/rails_app/app/controllers/users/registrations_controller.rb +0 -7
  33. data/test/rails_app/app/controllers/users/sessions_controller.rb +0 -11
  34. data/test/rails_app/app/controllers/users_controller.rb +0 -9
  35. data/test/rails_app/app/helpers/application_helper.rb +0 -2
  36. data/test/rails_app/app/mailers/janus_mailer.rb +0 -2
  37. data/test/rails_app/app/models/remote_token.rb +0 -6
  38. data/test/rails_app/app/models/user.rb +0 -8
  39. data/test/rails_app/config/application.rb +0 -42
  40. data/test/rails_app/config/boot.rb +0 -6
  41. data/test/rails_app/config/environment.rb +0 -5
  42. data/test/rails_app/config/environments/development.rb +0 -26
  43. data/test/rails_app/config/environments/production.rb +0 -49
  44. data/test/rails_app/config/environments/test.rb +0 -36
  45. data/test/rails_app/config/initializers/janus.rb +0 -11
  46. data/test/rails_app/config/initializers/secret_token.rb +0 -7
  47. data/test/rails_app/config/initializers/session_store.rb +0 -8
  48. data/test/rails_app/config/routes.rb +0 -12
  49. data/test/rails_app/db/migrate/20110323153820_create_users.rb +0 -34
  50. data/test/rails_app/db/migrate/20110331153546_create_remote_tokens.rb +0 -15
  51. data/test/rails_app/db/schema.rb +0 -45
  52. data/test/rails_app/db/seeds.rb +0 -7
  53. data/test/test_helper.rb +0 -103
  54. data/test/unit/confirmable_test.rb +0 -36
  55. data/test/unit/janus_test.rb +0 -27
  56. data/test/unit/rememberable_test.rb +0 -50
  57. data/test/unit/remote_authenticatable_test.rb +0 -37
  58. data/test/unit/remote_token_test.rb +0 -9
  59. data/test/unit/reset_password_test.rb +0 -45
  60. data/test/unit/trackable_test.rb +0 -21
  61. data/test/unit/user_test.rb +0 -60
@@ -2,48 +2,86 @@
2
2
 
3
3
  Janus is an authentication engine for Ruby on Rails 3 and is an alternative
4
4
  to the Warden + Devise combo, without the Rack middleware. The whole project
5
- is inspired by the Warden and Devise API but shall eventually be quite
6
- different since everything happens within ActionDispatch and not at the Rack
7
- level.
8
-
9
- The main difference for now is the cross domain authentication --which allows
10
- a user to single sign in and out across top level domains-- which required
11
- a finer grained control over setting and unsetting a user than Warden provides.
12
- Janus uses +login+ and +logout+ to actually sign the user in and out, while
13
- +set_user+ and +unset_user+ will manually set the session, without dispatching
14
- the +after_login+ and +after_logout+ hooks.
5
+ is inspired by the Warden and Devise API (in order to somehow compatible)
6
+ but is quite different since everything happens within ActionDispatch and not
7
+ at the Rack level.
8
+
9
+ This Rails instead of Rack difference, allows to actually have the main logic
10
+ within plain Rails controllers. For instance the database authentication is
11
+ called from SessionsController, and it's not just another strategy operating
12
+ at the Rack level within Warden (which requires to check that it's being
13
+ called from the correct URL). There ain't no factory to add strategy modules
14
+ to your models too. You must manually include the necessary ones.
15
+
16
+ Another difference is that you must actually create the necessary controllers,
17
+ models, mailers and views within your project (extending the default ones).
18
+ You will eventually need to have those controllers anyway, and having those
19
+ from the beginning allows to skip some configuration. The burdensome of
20
+ manually creating all those classes should eventually be leveraged by using
21
+ some rails generators.
22
+
23
+ Janus also provides a finer control over setting and unsetting a user than
24
+ Warden provides. Janus uses +login+ and +logout+ to actually sign the user
25
+ in and out, just like Warden, but actually uses +set_user+ and +unset_user+
26
+ to manually set the session, without dispatching the +after_login+ and
27
+ +after_logout+ hooks, of course.
28
+
29
+ Emails are also sent from the controllers, not from the models, because I
30
+ believe this is actually the job of controllers, not models.
15
31
 
16
32
  == Features
17
33
 
34
+ The main feature is of course having a framework for authenticating users
35
+ painleslly. Yet a very usefull feature is the cross domain authentication
36
+ --which allows a user to single sign in and out across top level domains.
37
+
38
+ How is the cross domain authentication strategy usefull? Let's imagine you
39
+ host of blogging website where users may host their blogs on other domains.
40
+ Since you don't rely on subdomains, it will be a pain to keep the user
41
+ authentified, because you can't rely on '.' domain cookie trick.
42
+
43
+ This is where RemoteAuthentication comes in, and allows you to painlessly
44
+ keep your users connected across the main website and their blogs. This
45
+ without actually tracking connections since this strategy takes advantage
46
+ of the +set_user+ and +unset_user+ methods, because they're not really
47
+ signing in, they just stay authentified across domains.
48
+
49
+ So, Janus provides the following API:
50
+
51
+ - full authentication system with strategies and hooks
52
+ - scoped authentications with parallel authentication (like `users`, `admin_users`, etc.)
53
+ - database authentication with password encryption (bcrypt) and validation
54
+ - remote authentication for cross domain single sign in / sign out
55
+ - abstract controllers for session management, registration, email confirmation and password reset
56
+ - route generation for the above controllers
57
+
58
+ And for the strategies and hooks:
59
+
18
60
  - DatabaseAuthenticatable
19
61
  - RemoteAuthenticatable
20
62
  - Confirmable
21
63
  - Rememberable
22
- - Trackable
23
-
24
- Note: login through Janus::Manager#set_user won't track the user.
25
-
26
- - authentication system with strategies and hooks
27
- - scoped authentications with parallel authentication
28
- - database authentication with password encryption, validation and remember me strategy
29
- - remote authentication for cross domain sign in / sign out
30
- - controllers: sessions, registrations, confirmations, passwords and their routes
31
- - route generation for above controllers
32
- - trackable hook
64
+ - Trackable (note that login through Janus::Manager#set_user won't track the user).
33
65
 
34
66
  == TODO
35
67
 
36
- - generators (janus:install, janus)
37
- - rename RemoteAuthenticatable to something like CrossDomainAuthenticatable(?)
68
+ - Simple configuration to use scrypt instead of bcrypt for password encryption.
69
+ - Reconfirmable when email changes.
70
+ - TokenAuthenticatable.
71
+ - Remember me on remote authenticated domains.
72
+ - Differenciate mailers per resource, by looking for Users::Mailer class.
73
+ - Generators: `janus:install` and `janus <scope>`.
74
+ - Integrate OmniAuth, or shall we let the user do it himself?
75
+ - Providing an OAuth 2.0 server whould be cool.
38
76
 
39
77
  == Install
40
78
 
41
- There is no automated way to install Janus yet, since generators are missing.
42
- Please remember that Janus is only compatible with Rails 3.
79
+ There is no automated way to install Janus yet, because generators are missing.
80
+ Also remember that Janus is only compatible with Rails 3+.
43
81
 
44
82
  First add the gem to your Gemfile:
45
83
 
46
- $ gem 'janus', :git => git://github.com/ysbaddaden/janus.git
84
+ $ gem 'janus'
47
85
 
48
86
  Configure your user models by including all or a selection of the Janus::Models
49
87
  modules:
@@ -60,7 +98,6 @@ modules:
60
98
  class Admin < ActiveRecord::Base
61
99
  include Janus::Models::Base
62
100
  include Janus::Models::DatabaseAuthenticatable
63
- include Janus::Models::RemoteAuthenticatable
64
101
  end
65
102
 
66
103
  Configure your routes:
@@ -68,7 +105,6 @@ Configure your routes:
68
105
  Name::Application.routes.map do
69
106
  janus :users, :session => true, :registration => true, :password => true, :confirmation => true
70
107
  janus :admins, :session => true
71
-
72
108
  root :to => "home#index"
73
109
  end
74
110
 
@@ -86,7 +122,7 @@ Create the required controllers:
86
122
  respond_to :html
87
123
  end
88
124
 
89
- class Users::ConfirmationssController < Janus::ConfirmationssController
125
+ class Users::ConfirmationsController < Janus::ConfirmationsController
90
126
  respond_to :html
91
127
  end
92
128
 
@@ -1,8 +1,8 @@
1
+ require 'active_support/core_ext/class'
1
2
  require 'janus/config'
2
3
  require 'janus/hooks'
3
4
  require 'janus/strategies'
4
5
  require 'janus/manager'
5
- require 'janus/routes'
6
6
 
7
7
  autoload :JanusHelper, 'janus/helper'
8
8
 
@@ -16,18 +16,6 @@ module Janus
16
16
  end
17
17
  end
18
18
 
19
- autoload :Mailer, 'janus/mailer'
20
- autoload :TestHelper, 'janus/test_helper'
21
-
22
- autoload :Helpers, 'janus/controllers/helpers'
23
- autoload :UrlHelpers, 'janus/controllers/url_helpers'
24
- autoload :InternalHelpers, 'janus/controllers/internal_helpers'
25
-
26
- autoload :SessionsController, 'janus/controllers/sessions_controller'
27
- autoload :RegistrationsController, 'janus/controllers/registrations_controller'
28
- autoload :ConfirmationsController, 'janus/controllers/confirmations_controller'
29
- autoload :PasswordsController, 'janus/controllers/passwords_controller'
30
-
31
19
  module Models
32
20
  autoload :Base, 'janus/models/base'
33
21
  autoload :DatabaseAuthenticatable, 'janus/models/database_authenticatable'
@@ -5,17 +5,21 @@ module Janus
5
5
  mattr_accessor :contact_email
6
6
 
7
7
  # DatabaseAuthenticatable
8
- mattr_accessor :authentication_keys, :stretches, :pepper
9
- self.authentication_keys = [:email]
8
+ mattr_accessor :authentication_keys, :encryptor, :stretches, :pepper, :scrypt_options
9
+ self.authentication_keys = [ :email ]
10
+ self.encryptor = :bcrypt
10
11
  self.stretches = 10
12
+ self.pepper = nil
13
+ self.scrypt_options = { :max_time => 0.25 }
11
14
 
12
15
  # Confirmable
13
- mattr_accessor :confirmation_key
16
+ mattr_accessor :confirmation_key #,reconfirmable
14
17
  self.confirmation_key = :confirm_token
18
+ # self.reconfirmable = true
15
19
 
16
20
  # Rememberable
17
- mattr_accessor :remember_for, :extend_remember_period, :remember_across_browsers
18
- self.remember_for = 2.weeks
21
+ mattr_accessor :remember_for, :extend_remember_period #, :remember_across_browsers
22
+ self.remember_for = 1.year
19
23
  self.extend_remember_period = false
20
24
  # self.remember_across_browsers = false
21
25
 
@@ -1,3 +1,5 @@
1
+ # This controller is responsible for confirming any user email. It's also
2
+ # responsible for resending the confirmation email on demand by the user.
1
3
  class Janus::ConfirmationsController < ApplicationController
2
4
  include Janus::InternalHelpers
3
5
 
@@ -13,19 +13,36 @@ module Janus
13
13
  end
14
14
  end
15
15
 
16
+ # Returns the current instance of Janus::Manager.
16
17
  def janus
17
18
  @janus ||= Janus::Manager.new(request, cookies)
18
19
  end
19
20
 
21
+ # Signs the current user out (from all scopes at once) in case of a CSRF attack.
22
+ # See ActionController::RequestForgeryProtection for documentation.
20
23
  def handle_unverified_requests
21
24
  janus.logout
25
+ super
22
26
  end
23
27
 
28
+ # Returns true if a scope user is currently authenticated.
24
29
  def signed_in?(scope)
25
30
  janus.authenticate?(scope)
26
31
  end
27
32
 
28
33
  module ClassMethods
34
+ # Aliases some Janus methods for convenience. For instance calling
35
+ # `janus(:user, :admin)` will generate the following methods:
36
+ #
37
+ # authenticate_user! # => janus.authenticate!(:user)
38
+ # current_user # => janus.authenticate(:user)
39
+ # user_signed_in? # => janus.authenticate?(:user)
40
+ # user_session # => janus.sesssion(:user)
41
+ #
42
+ # authenticate_admin! # => janus.authenticate!(:admin)
43
+ # current_admin # => janus.authenticate(:admin)
44
+ # admin_signed_in? # => janus.authenticate?(:admin)
45
+ # admin_session # => janus.sesssion(:admin)
29
46
  def janus(*scopes)
30
47
  scopes.each do |scope|
31
48
  class_eval <<-EOV
@@ -1,4 +1,7 @@
1
1
  module Janus
2
+ # A collection of abstraction helper methods used in Janus controllers and views.
3
+ # This should be of no particular outside of abstract controllers for Janus that
4
+ # must be working for all scopes at once.
2
5
  module InternalHelpers
3
6
  extend ActiveSupport::Concern
4
7
 
@@ -6,26 +9,35 @@ module Janus
6
9
  helper_method :janus_scope, :resource, :resource_class, :resource_name
7
10
  end
8
11
 
12
+ # Abstract method for the authenticate_scope! before filter, with scope
13
+ # as detected by janus_scope.
9
14
  def authenticate!
10
15
  send("authenticate_#{janus_scope}!")
11
16
  end
12
17
 
18
+ # Detects the scope from the controller name.
13
19
  def janus_scope
14
20
  @janus_scope ||= self.class.name.split('::', 2).first.underscore.singularize
15
21
  end
16
22
 
23
+ # Returns the `@user` instance variable (or `@admin` or whatever),
24
+ # as detected by janus_scope.
17
25
  def resource
18
26
  instance_variable_get(:"@#{janus_scope}")
19
27
  end
20
28
 
29
+ # Sets the `@user` instance variable (or `@admin` or whatever),
30
+ # as detected by janus_scope.
21
31
  def resource=(value)
22
32
  instance_variable_set(:"@#{janus_scope}", value)
23
33
  end
24
34
 
35
+ # Returns the `User` class (or `Admin` or whatever) as detected by janus_scope.
25
36
  def resource_class
26
37
  @resource_class ||= janus_scope.camelize.constantize
27
38
  end
28
39
 
40
+ # Alias for janus_scope.
29
41
  def resource_name
30
42
  janus_scope
31
43
  end
@@ -1,3 +1,6 @@
1
+ # This controller is responsible for resetting a lost password. It sends an
2
+ # email with an unique token, on demand by the user. Then allows the user to
3
+ # change its password (as long as the token is valid).
1
4
  class Janus::PasswordsController < ApplicationController
2
5
  include Janus::InternalHelpers
3
6
 
@@ -1,5 +1,13 @@
1
1
  require 'addressable/uri'
2
2
 
3
+ # This controller is responsible for creating and destroying
4
+ # authenticated user sessions.
5
+ #
6
+ # The creation uses the DatabaseAuthenticatable strategy, while the destruction
7
+ # simply destroys any session, whatever strategy it was created with. Janus
8
+ # hooks will be called, of course, allowing to destroy any Rememberable cookies
9
+ # for instance, as well as any user defined behavior.
10
+ #
3
11
  class Janus::SessionsController < ApplicationController
4
12
  include Janus::InternalHelpers
5
13
  # include Janus::UrlHelpers
@@ -34,7 +42,6 @@ class Janus::SessionsController < ApplicationController
34
42
  self.resource ||= resource_class.new(params[resource_name])
35
43
  resource.clean_up_passwords
36
44
  resource.errors.add(:base, :not_found)
37
-
38
45
  render "new", :status => :unauthorized
39
46
  end
40
47
  format.any { head :unauthorized }
@@ -51,41 +58,71 @@ class Janus::SessionsController < ApplicationController
51
58
  end
52
59
  end
53
60
 
61
+ # An overridable method that returns the default path to return the just
62
+ # signed in user to. Defaults to return the user object, which will be
63
+ # interpreted by rails as `user_path(user)`.
54
64
  def after_sign_in_url(user)
55
65
  user
56
66
  end
57
67
 
68
+ # An overridable method that returns the default path to return the just
69
+ # signed out user to. Defaults to `root_url`.
58
70
  def after_sign_out_url(scope)
59
71
  root_url
60
72
  end
61
73
 
62
- # Returns true if remote host is known and redirect with an auth_token should
63
- # be allowed or not. It must be overwritten by child class since it always
64
- # returns true by default.
74
+ # Returns true if host is known and that we allow to redirect the user
75
+ # with an auth_token.
76
+ #
77
+ # Warning: must be overwritten by child classes because it always
78
+ # returns false by default!
65
79
  def valid_remote_host?(host)
66
- true
80
+ false
81
+ end
82
+
83
+ # Returns an Array of URL that we shouldn't automatically return to. It
84
+ # actually returns URL to prevent infinite loops. We must for instance
85
+ # never return to new_sesssion_path.
86
+ #
87
+ # If you ever needd to override this method, don't forget to call `super`.
88
+ # For instance:
89
+ #
90
+ # def never_return_to(scope)
91
+ # super + [ my_peculiar_path, another_path ]
92
+ # end
93
+ #
94
+ def never_return_to(scope)
95
+ scope = Janus.scope_for(scope)
96
+ [
97
+ new_session_path(scope),
98
+ new_password_path(scope),
99
+ edit_password_path(scope)
100
+ ]
67
101
  end
68
102
 
69
- # Either redirects the user to after_sign_in_url or to
70
- # <tt>params[:return_to]</tt>. If return_to is an absolute URL, and not just
71
- # a path, valid_remote_host? will be invoked to check if we should redirect
72
- # to this URL or not --which is moslty of use for RemoteAuthenticatable to
73
- # securize auth tokens from unknown domains.
103
+ # Either redirects the user to after_sign_in_url or to <tt>params[:return_to]</tt>.
104
+ #
105
+ # If <tt>params[:return_to] is an absolute URL, and not just a path,
106
+ # valid_remote_host? will be invoked to check wether we should redirect
107
+ # to this URL or not, in order to secure auth tokens for
108
+ # RemoteAuthenticatable to leak into the wild.
74
109
  def redirect_after_sign_in(user)
75
110
  unless params[:return_to].blank?
76
111
  return_to = Addressable::URI.parse(params[:return_to])
77
-
78
- if return_to.host.nil? || return_to.host == request.host
79
- redirect_to params[:return_to]
80
- return
81
- elsif valid_remote_host?(return_to.host)
82
- if user.class.include?(Janus::Models::RemoteAuthenticatable)
83
- query = return_to.query_values || {}
84
- return_to.query_values = query.merge(user.class.remote_authentication_key => user.generate_remote_token!)
112
+
113
+ unless never_return_to(user).include?(return_to.path)
114
+ if return_to.host.nil? || return_to.host == request.host
115
+ redirect_to params[:return_to]
116
+ return
117
+ elsif valid_remote_host?(return_to.host)
118
+ if user.class.include?(Janus::Models::RemoteAuthenticatable)
119
+ query = return_to.query_values || {}
120
+ return_to.query_values = query.merge(user.class.remote_authentication_key => user.generate_remote_token!)
121
+ end
122
+
123
+ redirect_to return_to.to_s
124
+ return
85
125
  end
86
-
87
- redirect_to return_to.to_s
88
- return
89
126
  end
90
127
  end
91
128
 
@@ -34,7 +34,7 @@ module Janus
34
34
 
35
35
  # Logs a user in.
36
36
  #
37
- # FIXME: what should happen when a user signs in but a user is already signed in?!
37
+ # FIXME: what should happen when a user signs in but a user is already signed in for the same scope?!
38
38
  def login(user, options = {})
39
39
  options[:scope] ||= Janus.scope_for(user)
40
40
  set_user(user, options)
@@ -76,8 +76,13 @@ module Janus
76
76
 
77
77
  if authenticated?(scope)
78
78
  if @users[scope].nil?
79
+ begin
79
80
  @users[scope] = session(scope)[:user_class].find(session(scope)[:user_id])
80
- Janus::Manager.run_callbacks(:fetch, @users[scope], self, :scope => scope)
81
+ rescue ActiveRecord::RecordNotFound
82
+ unset_user(scope)
83
+ else
84
+ Janus::Manager.run_callbacks(:fetch, @users[scope], self, :scope => scope)
85
+ end
81
86
  end
82
87
 
83
88
  @users[scope]
@@ -1,4 +1,5 @@
1
1
  require 'bcrypt'
2
+ require 'scrypt'
2
3
 
3
4
  module Janus
4
5
  module Models
@@ -24,13 +25,13 @@ module Janus
24
25
 
25
26
  included do
26
27
  attr_protected :encrypted_password, :reset_password_token, :reset_password_sent_at
27
- attr_reader :password
28
- attr_accessor :current_password
28
+ attr_reader :password
29
+ attr_accessor :current_password
29
30
 
30
31
  validates :password, :presence => true, :confirmation => true, :if => :password_required?
31
32
  validate :validate_current_password, :on => :update, :if => :current_password
32
33
 
33
- janus_config(:authentication_keys, :stretches, :pepper)
34
+ janus_config(:authentication_keys, :encryptor, :stretches, :pepper, :scrypt_options)
34
35
  end
35
36
 
36
37
  def password=(password)
@@ -38,13 +39,28 @@ module Janus
38
39
  self.encrypted_password = digest_password(@password) unless @password.blank?
39
40
  end
40
41
 
41
- # Checks if a given password matches this user password.
42
+ # Checks if a given password matches this user's password.
42
43
  def valid_password?(password)
43
- ::BCrypt::Password.new(encrypted_password) == "#{password}#{self.class.pepper}"
44
+ case self.class.encryptor
45
+ when :bcrypt
46
+ ::BCrypt::Password.new(encrypted_password) == salted_password(password)
47
+ when :scrypt
48
+ ::SCrypt::Password.new(encrypted_password) == salted_password(password)
49
+ end
44
50
  end
45
51
 
52
+ # Digests a password using either bcrypt or scrypt (as configured by `config.encryptor`).
46
53
  def digest_password(password)
47
- ::BCrypt::Password.create("#{password}#{self.class.pepper}", :cost => self.class.stretches).to_s
54
+ case self.class.encryptor
55
+ when :bcrypt
56
+ ::BCrypt::Password.create(salted_password(password), :cost => self.class.stretches).to_s
57
+ when :scrypt
58
+ ::SCrypt::Password.create(salted_password(password), self.class.scrypt_options).to_s
59
+ end
60
+ end
61
+
62
+ def salted_password(password)
63
+ "#{password}#{self.class.pepper}"
48
64
  end
49
65
 
50
66
  def clean_up_passwords