devise_cloudfuji_authenticatable 1.0.4

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 (62) hide show
  1. data/.gitignore +4 -0
  2. data/.project +12 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +10 -0
  6. data/README.md +121 -0
  7. data/Rakefile +1 -0
  8. data/app/controllers/devise/cas_sessions_controller.rb +101 -0
  9. data/app/views/devise/cas_sessions/new.html.erb +1 -0
  10. data/app/views/devise/cas_sessions/unregistered.html.erb +150 -0
  11. data/app/views/devise/cas_sessions/unregistered.html.erb.old +2 -0
  12. data/devise_cloudfuji_authenticatable.gemspec +36 -0
  13. data/lib/devise_cas_authenticatable.rb +135 -0
  14. data/lib/devise_cas_authenticatable/exceptions.rb +10 -0
  15. data/lib/devise_cas_authenticatable/missing_session_helpers.rb +9 -0
  16. data/lib/devise_cas_authenticatable/model.rb +56 -0
  17. data/lib/devise_cas_authenticatable/routes.rb +37 -0
  18. data/lib/devise_cas_authenticatable/schema.rb +13 -0
  19. data/lib/devise_cas_authenticatable/single_sign_out.rb +22 -0
  20. data/lib/devise_cas_authenticatable/single_sign_out/session_store/active_record.rb +12 -0
  21. data/lib/devise_cas_authenticatable/single_sign_out/session_store/redis.rb +27 -0
  22. data/lib/devise_cas_authenticatable/single_sign_out/strategies.rb +58 -0
  23. data/lib/devise_cas_authenticatable/single_sign_out/strategies/base.rb +11 -0
  24. data/lib/devise_cas_authenticatable/single_sign_out/strategies/rails_cache.rb +31 -0
  25. data/lib/devise_cas_authenticatable/strategy.rb +56 -0
  26. data/lib/devise_cloudfuji_authenticatable.rb +8 -0
  27. data/lib/devise_cloudfuji_authenticatable/version.rb +3 -0
  28. data/rails/init.rb +1 -0
  29. data/spec/devise_cas_authenticatable/model_spec.rb +39 -0
  30. data/spec/routes_spec.rb +38 -0
  31. data/spec/scenario/.gitignore +4 -0
  32. data/spec/scenario/app/controllers/application_controller.rb +3 -0
  33. data/spec/scenario/app/controllers/home_controller.rb +7 -0
  34. data/spec/scenario/app/models/user.rb +3 -0
  35. data/spec/scenario/app/views/layouts/application.html.erb +17 -0
  36. data/spec/scenario/config.ru +4 -0
  37. data/spec/scenario/config/application.rb +38 -0
  38. data/spec/scenario/config/boot.rb +13 -0
  39. data/spec/scenario/config/castronaut.yml +32 -0
  40. data/spec/scenario/config/database.yml +22 -0
  41. data/spec/scenario/config/environment.rb +5 -0
  42. data/spec/scenario/config/environments/development.rb +25 -0
  43. data/spec/scenario/config/environments/production.rb +49 -0
  44. data/spec/scenario/config/environments/test.rb +35 -0
  45. data/spec/scenario/config/initializers/backtrace_silencers.rb +7 -0
  46. data/spec/scenario/config/initializers/castronaut.rb +1 -0
  47. data/spec/scenario/config/initializers/devise.rb +3 -0
  48. data/spec/scenario/config/initializers/inflections.rb +10 -0
  49. data/spec/scenario/config/initializers/mime_types.rb +5 -0
  50. data/spec/scenario/config/initializers/secret_token.rb +7 -0
  51. data/spec/scenario/config/initializers/session_store.rb +8 -0
  52. data/spec/scenario/config/locales/en.yml +5 -0
  53. data/spec/scenario/config/routes.rb +8 -0
  54. data/spec/scenario/config/rubycas-server.yml +13 -0
  55. data/spec/scenario/db/migrate/20100401102949_create_tables.rb +15 -0
  56. data/spec/scenario/db/migrate/20111002012903_add_sessions_table.rb +16 -0
  57. data/spec/scenario/db/schema.rb +25 -0
  58. data/spec/scenario/public/.gitkeep +0 -0
  59. data/spec/spec_helper.rb +23 -0
  60. data/spec/strategy_spec.rb +87 -0
  61. data/spec/support/migrations.rb +4 -0
  62. metadata +236 -0
@@ -0,0 +1,135 @@
1
+ if defined?(Rails) && Rails::VERSION::MAJOR == 3
2
+ require "action_dispatch"
3
+ end
4
+
5
+ require 'devise'
6
+
7
+ require 'devise_cas_authenticatable/schema'
8
+ require 'devise_cas_authenticatable/routes'
9
+ require 'devise_cas_authenticatable/strategy'
10
+ require 'devise_cas_authenticatable/exceptions'
11
+ require 'devise_cas_authenticatable/missing_session_helpers'
12
+ require 'devise_cas_authenticatable/single_sign_out'
13
+
14
+ if ActiveSupport.respond_to? :on_load
15
+ ActiveSupport.on_load(:action_controller) do
16
+ include MissingSessionHelpers
17
+ end
18
+
19
+ ActiveSupport.on_load(:action_view) do
20
+ include MissingSessionHelpers
21
+ end
22
+ else
23
+ ActionController.instance_eval do
24
+ include MissingSessionHelpers
25
+ end
26
+
27
+ ActionView.instance_eval do
28
+ include MissingSessionHelpers
29
+ end
30
+ end
31
+
32
+ if defined?(ActiveRecord::SessionStore)
33
+ require 'devise_cas_authenticatable/single_sign_out/session_store/active_record'
34
+ end
35
+
36
+ if defined?(Redis::Store)
37
+ require 'devise_cas_authenticatable/single_sign_out/session_store/redis'
38
+ end
39
+
40
+ require 'rubycas-client'
41
+
42
+ # Register as a Rails engine if Rails::Engine exists
43
+ begin
44
+ Rails::Engine
45
+ rescue
46
+ else
47
+ module DeviseCasAuthenticatable
48
+ class Engine < Rails::Engine
49
+ end
50
+ end
51
+ end
52
+
53
+ module Devise
54
+ # The base URL of the CAS server. For example, http://cas.example.com. Specifying this
55
+ # is mandatory.
56
+ @@cas_base_url = ENV['CLOUDFUJI_CAS_URL'] || "https://cloudfuji.com/cas"
57
+
58
+ # The login URL of the CAS server. If undefined, will default based on cas_base_url.
59
+ @@cas_login_url = nil
60
+
61
+ # The login URL of the CAS server. If undefined, will default based on cas_base_url.
62
+ @@cas_logout_url = nil
63
+
64
+ # The login URL of the CAS server. If undefined, will default based on cas_base_url.
65
+ @@cas_validate_url = nil
66
+
67
+ # Should devise_cas_authenticatable enable single-sign-out? Requires use of a supported
68
+ # session_store. Currently supports active_record or redis.
69
+ # False by default.
70
+ @@cas_enable_single_sign_out = true
71
+
72
+ # What strategy should single sign out use for tracking token->session ID mapping.
73
+ # :rails_cache by default.
74
+ @@cas_single_sign_out_mapping_strategy = :rails_cache
75
+
76
+ # Should devise_cas_authenticatable attempt to create new user records for
77
+ # unknown usernames? True by default.
78
+ @@cas_create_user = true
79
+
80
+ # The model attribute used for query conditions. Should be the same as
81
+ # the rubycas-server username_column. :username by default
82
+ @@cas_username_column = :ido_id
83
+
84
+ # Name of the parameter passed in the logout query
85
+ @@cas_destination_logout_param_name = nil
86
+
87
+ mattr_accessor :cas_base_url, :cas_login_url, :cas_logout_url, :cas_validate_url, :cas_create_user, :cas_destination_logout_param_name, :cas_username_column, :cas_enable_single_sign_out, :cas_single_sign_out_mapping_strategy
88
+
89
+ def self.cas_create_user?
90
+ cas_create_user
91
+ end
92
+
93
+ # Return a CASClient::Client instance based on configuration parameters.
94
+ def self.cas_client
95
+ @@cas_client ||= CASClient::Client.new(
96
+ :cas_destination_logout_param_name => @@cas_destination_logout_param_name,
97
+ :cas_base_url => @@cas_base_url,
98
+ :login_url => @@cas_login_url,
99
+ :logout_url => @@cas_logout_url,
100
+ :validate_url => @@cas_validate_url,
101
+ :enable_single_sign_out => @@cas_enable_single_sign_out
102
+ )
103
+ end
104
+
105
+ def self.cas_service_url(base_url, mapping)
106
+ cas_action_url(base_url, mapping, "service")
107
+ end
108
+
109
+ def self.cas_unregistered_url(base_url, mapping)
110
+ cas_action_url(base_url, mapping, "unregistered")
111
+ end
112
+
113
+ private
114
+ def self.cas_action_url(base_url, mapping, action)
115
+ u = URI.parse(base_url)
116
+ u.query = nil
117
+ u.path = if mapping.respond_to?(:fullpath)
118
+ mapping.fullpath
119
+ else
120
+ mapping.raw_path
121
+ end
122
+ u.path << "/"
123
+ u.path << action
124
+ u.to_s
125
+
126
+ return u.to_s
127
+ end
128
+
129
+ end
130
+
131
+ Devise.add_module(:cloudfuji_authenticatable,
132
+ :strategy => true,
133
+ :controller => :cas_sessions,
134
+ :route => :cloudfuji_authenticatable,
135
+ :model => 'devise_cas_authenticatable/model')
@@ -0,0 +1,10 @@
1
+ # Thrown when a user attempts to pass a CAS ticket that the server
2
+ # says is invalid.
3
+ class InvalidCasTicketException < Exception
4
+ attr_reader :ticket
5
+
6
+ def initialize(ticket, msg=nil)
7
+ super(msg)
8
+ @ticket = ticket
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module MissingSessionHelpers
2
+ def new_session_path(resource_name, *args)
3
+ send "new_#{resource_name}_session_path".to_sym, *args
4
+ end
5
+
6
+ def destroy_session_path(resource_name, *args)
7
+ send "destroy_#{resource_name}_session_path".to_sym, *args
8
+ end
9
+ end
@@ -0,0 +1,56 @@
1
+ module Devise
2
+ module Models
3
+ # Extends your User class with support for CAS ticket authentication.
4
+ module CloudfujiAuthenticatable
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+
8
+ if defined?(Mongoid)
9
+ base.class_eval do
10
+ field :ido_id # TODO check with someone who's using Mongoid
11
+ end
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ # Authenticate a CAS ticket and return the resulting user object. Behavior is as follows:
17
+ #
18
+ # * Check ticket validity using RubyCAS::Client. Return nil if the ticket is invalid.
19
+ # * Find a matching user by username (will use find_for_authentication if available).
20
+ # * If the user does not exist, but Devise.cas_create_user is set, attempt to create the
21
+ # user object in the database. If cas_extra_attributes= is defined, this will also
22
+ # pass in the ticket's extra_attributes hash.
23
+ # * Return the resulting user object.
24
+ def authenticate_with_cas_ticket(ticket)
25
+ ::Devise.cas_client.validate_service_ticket(ticket) unless ticket.has_been_validated?
26
+
27
+ puts "ticket = #{ticket.inspect}"
28
+
29
+ if ticket.is_valid?
30
+ conditions = {::Devise.cas_username_column => ticket.respond_to?(:user) ? ticket.user : ticket.response.user}
31
+ # We don't want to override Devise 1.1's find_for_authentication
32
+ resource = if respond_to?(:find_for_authentication)
33
+ find_for_authentication(conditions)
34
+ else
35
+ find(:first, :conditions => conditions)
36
+ end
37
+
38
+ resource = new(conditions) if (resource.nil? and ::Devise.cas_create_user?)
39
+
40
+ puts "found #{resource.inspect}"
41
+
42
+ return nil unless resource
43
+
44
+ if resource.respond_to? :cloudfuji_extra_attributes
45
+ extra_attributes = ticket.respond_to?(:extra_attributes) ? ticket.extra_attributes : ticket.response.extra_attributes
46
+ resource.cloudfuji_extra_attributes(extra_attributes)
47
+ end
48
+
49
+ resource.save
50
+ resource
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,37 @@
1
+ if defined?(ActionDispatch)
2
+ # Rails 3
3
+
4
+ ActionDispatch::Routing::Mapper.class_eval do
5
+ protected
6
+
7
+ def devise_cloudfuji_authenticatable(mapping, controllers)
8
+ # service endpoint for CAS server
9
+ get "service", :to => "#{controllers[:cas_sessions]}#service", :as => "service"
10
+ post "service", :to => "#{controllers[:cas_sessions]}#single_sign_out", :as => "single_sign_out"
11
+
12
+ resource :session, :only => [], :controller => controllers[:cas_sessions], :path => "" do
13
+ get :new, :path => mapping.path_names[:sign_in], :as => "new"
14
+ get :unregistered
15
+ post :create, :path => mapping.path_names[:sign_in]
16
+ match :destroy, :path => mapping.path_names[:sign_out], :as => "destroy"
17
+ end
18
+ end
19
+ end
20
+ else
21
+
22
+ # Rails 2
23
+ ActionController::Routing::RouteSet::Mapper.class_eval do
24
+ protected
25
+
26
+ def cloudfuji_authenticatable(routes, mapping)
27
+ routes.with_options(:controller => 'devise/cas_sessions', :name_prefix => nil) do |session|
28
+ session.send(:"#{mapping.name}_service", '/service', :action => 'service', :conditions => {:method => :get})
29
+ session.send(:"#{mapping.name}_service", '/service', :action => 'single_sign_out', :conditions => {:method => :post})
30
+ session.send(:"unregistered_#{mapping.name}_session", '/unregistered', :action => "unregistered", :conditions => {:method => :get})
31
+ session.send(:"new_#{mapping.name}_session", mapping.path_names[:sign_in], :action => 'new', :conditions => {:method => :get})
32
+ session.send(:"#{mapping.name}_session", mapping.path_names[:sign_in], :action => 'create', :conditions => {:method => :post})
33
+ session.send(:"destroy_#{mapping.name}_session", mapping.path_names[:sign_out], :action => 'destroy', :conditions => { :method => :get })
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ require 'devise/schema'
2
+
3
+ module Devise
4
+ module Schema
5
+ def cloudfuji_authenticatable
6
+ if respond_to? :apply_devise_schema
7
+ apply_devise_schema :ido_id, String
8
+ else
9
+ apply_schema :ido_id, String
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ module DeviseCasAuthenticatable
2
+ module SingleSignOut
3
+ module SetSession
4
+ def set_session_with_storage(env, sid, session_data, options={})
5
+ if session_data['cas_last_valid_ticket_store']
6
+ ::DeviseCasAuthenticatable::SingleSignOut::Strategies.current_strategy.store_session_id_for_index(session_data['cas_last_valid_ticket'], sid)
7
+ session_data['cas_last_valid_ticket_store'] = nil
8
+ end
9
+
10
+ if method(:set_session_without_storage).arity == 4
11
+ set_session_without_storage(env, sid, session_data, options)
12
+ else
13
+ set_session_without_storage(env, sid, session_data)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ require 'devise_cas_authenticatable/single_sign_out/strategies'
21
+ require 'devise_cas_authenticatable/single_sign_out/strategies/base'
22
+ require 'devise_cas_authenticatable/single_sign_out/strategies/rails_cache'
@@ -0,0 +1,12 @@
1
+ ActiveRecord::SessionStore.class_eval do
2
+
3
+ include DeviseCasAuthenticatable::SingleSignOut::SetSession
4
+ alias_method_chain :set_session, :storage
5
+
6
+ #def destroy_session(env, session_id, options)
7
+ # if session = Session::find_by_session_id(sid)
8
+ # session.destroy
9
+ # end
10
+ #end
11
+
12
+ end
@@ -0,0 +1,27 @@
1
+ require "action_controller/session/redis_session_store"
2
+
3
+ module DeviseCasAuthenticatable
4
+ module SingleSignOut
5
+ module RedisSessionStore
6
+
7
+ include DeviseCasAuthenticatable::SingleSignOut::SetSession
8
+
9
+ def destroy_session(sid)
10
+ @pool.del(sid)
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+
17
+ if ::Redis::Store.rails3?
18
+ ActionDispatch::Session::RedisSessionStore.class_eval do
19
+ include DeviseCasAuthenticatable::SingleSignOut::RedisSessionStore
20
+ alias_method_chain :set_session, :storage
21
+ end
22
+ else
23
+ ActionController::Session::RedisSessionStore.class_eval do
24
+ include DeviseCasAuthenticatable::SingleSignOut::RedisSessionStore
25
+ alias_method_chain :set_session, :storage
26
+ end
27
+ end
@@ -0,0 +1,58 @@
1
+ module DeviseCasAuthenticatable
2
+ module SingleSignOut
3
+ module Strategies
4
+ class << self
5
+
6
+ # Add a strategy and store it in a hash.
7
+ def add(label, strategy, &block)
8
+ strategy ||= Class.new(DeviseCasAuthenticatable::SingleSignOut::Strategies::Base)
9
+ strategy.class_eval(&block) if block_given?
10
+
11
+ check_method(label, strategy, :store_session_id_for_index)
12
+ check_method(label, strategy, :find_session_id_by_index)
13
+ check_method(label, strategy, :delete_session_index)
14
+
15
+ unless strategy.ancestors.include?(DeviseCasAuthenticatable::SingleSignOut::Strategies::Base)
16
+ raise "#{label.inspect} is not a #{base}"
17
+ end
18
+
19
+ _strategies[label] = strategy.new()
20
+ end
21
+
22
+ # Update a previously given strategy.
23
+ def update(label, &block)
24
+ strategy = _strategies[label]
25
+ raise "Unknown strategy #{label.inspect}" unless strategy
26
+ add(label, strategy, &block)
27
+ end
28
+
29
+ # Provides access to strategies by label
30
+ def [](label)
31
+ _strategies[label]
32
+ end
33
+
34
+ def current_strategy
35
+ self[::Devise.cas_single_sign_out_mapping_strategy]
36
+ end
37
+
38
+ # Clears all declared.
39
+ def clear!
40
+ _strategies.clear
41
+ end
42
+
43
+ private
44
+
45
+ def _strategies
46
+ @strategies ||= {}
47
+ end
48
+
49
+ def check_method(label, strategy, method)
50
+ unless strategy.method_defined?(method)
51
+ raise NoMethodError, "#{method.to_s} is not declared in the #{label.inspect} strategy"
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,11 @@
1
+ module DeviseCasAuthenticatable
2
+ module SingleSignOut
3
+ module Strategies
4
+ class Base
5
+ def logger
6
+ @logger ||= Rails.logger
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ module DeviseCasAuthenticatable
2
+ module SingleSignOut
3
+ module Strategies
4
+ class RailsCache < Base
5
+ def store_session_id_for_index(session_index, session_id)
6
+ logger.debug("Storing #{session_id} for index #{session_index}")
7
+ Rails.cache.write(cache_key(session_index), session_id)
8
+ end
9
+
10
+ def find_session_id_by_index(session_index)
11
+ sid = Rails.cache.read(cache_key(session_index))
12
+ logger.debug("Found session id #{sid.inspect} for index #{session_index.inspect}")
13
+ sid
14
+ end
15
+
16
+ def delete_session_index(session_index)
17
+ logger.info("Deleting index #{session_index}")
18
+ Rails.cache.delete(cache_key(session_index))
19
+ end
20
+
21
+ private
22
+
23
+ def cache_key(session_index)
24
+ "devise_cas_authenticatable:#{session_index}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ::DeviseCasAuthenticatable::SingleSignOut::Strategies.add( :rails_cache, DeviseCasAuthenticatable::SingleSignOut::Strategies::RailsCache )
@@ -0,0 +1,56 @@
1
+ require 'devise/strategies/base'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module Devise
6
+ module Strategies
7
+ class CasAuthenticatable < Base
8
+ # True if the mapping supports authenticate_with_cas_ticket.
9
+ def valid?
10
+ mapping.to.respond_to?(:authenticate_with_cas_ticket) && params[:ticket]
11
+ end
12
+
13
+ # Try to authenticate a user using the CAS ticket passed in params.
14
+ # If the ticket is valid and the model's authenticate_with_cas_ticket method
15
+ # returns a user, then return success. If the ticket is invalid, then either
16
+ # fail (if we're just returning from the CAS server, based on the referrer)
17
+ # or attempt to redirect to the CAS server's login URL.
18
+ def authenticate!
19
+ ticket = read_ticket(params)
20
+ fail!(:invalid) if not ticket
21
+
22
+ if resource = mapping.to.authenticate_with_cas_ticket(ticket)
23
+ # Store the ticket in the session for later usage
24
+ if ::Devise.cas_enable_single_sign_out
25
+ session['cas_last_valid_ticket'] = ticket.ticket
26
+ session['cas_last_valid_ticket_store'] = true
27
+ end
28
+
29
+ success!(resource)
30
+ elsif ticket.is_valid?
31
+ ido_id = ticket.respond_to?(:user) ? ticket.user : ticket.response.user
32
+ redirect!(::Devise.cas_unregistered_url(request.url, mapping), :ido_id => ido_id)
33
+ #fail!("The user #{ticket.response.user} is not registered with this site. Please use a different account.")
34
+ else
35
+ fail!(:invalid)
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ def read_ticket(params)
42
+ ticket = params[:ticket]
43
+ return nil unless ticket
44
+
45
+ service_url = ::Devise.cas_service_url(request.url, mapping)
46
+ if ticket =~ /^PT-/
47
+ ::CASClient::ProxyTicket.new(ticket, service_url, params[:renew])
48
+ else
49
+ ::CASClient::ServiceTicket.new(ticket, service_url, params[:renew])
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ Warden::Strategies.add(:cloudfuji_authenticatable, Devise::Strategies::CasAuthenticatable)