janus 0.5.0 → 0.6.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 (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