rubycas-client 0.9.1 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +33 -0
- data/README +95 -47
- data/Rakefile +22 -0
- data/init.rb +11 -0
- data/install.rb +1 -0
- data/lib/cas.rb +188 -0
- data/lib/cas_auth.rb +311 -0
- data/lib/cas_proxy_callback_controller.rb +72 -0
- metadata +8 -3
- data/lib/cas-client.rb +0 -289
data/LICENSE
CHANGED
@@ -1,3 +1,36 @@
|
|
1
|
+
Copyright (c) 2006 Karolinska Institutet
|
2
|
+
(Karolinska Institutet, Stockholm, Sweden).
|
3
|
+
All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions
|
7
|
+
are met:
|
8
|
+
|
9
|
+
1. Redistributions of source code must retain the above copyright
|
10
|
+
notice, this list of conditions and the following disclaimer.
|
11
|
+
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright
|
13
|
+
notice, this list of conditions and the following disclaimer in the
|
14
|
+
documentation and/or other materials provided with the distribution.
|
15
|
+
|
16
|
+
3. Neither the name of Karolinska Institutet nor the names of its contributors
|
17
|
+
may be used to endorse or promote products derived from this software
|
18
|
+
without specific prior written permission.
|
19
|
+
|
20
|
+
THIS SOFTWARE IS PROVIDED BY KAROLINSKA INSTITUTET AND CONTRIBUTORS ``AS IS''
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
23
|
+
ARE DISCLAIMED. IN NO EVENT SHALL KAROLINSKA INSTITUTET OR CONTRIBUTORS BE
|
24
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
25
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
26
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
27
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
28
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
29
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
30
|
+
POSSIBILITY OF SUCH DAMAGE.
|
31
|
+
|
32
|
+
===============================================================================
|
33
|
+
|
1
34
|
GNU LESSER GENERAL PUBLIC LICENSE
|
2
35
|
Version 2.1, February 1999
|
3
36
|
|
data/README
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
= RubyCAS-Client
|
2
2
|
|
3
|
-
Author::
|
4
|
-
Copyright::
|
3
|
+
Author:: Ola Bini <ola.bini AT ki DOT se>, Matt Zukowski <matt AT roughest DOT net>
|
4
|
+
Copyright:: (c) 2006 Karolinska Institutet, portions (c) 2006 Urbacon Ltd.
|
5
5
|
License:: GNU Lesser General Public License v2.1 (LGPL 2.1)
|
6
6
|
Website:: http://rubyforge.org/projects/rubycas-client
|
7
7
|
|
@@ -19,10 +19,14 @@ about the CAS protocol here: http://www.ja-sig.org/products/cas
|
|
19
19
|
|
20
20
|
This CAS client library is designed to work easily with Rails, but can of course be used elsewhere.
|
21
21
|
|
22
|
-
|
22
|
+
== Installing
|
23
23
|
|
24
|
-
You can always download the latest version of RubyCAS-Client from the project's rubyforge page at http://rubyforge.org/projects/rubycas-client
|
25
|
-
|
24
|
+
You can always download the latest version of RubyCAS-Client from the project's rubyforge page at http://rubyforge.org/projects/rubycas-client,
|
25
|
+
however probably the easiest way to install CAS support into your Rails app is via the plugins facility:
|
26
|
+
|
27
|
+
./script/plugin install http://rubycas-client.rubyforge.org/plugin/current
|
28
|
+
|
29
|
+
Alternatively, the library is also available as a gem, which can be installed by:
|
26
30
|
|
27
31
|
gem install rubycas-client
|
28
32
|
|
@@ -30,72 +34,116 @@ The latest development version is availabe via subversion:
|
|
30
34
|
|
31
35
|
svn checkout svn://rubyforge.org/var/svn/rubycas-client/trunk/ruby
|
32
36
|
|
37
|
+
Or you can install the latest development version into your Rails app as a plugin:
|
38
|
+
|
39
|
+
./script/plugin install -x svn://rubyforge.org/var/svn/rubycas-client/trunk/ruby
|
40
|
+
|
33
41
|
Please contact the developers via the {rubyforge.org page}[svn checkout svn://rubyforge.org/var/svn/rubycas-client] if you have bug fixes
|
34
42
|
or enhancements you would like to contribute back.
|
35
43
|
|
36
|
-
|
44
|
+
== Examples
|
37
45
|
|
38
|
-
|
46
|
+
==== Here is an example of how to use the library in your Rails application:
|
39
47
|
|
40
|
-
|
48
|
+
Somewhere in your +config/environment.rb+ file add this (assuming that you have RubyCAS-Client installed as a plugin, otherwise
|
49
|
+
you'll need to +require 'cas_auth'+ and +require 'cas_proxy_callback_controller'+):
|
41
50
|
|
42
|
-
CAS.
|
43
|
-
CAS.cas_server_port = '443'
|
51
|
+
CAS::Filter.cas_base_url = "https://login.example.com/cas"
|
44
52
|
|
45
53
|
Then, in your +app/controllers/application.rb+ (or in whatever controller you want to add the CAS filter for):
|
46
54
|
|
47
|
-
before_filter CAS::
|
55
|
+
before_filter CAS::Filter
|
48
56
|
|
49
57
|
That's it. You should now find that you are redirected to your CAS login page when you try to access any action
|
50
58
|
in your protected controller. You can of course qualify the +before_filter+ as you would with any other ActionController
|
51
|
-
filter. For example: +before_filter CAS::
|
59
|
+
filter. For example: +before_filter CAS::Filter, :except => [ :unprotected_action, :another_unprotected_action ]
|
52
60
|
|
53
|
-
<b>Once the user has been authenticated, their authenticated username is available under +
|
61
|
+
<b>Once the user has been authenticated, their authenticated username is available under +request.username+
|
62
|
+
(and also under +session[:casfilteruser]+).</b> If you want to do something with this username (for example load a
|
63
|
+
user record from the database), you can append another filter method that checks for this value and does whatever you need
|
64
|
+
it to do.
|
54
65
|
|
55
|
-
|
66
|
+
==== A more complicated example:
|
56
67
|
|
57
|
-
|
58
|
-
|
59
|
-
the username you may want to store a User object (e.g. a User ActiveRecord model). Here is an example of how to do this:
|
68
|
+
Here is a more complicated configuration showing most of the configuration options (this does not show proxy options however,
|
69
|
+
which are covered in the next section):
|
60
70
|
|
61
|
-
|
71
|
+
CAS::Filter.login_url = "https://login.example.com/cas/login" # the URI of the CAS login page
|
72
|
+
CAS::Filter.validate_url = "https://login.example.com/cas/serviceValidate" # the URI where CAS ticket validation requests are sent
|
73
|
+
CAS::Filter.server_name = "yourapplication.example.com:3000" # the server name of your CAS-protected application
|
74
|
+
CAS::Filter.renew = false # force re-authentication? see http://www.ja-sig.org/products/cas/overview/protocol
|
75
|
+
CAS::Filter.wrap_request = true # make the username available under request.username?
|
76
|
+
CAS::Filter.gateway = false # act as cas gateway? see http://www.ja-sig.org/products/cas/overview/protocol
|
77
|
+
CAS::Filter.session_username = :casfilteruser # this is the hash in the session where the authenticated username will be stored
|
62
78
|
|
63
|
-
require_gem 'rubycas-client'
|
64
79
|
|
65
|
-
|
66
|
-
CAS.cas_server_port = '443'
|
80
|
+
==== How to act as a CAS proxy:
|
67
81
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
82
|
+
CAS 2.0 has a built-in mechanism that allows a CAS-authenticated application to pass on its authentication to other applications.
|
83
|
+
An example where this is useful might be a portal site, where the user logs in to a central website and then gets forwarded to
|
84
|
+
various other sites that run independently of the portal system (but are always accessed via the portal). The exact mechanism
|
85
|
+
behind this is rather complicated so I won't go over it here. If you wish to learn more about CAS proxying, a great walkthrough
|
86
|
+
is available at http://www.ja-sig.org/wiki/display/CAS/Proxy+CAS+Walkthrough.
|
87
|
+
|
88
|
+
RubyCAS-Client fully supports proxying, so a CAS-protected Rails application can act as a CAS proxy.
|
89
|
+
|
90
|
+
Additionally, RubyCAS-Client comes with a controller that can act as a CAS proxy callback receiver. This is necessary because
|
91
|
+
when your application requests to act as a CAS proxy, the CAS server must contact your application to deposit the proxy-granting-ticket
|
92
|
+
(PGT). Note that in this case the CAS server CONTACTS YOU, rather than you contacting the CAS server (as in all other CAS operations).
|
93
|
+
|
94
|
+
Confused? Don't worry, you don't really have to understand this to use it. To enable your Rails app to act as a CAS proxy,
|
95
|
+
all you need to do is this:
|
96
|
+
|
97
|
+
In your +config/environment.rb+:
|
98
|
+
|
99
|
+
CAS::Filter.cas_base_url = "https://login.example.com/cas"
|
100
|
+
CAS::Filter.proxy_callback_url = "https://yourrailsapp.com/cas_proxy_callback/receive_pgt"
|
101
|
+
CAS::Filter.proxy_retrieval_url = "https://yourrailsapp.com/cas_proxy_callback/retrieve_pgt"
|
77
102
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
103
|
+
In +config/routes.rb+ make sure that you have a route that will allow requests to /cas_proxy_callback/:action to be routed to the
|
104
|
+
CasProxyCallbackController. This should work as-is with the standard Rails routes setup, but if you have disabled the default
|
105
|
+
route, you should add the following:
|
106
|
+
|
107
|
+
map.cas_proxy_callback 'cas_proxy_callback/:action', :controller => 'cas_proxy_callback'
|
108
|
+
|
109
|
+
Once your user logs in to CAS via your application, you can do the following to obtain a service ticket that can then be used
|
110
|
+
to authenticate another application:
|
111
|
+
|
112
|
+
service_uri = "http://some.other.application"
|
113
|
+
proxy_granting_ticket = session[:casfilterpgt]
|
114
|
+
ticket = CAS::Filter.request_proxy_ticket(service_uri, proxy_granting_ticket)
|
115
|
+
|
116
|
+
+ticket+ should now contain a valid service ticket. You can use it to authenticate your other by sending it and the service URI
|
117
|
+
as query parameters to your target application:
|
118
|
+
|
119
|
+
http://some.other.application?service=#{ticket.target_service}&ticket=#{ticket.proxy_ticket}
|
82
120
|
|
83
|
-
|
84
|
-
+save_user_data+ is also overidden to save any changes made to the user model by the login process (in this case,
|
85
|
-
the last_login column is updated). See the CAS module documentation for more information on callbacks.
|
121
|
+
This is of course assuming that some.other.application is also protected by the CAS filter.
|
86
122
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
authentication ticket has expired, but the local session authentication has not. To prevent this from happening
|
91
|
-
you can specify a +refresh_ticket_interval+ (in minutes) as follows:
|
123
|
+
For extra security -- and you will likely want to do this on production machines in the wild -- in the proxied app's configuration
|
124
|
+
(some.other.appliction in this example) you can specify the list of authorized proxies. For example, on your proxied app the CAS
|
125
|
+
configuration might look something like this:
|
92
126
|
|
93
|
-
CAS.
|
127
|
+
CAS::Filter.cas_base_url = "https://login.example.com/cas"
|
128
|
+
CAS::Filter.server_name = "some.other.application"
|
129
|
+
CAS::Filter.authorized_proxies = ["https://yourrailsapp.com/cas/proxy_callback/receive_pgt"]
|
94
130
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
131
|
+
If no authorized proxies are given, the filter will accept receipts from any proxy.
|
132
|
+
|
133
|
+
===== Additional notes and caveats:
|
134
|
+
|
135
|
+
Note that when the CAS filter runs, the PGT is stored in session[:casfilterpgt]. This value must be passed to CAS::Filter#request_proxy_ticket.
|
136
|
+
Also, note that CAS::Filter#request_proxy_ticket will URI-encode the service_uri before passing it to the CAS server, and the service
|
137
|
+
value must henceforth always be passed as URI-encoded (this can be problematic when your proxied application uses some CAS client other than
|
138
|
+
RubyCAS-Client).
|
139
|
+
|
140
|
+
<b>The proxy url must be an https address.</b> Otherwise CAS will refuse to communicate with it. This means that if you are using
|
141
|
+
the bundled cas_proxy_callback controller, you will have to host your application on an https-enabled server. This can be a bit
|
142
|
+
tricky with Rails. WEBrick's SSL support is difficult to configure, and Mongrel doesn't support SSL at all. One workaround is to
|
143
|
+
use a reverse proxy like Pound[http://www.apsis.ch/pound/], which will accept https connections and locally re-route them
|
144
|
+
to your Rails application. Also, note that <i>self-signed SSL certificates likely won't work</i>. You will probably need to use
|
145
|
+
a real certificate purchased from a trusted CA authority (there are ways around this, but good luck :)
|
146
|
+
|
99
147
|
|
100
148
|
== License
|
101
149
|
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the cas_auth plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Generate documentation for the cas_auth plugin.'
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'CasAuth'
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
20
|
+
rdoc.rdoc_files.include('README')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'cas_auth'
|
2
|
+
|
3
|
+
#CAS::Filter.logger = RAILS_DEFAULT_LOGGER if !RAILS_DEFAULT_LOGGER.nil?
|
4
|
+
#CAS::Filter.logger = config.logger if !config.logger.nil?
|
5
|
+
|
6
|
+
CAS::Filter.logger = Logger.new "#{RAILS_ROOT}/log/cas_filter.log"
|
7
|
+
CAS::Filter.logger.level = Logger::DEBUG
|
8
|
+
|
9
|
+
#class ActionController::Base
|
10
|
+
# append_before_filter CAS::Filter
|
11
|
+
#end
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
data/lib/cas.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'rexml/document'
|
3
|
+
|
4
|
+
module CAS
|
5
|
+
class CASException < Exception
|
6
|
+
end
|
7
|
+
class AuthenticationException < CASException
|
8
|
+
end
|
9
|
+
class ValidationException < CASException
|
10
|
+
end
|
11
|
+
|
12
|
+
class Receipt
|
13
|
+
attr_accessor :validate_url, :pgt_iou, :primary_authentication, :proxy_callback_url, :proxy_list, :user_name
|
14
|
+
|
15
|
+
def primary_authentication?
|
16
|
+
primary_authentication
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(ptv)
|
20
|
+
if !ptv.successful_authentication?
|
21
|
+
begin
|
22
|
+
ptv.validate
|
23
|
+
rescue ValidationException=>vald
|
24
|
+
raise AuthenticationException, "Unable to validate ProxyTicketValidator [#{ptv}] [#{vald}]"
|
25
|
+
end
|
26
|
+
raise AuthenticationException, "Unable to validate ProxyTicketValidator because of no success with validation[#{ptv}]" unless ptv.successful_authentication?
|
27
|
+
end
|
28
|
+
self.validate_url = ptv.validate_url
|
29
|
+
self.pgt_iou = ptv.pgt_iou
|
30
|
+
self.user_name = ptv.user
|
31
|
+
self.proxy_callback_url = ptv.proxy_callback_url
|
32
|
+
self.proxy_list = ptv.proxy_list
|
33
|
+
self.primary_authentication = ptv.renewed?
|
34
|
+
raise AuthenticationException, "Validation of [#{ptv}] did not result in an internally consistent Receipt" unless validate
|
35
|
+
end
|
36
|
+
|
37
|
+
def proxied?
|
38
|
+
!proxy_list.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def proxying_service
|
42
|
+
proxy_list.first
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
"[#{super} - userName=[#{user_name}] validateUrl=[#{validate_url}] proxyCallbackUrl=[#{proxy_callback_url}] pgtIou=[#{pgt_iou}] proxyList=[#{proxy_list}]"
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate
|
50
|
+
user_name &&
|
51
|
+
validate_url &&
|
52
|
+
proxy_list &&
|
53
|
+
!(primary_authentication? && !proxy_list.empty?) # May not be both primary authenitication and proxied.
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class AbstractCASResponse
|
58
|
+
|
59
|
+
def self.retrieve(uri_str)
|
60
|
+
prs = URI.parse(uri_str)
|
61
|
+
# puts prs.inspect
|
62
|
+
https = Net::HTTP.new(prs.host,prs.port)
|
63
|
+
# puts https.inspect
|
64
|
+
https.use_ssl=true
|
65
|
+
https.start { |conn|
|
66
|
+
# TODO: make sure that HTTP status code in the response is 200... maybe throw exception if is 500?
|
67
|
+
conn.get("#{prs.path}?#{prs.query}").body.strip
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
def parse_unsuccessful(elm)
|
73
|
+
# puts "unsuccessful"
|
74
|
+
@error_message = elm.text.strip
|
75
|
+
@error_code = elm.attributes["code"].strip
|
76
|
+
@successful_authentication = false
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse(str)
|
80
|
+
# puts "parsing... #{str}"
|
81
|
+
doc = REXML::Document.new str
|
82
|
+
resp = doc.elements["cas:serviceResponse"].elements[1]
|
83
|
+
# puts "resp... #{resp.name}"
|
84
|
+
if successful_response? resp
|
85
|
+
parse_successful(resp)
|
86
|
+
else
|
87
|
+
parse_unsuccessful(resp)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class ServiceTicketValidator < AbstractCASResponse
|
93
|
+
attr_accessor :validate_url, :proxy_callback_url, :renew, :service_ticket, :service
|
94
|
+
attr_reader :pgt_iou, :user, :error_code, :error_message, :entire_response, :successful_authentication
|
95
|
+
|
96
|
+
def renewed?
|
97
|
+
renew
|
98
|
+
end
|
99
|
+
|
100
|
+
def successful_authentication?
|
101
|
+
successful_authentication
|
102
|
+
end
|
103
|
+
|
104
|
+
def validate
|
105
|
+
raise ValidationException, "must set validation URL and ticket" if validate_url.nil? || service_ticket.nil?
|
106
|
+
clear!
|
107
|
+
@attempted_authentication = true
|
108
|
+
url_building = "#{validate_url}#{(url_building =~ /\?/)?'&':'?'}service=#{service}&ticket=#{service_ticket}"
|
109
|
+
url_building += "&pgtUrl=#{proxy_callback_url}" if proxy_callback_url
|
110
|
+
url_building += "&renew=true" if renew
|
111
|
+
@@entire_response = ServiceTicketValidator.retrieve url_building
|
112
|
+
parse @@entire_response
|
113
|
+
end
|
114
|
+
|
115
|
+
def clear!
|
116
|
+
@user = @pgt_iou = @error_message = nil
|
117
|
+
@successful_authentication = @attempted_authentication = false
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_s
|
121
|
+
"[#{super} - validateUrl=[#{validate_url}] proxyCallbackUrl=[#{proxy_callback_url}] ticket=[#{service_ticket}] service=[#{service} pgtIou=[#{pgt_iou}] user=[#{user}] errorCode=[#{error_message}] errorMessage=[#{error_message}] renew=[#{renew}] entireResponse=[#{entire_response}]]"
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
def parse_successful(elm)
|
126
|
+
# puts "successful"
|
127
|
+
@user = elm.elements["cas:user"] && elm.elements["cas:user"].text.strip
|
128
|
+
# puts "user: #{@user}"
|
129
|
+
@pgt_iou = elm.elements["cas:proxyGrantingTicket"] && elm.elements["cas:proxyGrantingTicket"].text.strip
|
130
|
+
# puts "pgt_iou: #{@pgt_iou}"
|
131
|
+
@successful_authentication = true
|
132
|
+
end
|
133
|
+
|
134
|
+
def successful_response?(resp)
|
135
|
+
resp.name == "authenticationSuccess"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class ProxyTicketValidator < ServiceTicketValidator
|
140
|
+
attr_reader :proxy_list
|
141
|
+
@@response_prefix = "proxy"
|
142
|
+
|
143
|
+
def initialize
|
144
|
+
super
|
145
|
+
@proxy_list = []
|
146
|
+
end
|
147
|
+
|
148
|
+
def clear!
|
149
|
+
super
|
150
|
+
@proxy_list = []
|
151
|
+
end
|
152
|
+
|
153
|
+
protected
|
154
|
+
def parse_successful(elm)
|
155
|
+
super(elm)
|
156
|
+
# puts "proxy_successful"
|
157
|
+
proxies = elm.elements["cas:proxies"]
|
158
|
+
if proxies
|
159
|
+
proxies.elements.each("cas:proxy") { |prox|
|
160
|
+
@proxy_list ||= []
|
161
|
+
@proxy_list << prox.text.strip
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class ProxyTicketRequest < AbstractCASResponse
|
168
|
+
attr_accessor :proxy_url, :target_service, :pgt
|
169
|
+
attr_reader :proxy_ticket
|
170
|
+
|
171
|
+
def request
|
172
|
+
url_building = "#{proxy_url}#{(url_building =~ /\?/)?'&':'?'}targetService=#{target_service}&pgt=#{pgt}"
|
173
|
+
# puts "REQUESTING:"+url_building
|
174
|
+
@@entire_response = ServiceTicketValidator.retrieve url_building
|
175
|
+
# puts @@entire_response.to_s
|
176
|
+
parse @@entire_response
|
177
|
+
end
|
178
|
+
|
179
|
+
protected
|
180
|
+
def parse_successful(elm)
|
181
|
+
@proxy_ticket = elm.elements["cas:proxyTicket"] && elm.elements["cas:proxyTicket"].text.strip
|
182
|
+
end
|
183
|
+
|
184
|
+
def successful_response?(resp)
|
185
|
+
resp.name == "proxySuccess"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
data/lib/cas_auth.rb
ADDED
@@ -0,0 +1,311 @@
|
|
1
|
+
# RubyCAS-Client is a client and Rails filter for the CAS protocol.
|
2
|
+
# Copyright (c) 2006 Karolinska Institutet
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program; if not, write to the Free Software Foundation,
|
15
|
+
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
16
|
+
|
17
|
+
require 'uri'
|
18
|
+
require 'logger'
|
19
|
+
|
20
|
+
|
21
|
+
require File.dirname(File.expand_path(__FILE__))+'/cas'
|
22
|
+
|
23
|
+
module CAS
|
24
|
+
# The DummyLogger is a class which might pass through to a real Logger
|
25
|
+
# if one is assigned. However, it can gracefully swallow any logging calls
|
26
|
+
# if there is now Logger assigned.
|
27
|
+
class LoggerWrapper
|
28
|
+
def initialize(logger=nil)
|
29
|
+
set_logger(logger)
|
30
|
+
end
|
31
|
+
# Assign the 'real' Logger instance that this dummy instance wraps around.
|
32
|
+
def set_logger(logger)
|
33
|
+
@logger = logger
|
34
|
+
end
|
35
|
+
# log using the appropriate method if we have a logger
|
36
|
+
# if we dont' have a logger, ignore completely.
|
37
|
+
def method_missing(name, *args)
|
38
|
+
if @logger && @logger.respond_to?(name)
|
39
|
+
@logger.send(name, *args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
LOGGER = CAS::LoggerWrapper.new
|
45
|
+
|
46
|
+
# Allows authentication through a CAS server.
|
47
|
+
# The precondition for this filter to work is that you have an
|
48
|
+
# authentication infrastructure. As such, this is for the enterprise
|
49
|
+
# rather than small shops.
|
50
|
+
#
|
51
|
+
# To use CAS::Filter for authentication, add something like this to
|
52
|
+
# your environment:
|
53
|
+
#
|
54
|
+
# CAS::Filter.server_name = "yourapplication.server.name"
|
55
|
+
# CAS::Filter.cas_base_url = "https://cas.company.com
|
56
|
+
#
|
57
|
+
# The filter will try to use the standard CAS page locations based on this URL.
|
58
|
+
# Or you can explicitly specify the individual URLs:
|
59
|
+
#
|
60
|
+
# CAS::Filter.server_name = "yourapplication.server.name"
|
61
|
+
# CAS::Filter.login_url = "https://cas.company.com/login"
|
62
|
+
# CAS::Filter.validate_url = "https://cas.company.com/proxyValidate"
|
63
|
+
#
|
64
|
+
# It is of course possible to use different configurations in development, test
|
65
|
+
# and production by placing the configuration in the appropriate environments file.
|
66
|
+
#
|
67
|
+
# To add CAS protection to a controller:
|
68
|
+
#
|
69
|
+
# before_filter CAS::Filter
|
70
|
+
#
|
71
|
+
# All of the standard Rails filter qualifiers can also be used. For example:
|
72
|
+
#
|
73
|
+
# before_filter CAS::Filter, :only => [:admin, :private]
|
74
|
+
#
|
75
|
+
# By default CAS::Filter saves the logged in user in session[:casfilteruser] but
|
76
|
+
# that name can be changed by setting CAS::Filter.session_username
|
77
|
+
# The username is also available from the request by
|
78
|
+
#
|
79
|
+
# request.username
|
80
|
+
#
|
81
|
+
# This wrapping of the request can be disabled by
|
82
|
+
#
|
83
|
+
# CAS::Filter.wrap_request = false
|
84
|
+
#
|
85
|
+
# Proxying is also possible. Please see the README for examples.
|
86
|
+
#
|
87
|
+
class Filter
|
88
|
+
@@login_url = "https://localhost/login"
|
89
|
+
@@logout_url = nil
|
90
|
+
@@validate_url = "https://localhost/proxyValidate"
|
91
|
+
@@server_name = "localhost"
|
92
|
+
@@renew = false
|
93
|
+
@@session_username = :casfilteruser
|
94
|
+
@@query_string = {}
|
95
|
+
@@fake = nil
|
96
|
+
@@pgt = nil
|
97
|
+
cattr_accessor :query_string
|
98
|
+
cattr_accessor :login_url, :validate_url, :service_url, :server_name, :renew, :wrap_request, :gateway, :session_username
|
99
|
+
cattr_accessor :proxy_url, :proxy_callback_url, :proxy_retrieval_url
|
100
|
+
@@authorized_proxies = []
|
101
|
+
cattr_accessor :authorized_proxies
|
102
|
+
|
103
|
+
|
104
|
+
class << self
|
105
|
+
# Retrieves the current Logger instance
|
106
|
+
def logger
|
107
|
+
CAS::LOGGER
|
108
|
+
end
|
109
|
+
def logger=(val)
|
110
|
+
CAS::LOGGER.set_logger(val)
|
111
|
+
end
|
112
|
+
|
113
|
+
alias :log :logger
|
114
|
+
alias :log= :logger=
|
115
|
+
|
116
|
+
def create_logout_url
|
117
|
+
if !@@logout_url && @@login_url =~ %r{^(.+?)/[^/]*$}
|
118
|
+
@@logout_url = "#{$1}/logout"
|
119
|
+
end
|
120
|
+
logger.info "Created logout-url: #{@@logout_url}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def logout_url(controller)
|
124
|
+
create_logout_url unless @@logout_url
|
125
|
+
url = redirect_url(controller,@@logout_url)
|
126
|
+
logger.info "Using logout-url #{url}"
|
127
|
+
url
|
128
|
+
end
|
129
|
+
|
130
|
+
def logout_url=(val)
|
131
|
+
@@logout_url = val
|
132
|
+
end
|
133
|
+
|
134
|
+
def cas_base_url=(url)
|
135
|
+
CAS::Filter.login_url = "#{url}/login"
|
136
|
+
CAS::Filter.validate_url = "#{url}/proxyValidate"
|
137
|
+
CAS::Filter.proxy_url = "#{url}/proxy"
|
138
|
+
end
|
139
|
+
|
140
|
+
def fake
|
141
|
+
@@fake
|
142
|
+
end
|
143
|
+
|
144
|
+
def fake=(val)
|
145
|
+
if val.nil?
|
146
|
+
alias :filter :filter_r
|
147
|
+
else
|
148
|
+
alias :filter :filter_f
|
149
|
+
end
|
150
|
+
@@fake = val
|
151
|
+
end
|
152
|
+
|
153
|
+
def filter_f(controller)
|
154
|
+
logger.debug("entering fake cas filter")
|
155
|
+
username = @@fake
|
156
|
+
if :failure == @@fake
|
157
|
+
return false
|
158
|
+
elsif :param == @@fake
|
159
|
+
username = controller.params['username']
|
160
|
+
elsif Proc === @@fake
|
161
|
+
username = @@fake.call(controller)
|
162
|
+
end
|
163
|
+
logger.debug("our username is: #{username}")
|
164
|
+
controller.session[@@session_username] = username
|
165
|
+
return true
|
166
|
+
end
|
167
|
+
|
168
|
+
def filter_r(controller)
|
169
|
+
logger.debug("filter of controller: #{controller}")
|
170
|
+
receipt = controller.session[:casfilterreceipt]
|
171
|
+
logger.info("receipt: #{receipt}")
|
172
|
+
valid = false
|
173
|
+
if receipt
|
174
|
+
valid = validate_receipt(receipt)
|
175
|
+
logger.info("valid receipt?: #{valid}")
|
176
|
+
else
|
177
|
+
reqticket = controller.params["ticket"]
|
178
|
+
logger.info("ticket: #{reqticket}")
|
179
|
+
if reqticket
|
180
|
+
# We temporarily allow ActionController requests to be handled concurrently.
|
181
|
+
# Otherwise proxy granting ticket callbacks from CAS wouldn't work, since
|
182
|
+
# the Rails server would be deadlocked while it waits for the CAS server to validate
|
183
|
+
# the ticket, and the CAS server waits for the Rails server to receive the PGT callback.
|
184
|
+
# Note that since the allow_concurrency option is undocumented and considered
|
185
|
+
# experimental, what we're doing here may cause unforseen problems. Beware!
|
186
|
+
ActionController::Base.allow_concurrency = true
|
187
|
+
receipt = authenticated_user(reqticket,controller)
|
188
|
+
ActionController::Base.allow_concurrency = false
|
189
|
+
|
190
|
+
logger.info("new receipt: #{receipt}")
|
191
|
+
logger.info("validate_receipt: " + validate_receipt(receipt).to_s)
|
192
|
+
if receipt && validate_receipt(receipt)
|
193
|
+
controller.session[:casfilterreceipt] = receipt
|
194
|
+
controller.session[@@session_username] = receipt.user_name
|
195
|
+
|
196
|
+
if receipt.pgt_iou
|
197
|
+
ActionController::Base.allow_concurrency = true
|
198
|
+
retrieve_url = "#{@@proxy_retrieval_url}?pgtIou=#{receipt.pgt_iou}"
|
199
|
+
logger.info("retrieving pgt from: #{retrieve_url}")
|
200
|
+
controller.session[:casfilterpgt] = CAS::ServiceTicketValidator.retrieve(retrieve_url)
|
201
|
+
ActionController::Base.allow_concurrency = false
|
202
|
+
end
|
203
|
+
|
204
|
+
valid = true
|
205
|
+
end
|
206
|
+
else
|
207
|
+
did_gateway = controller.session[:casfiltergateway]
|
208
|
+
raise CASException, "Can't redirect without login url" if !@@login_url
|
209
|
+
if did_gateway
|
210
|
+
if controller.session[@@session_username]
|
211
|
+
valid = true
|
212
|
+
else
|
213
|
+
controller.session[:casfiltergateway] = true
|
214
|
+
end
|
215
|
+
else
|
216
|
+
controller.session[:casfiltergateway] = true
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
logger.info("will send redirect #{redirect_url(controller)}") if !valid
|
221
|
+
controller.send :redirect_to,redirect_url(controller) if !valid
|
222
|
+
return valid
|
223
|
+
end
|
224
|
+
alias :filter :filter_r
|
225
|
+
|
226
|
+
|
227
|
+
def request_proxy_ticket(target_service, pgt)
|
228
|
+
r = ProxyTicketRequest.new
|
229
|
+
r.proxy_url = @@proxy_url
|
230
|
+
r.target_service = escape_service_uri(target_service)
|
231
|
+
r.pgt = pgt
|
232
|
+
|
233
|
+
raise "Cannot request a proxy ticket for service #{r.target_service} because no proxy granting ticket (PGT) has been set." unless r.pgt
|
234
|
+
|
235
|
+
logger.info("requesting proxy ticket for service #{r.target_service} with pgt #{pgt}")
|
236
|
+
r.request
|
237
|
+
|
238
|
+
if r.proxy_ticket
|
239
|
+
logger.info("got proxy ticket #{r.proxy_ticket} for service #{r.target_service}")
|
240
|
+
else
|
241
|
+
logger.warn("did not receive a proxy ticket for service #{r.target_service}!")
|
242
|
+
end
|
243
|
+
|
244
|
+
return r
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
private
|
249
|
+
def self.validate_receipt(receipt)
|
250
|
+
if receipt
|
251
|
+
logger.debug "authorized proxies: #{@@authorized_proxies.inspect}"
|
252
|
+
logger.debug "proxying service: #{receipt.proxying_service.inspect}"
|
253
|
+
end
|
254
|
+
|
255
|
+
valid = receipt && !(@@renew && !receipt.primary_authentication?)
|
256
|
+
|
257
|
+
if @@authorized_proxies and !@@authorized_proxies.empty?
|
258
|
+
valid = valid && !(receipt.proxied? && !@@authorized_proxies.include?(receipt.proxying_service))
|
259
|
+
end
|
260
|
+
|
261
|
+
return valid
|
262
|
+
end
|
263
|
+
|
264
|
+
def self.authenticated_user(tick, controller)
|
265
|
+
pv = ProxyTicketValidator.new
|
266
|
+
pv.validate_url = @@validate_url
|
267
|
+
pv.service_ticket = tick
|
268
|
+
pv.service = service_url(controller)
|
269
|
+
pv.renew = @@renew
|
270
|
+
pv.proxy_callback_url = @@proxy_callback_url
|
271
|
+
receipt = nil
|
272
|
+
logger.debug("pv: #{pv.inspect}")
|
273
|
+
begin
|
274
|
+
receipt = Receipt.new(pv)
|
275
|
+
rescue AuthenticationException=>auth
|
276
|
+
logger.warn("filter: had an authentication-exception #{auth}")
|
277
|
+
end
|
278
|
+
receipt
|
279
|
+
end
|
280
|
+
def self.service_url(controller)
|
281
|
+
before = @@service_url || guess_service(controller)
|
282
|
+
logger.debug("before: #{before}")
|
283
|
+
after = escape_service_uri(before)
|
284
|
+
logger.debug("after: #{after}")
|
285
|
+
after
|
286
|
+
end
|
287
|
+
def self.redirect_url(controller,url=@@login_url)
|
288
|
+
"#{url}?service=#{service_url(controller)}" + ((@@renew)? "&renew=true":"") + ((@@gateway)? "&gateway=true":"") + ((@@query_string.nil?)? "" : "&"+(@@query_string.collect { |k,v| "#{k}=#{v}"}.join("&")))
|
289
|
+
end
|
290
|
+
def self.guess_service(controller)
|
291
|
+
# we're assuming that controller.params[:service] is url-encoded!
|
292
|
+
return controller.params[:service] if controller.params.include? :service
|
293
|
+
|
294
|
+
req = controller.request
|
295
|
+
parms = controller.params.dup
|
296
|
+
parms.delete("ticket")
|
297
|
+
query = (parms.collect {|key, val| "#{key}=#{val}"}).join("&")
|
298
|
+
query = "?" + query unless query.empty?
|
299
|
+
"#{req.protocol}#{@@server_name}#{req.request_uri.split(/\?/)[0]}#{query}"
|
300
|
+
end
|
301
|
+
def self.escape_service_uri(uri)
|
302
|
+
URI.encode(uri, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]", false, 'U').freeze)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
class ActionController::AbstractRequest
|
308
|
+
def username
|
309
|
+
session[CAS::Filter.session_username]
|
310
|
+
end
|
311
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
|
3
|
+
# Controller that responds to proxy generating ticket callbacks from the CAS server and allows
|
4
|
+
# for retrieval of those PGTs.
|
5
|
+
class CasProxyCallbackController < ActionController::Base
|
6
|
+
|
7
|
+
# Receives a proxy granting ticket from the CAS server and stores it in the database.
|
8
|
+
# Note that this action should ALWAYS be called via https, otherwise you have a gaping security hole.
|
9
|
+
# In fact, the JA-SIG implementation of the CAS server will refuse to send PGTs to non-https URLs.
|
10
|
+
def receive_pgt
|
11
|
+
render_error "PGTs can be received only via HTTPS or local connections." and return unless
|
12
|
+
request.ssl? or request.env['REMOTE_HOST'] == "127.0.0.1"
|
13
|
+
|
14
|
+
pgtIou = params['pgtIou']
|
15
|
+
pgtId = params['pgtId']
|
16
|
+
|
17
|
+
# We need to render a response with HTTP status code 200 when no pgtIou/pgtId is specified because CAS seems first
|
18
|
+
# call the action without any parameters (maybe to check if the server responds correctly) and only then again,
|
19
|
+
# this time with the required params.
|
20
|
+
render :text => "Okay, the server is up, but please specify a pgtIou and pgtId." and return unless pgtIou and pgtId
|
21
|
+
|
22
|
+
# TODO: pstore contents should probably be encrypted...
|
23
|
+
pstore = open_pstore
|
24
|
+
|
25
|
+
pstore.transaction do
|
26
|
+
pstore[pgtIou] = pgtId
|
27
|
+
end
|
28
|
+
|
29
|
+
render :text => "PGT received. Thank you!" and return
|
30
|
+
end
|
31
|
+
|
32
|
+
# Retreives a proxy granting ticket, sends it to output, and deletes the pgt from session storage.
|
33
|
+
# Note that this action should ALWAYS be called via https, otherwise you have a gaping security hole --
|
34
|
+
# in fact, the action will not work if the request is not made via SSL or is not local (we allow for local
|
35
|
+
# non-SSL requests since this allows for the use of reverse HTTPS proxies like Pound).
|
36
|
+
def retrieve_pgt
|
37
|
+
render_error "You can only retrieve PGTs via HTTPS or local connections." and return unless
|
38
|
+
request.ssl? or request.env['REMOTE_HOST'] == "127.0.0.1"
|
39
|
+
|
40
|
+
pgtIou = params['pgtIou']
|
41
|
+
|
42
|
+
render_error "No pgtIou specified. Cannot retreive the pgtId." and return unless pgtIou
|
43
|
+
|
44
|
+
pstore = open_pstore
|
45
|
+
|
46
|
+
pgt = nil
|
47
|
+
pstore.transaction do
|
48
|
+
pgt = pstore[pgtIou]
|
49
|
+
end
|
50
|
+
|
51
|
+
if not pgt
|
52
|
+
render_error "Invalid pgtIou specified. Perhaps this pgt has already been retrieved?" and return
|
53
|
+
end
|
54
|
+
|
55
|
+
render :text => pgt
|
56
|
+
|
57
|
+
# TODO: need to periodically clean the storage, otherwise it will just keep growing
|
58
|
+
pstore.transaction do
|
59
|
+
pstore.delete pgtIou
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def render_error(msg)
|
65
|
+
# Note that the error messages are mostly just for debugging, since the CAS server never reads them.
|
66
|
+
render :text => msg, :status => 500
|
67
|
+
end
|
68
|
+
|
69
|
+
def open_pstore
|
70
|
+
PStore.new("#{RAILS_ROOT}/tmp/cas_pgt.pstore")
|
71
|
+
end
|
72
|
+
end
|
metadata
CHANGED
@@ -3,7 +3,7 @@ rubygems_version: 0.8.11
|
|
3
3
|
specification_version: 1
|
4
4
|
name: rubycas-client
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
6
|
+
version: 0.10.0
|
7
7
|
date: 2006-09-07 00:00:00 -04:00
|
8
8
|
summary: Client library for the CAS single-sign-on protocol.
|
9
9
|
require_paths:
|
@@ -12,7 +12,7 @@ email: matt@roughest.net
|
|
12
12
|
homepage: http://rubycas-client.rubyforge.org
|
13
13
|
rubyforge_project: rubycas-client
|
14
14
|
description: RubyCAS-Client is a Ruby client library for Yale's Central Authentication Service (CAS) single-sign-on protocol for web-based applications.
|
15
|
-
autorequire:
|
15
|
+
autorequire:
|
16
16
|
default_executable:
|
17
17
|
bindir: bin
|
18
18
|
has_rdoc: true
|
@@ -29,8 +29,13 @@ authors:
|
|
29
29
|
- Matt Zukowski
|
30
30
|
- Matt Walker
|
31
31
|
files:
|
32
|
-
-
|
32
|
+
- install.rb
|
33
|
+
- init.rb
|
34
|
+
- lib/cas_proxy_callback_controller.rb
|
35
|
+
- lib/cas.rb
|
36
|
+
- lib/cas_auth.rb
|
33
37
|
- LICENSE
|
38
|
+
- Rakefile
|
34
39
|
- README
|
35
40
|
test_files: []
|
36
41
|
|
data/lib/cas-client.rb
DELETED
@@ -1,289 +0,0 @@
|
|
1
|
-
# RubyCAS-Client is a Ruby client library for Yale's Central Authentication Service (CAS) protocol.
|
2
|
-
#
|
3
|
-
# Author:: Matt Walker, with modification and docs by Matt Zukowski
|
4
|
-
# Copyright:: Copyright (c) retained by the authors
|
5
|
-
# License:: GNU Lesser General Public License v2.1 (LGPL 2.1)
|
6
|
-
# Website:: http://rubyforge.org/projects/rubycas-client
|
7
|
-
#
|
8
|
-
# This program is free software; you can redistribute it and/or
|
9
|
-
# modify it under the terms of the GNU General Public License
|
10
|
-
# as published by the Free Software Foundation; either version 2
|
11
|
-
# of the License, or (at your option) any later version.
|
12
|
-
#
|
13
|
-
# This program is distributed in the hope that it will be useful,
|
14
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
15
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
16
|
-
# GNU General Public License for more details.
|
17
|
-
#
|
18
|
-
# You should have received a copy of the GNU General Public License
|
19
|
-
# along with this program; if not, write to the Free Software
|
20
|
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
21
|
-
|
22
|
-
|
23
|
-
require 'net/https'
|
24
|
-
require 'rexml/document'
|
25
|
-
|
26
|
-
require 'rubygems'
|
27
|
-
require_gem 'actionpack'
|
28
|
-
|
29
|
-
# We must override redirect_to in ActionController::Base to allow this class to
|
30
|
-
# redirect the user to the CAS server for login, since redirect_to is not public by default.
|
31
|
-
class ActionController::Base; public :redirect_to; end
|
32
|
-
|
33
|
-
# Module containing the CASFilter and associated constants. This particular
|
34
|
-
# version is designed to be used with Rails, but you can imagine expanding it
|
35
|
-
# to work in other settings as well. If you would like to override the default
|
36
|
-
# values of @@cas_server_host or @@cas_server_port, you may do so as follows:
|
37
|
-
#
|
38
|
-
# require 'cas'
|
39
|
-
#
|
40
|
-
# CAS.cas_server_host = 'netid.tamu.edu'
|
41
|
-
# CAS.cas_server_port = 443
|
42
|
-
#
|
43
|
-
# By default, the filter will store the authenticated username in session[:user]. You can
|
44
|
-
# modify this behaviour to have session[:user] contain something like your User ActiveRecord object by
|
45
|
-
# modifying various callbacks like so (see the documentation for each callback method for details):
|
46
|
-
#
|
47
|
-
# module CAS
|
48
|
-
# def self.load_user_data(username, cas_payload)
|
49
|
-
# user = User.find_by_username(username)
|
50
|
-
# user.last_login = Time.now
|
51
|
-
# return user
|
52
|
-
# end
|
53
|
-
#
|
54
|
-
# def self.save_user_data(user)
|
55
|
-
# user.save
|
56
|
-
# end
|
57
|
-
# end
|
58
|
-
#
|
59
|
-
# Author:: Matt Walker, with modification and docs by Matt Zukowski
|
60
|
-
# Acknowledgements:: James Smith provided me with my first Ruby CAS client, on which this code is based.
|
61
|
-
module CAS
|
62
|
-
# The hostname of the CAS server to authenticate against.
|
63
|
-
@@cas_server_host = 'login.mycompany.com'
|
64
|
-
|
65
|
-
# The port the CAS server is running on.
|
66
|
-
@@cas_server_port = 443
|
67
|
-
|
68
|
-
# Normally, CAS authentication is only done once -- during the first filter call. After that the user's
|
69
|
-
# username is stored in the local session and the CAS server is not contacted again until the local (i.e. Rails)
|
70
|
-
# session expires. By setting this to a value other than nil, the user will be re-authenticated with CAS
|
71
|
-
# every @@refresh_ticket_interval minutes. If you set this to 0, the CAS ticket will be re-validated on every
|
72
|
-
# request.
|
73
|
-
@@refresh_ticket_interval = nil
|
74
|
-
|
75
|
-
# The class for performing authentication filtering in Rails. The most
|
76
|
-
# important method is +filter+, which is static (thus requiring assisting
|
77
|
-
# methods to be static as well). This version is simplistic, but you can
|
78
|
-
# imagine extending it to allow gatewaying or authentication by proxy.
|
79
|
-
class CASFilter
|
80
|
-
# Filtering method specific to Rails. In particular, it is meant to be
|
81
|
-
# used with Rails' +before_filter+ method to add CAS authentication to
|
82
|
-
# one or more actions, as in the following example:
|
83
|
-
#
|
84
|
-
# require 'cas'
|
85
|
-
#
|
86
|
-
# class AdminController < ApplicationController
|
87
|
-
# before_filter CAS::CASFilter, :except => :logout
|
88
|
-
# # ...
|
89
|
-
# end
|
90
|
-
#
|
91
|
-
# When a user logs in, their user object is stashed in the session.
|
92
|
-
# This indicates to all future requests that the user is authenticated.
|
93
|
-
# If there is no user object in the session, the user must provide a
|
94
|
-
# valid CAS ticket to login. If they have no ticket, they are
|
95
|
-
# redirected to the CAS server to get one. If they do have a ticket,
|
96
|
-
# it is validated with the server before creating their user object in
|
97
|
-
# the session (and possibly database).
|
98
|
-
#
|
99
|
-
# Inputs:
|
100
|
-
# [controller] The ActionController performing filtering. If its session contains a user object, the user must have successfully authenticated against CAS.
|
101
|
-
#
|
102
|
-
# Returns a boolean: did user successfully authenticate?
|
103
|
-
def self.filter(controller)
|
104
|
-
RAILS_DEFAULT_LOGGER.info "Starting CAS filter..."
|
105
|
-
|
106
|
-
# If we have a user, a successful login was made for this session
|
107
|
-
if (!controller.session[:user].nil?)
|
108
|
-
# If the local session auth info is older than @@refresh_ticket_interval, then re-submit the ticket for validation
|
109
|
-
# to ensure that the ticket hasn't expired on the CAS end
|
110
|
-
cas_ticket_timestamp = controller.session[:cas_ticket_timestamp]
|
111
|
-
|
112
|
-
if (cas_ticket_timestamp and cas_ticket_timestamp.kind_of? Time)
|
113
|
-
time_elapsed = Time.now - cas_ticket_timestamp
|
114
|
-
|
115
|
-
if time_elapsed < (CAS.refresh_ticket_interval * 60)
|
116
|
-
return true # local session info isn't expired so we're okay
|
117
|
-
end
|
118
|
-
end
|
119
|
-
# local session info is expired so we continue with full ticket validation...
|
120
|
-
end
|
121
|
-
|
122
|
-
RAILS_DEFAULT_LOGGER.debug "No user session present, begin CAS authenticatoin process..."
|
123
|
-
|
124
|
-
# Otherwise, we require a ticket to authenticate the user
|
125
|
-
service = controller.url_for()
|
126
|
-
ticket = controller.params[:ticket]
|
127
|
-
|
128
|
-
if ticket.nil? || ticket == ""
|
129
|
-
self.redirect_to_login(controller, service)
|
130
|
-
return false
|
131
|
-
end
|
132
|
-
|
133
|
-
cas_payload = validate_ticket(service, ticket)
|
134
|
-
|
135
|
-
#TODO: redirect to login with appropriate error message (i.e. "your session has expried" or "invalid response from cas", etc.)
|
136
|
-
if cas_payload.nil? || cas_payload.length != 1
|
137
|
-
self.redirect_to_login(controller, service)
|
138
|
-
return false
|
139
|
-
end
|
140
|
-
|
141
|
-
username = cas_payload[:username]
|
142
|
-
|
143
|
-
user = CAS.load_user_data(username, cas_payload)
|
144
|
-
CAS.save_user_data(user)
|
145
|
-
|
146
|
-
controller.session[:user] = user
|
147
|
-
controller.session[:cas_ticket_timestamp] = Time.now
|
148
|
-
return true
|
149
|
-
end
|
150
|
-
|
151
|
-
private
|
152
|
-
|
153
|
-
# Validates a CAS ticket with the server.
|
154
|
-
#
|
155
|
-
# Inputs:
|
156
|
-
# [service] The URL of the calling service.
|
157
|
-
# [ticket] The CAS ticket returned by the server in the URL.
|
158
|
-
#
|
159
|
-
# Returns an array: [ NetID, UIN ]
|
160
|
-
def self.validate_ticket(service, ticket)
|
161
|
-
RAILS_DEFAULT_LOGGER.debug "Validating ticket #{ticket}..."
|
162
|
-
|
163
|
-
http = Net::HTTP.new(CAS.cas_server_host, CAS.cas_server_port)
|
164
|
-
http.use_ssl = true
|
165
|
-
page = http.get("/cas/serviceValidate?service=#{service}&ticket=#{ticket}").body
|
166
|
-
|
167
|
-
RAILS_DEFAULT_LOGGER.debug "Got response:\n"+page
|
168
|
-
|
169
|
-
# Parse XML document returned by CAS server
|
170
|
-
doc = REXML::Document.new(page)
|
171
|
-
return unless REXML::XPath.first(doc, 'cas:serviceResponse/cas:authenticationFailure',
|
172
|
-
'cas:serviceResponse' => 'http://www.yale.edu/tp/cas').nil?
|
173
|
-
return if REXML::XPath.first(doc, 'cas:serviceResponse/cas:authenticationSuccess',
|
174
|
-
'cas:serviceResponse' => 'http://www.yale.edu/tp/cas').nil?
|
175
|
-
|
176
|
-
cas_payload = CAS.process_cas_xml(doc)
|
177
|
-
|
178
|
-
return cas_payload
|
179
|
-
end
|
180
|
-
|
181
|
-
def self.redirect_to_login(controller, service)
|
182
|
-
RAILS_DEFAULT_LOGGER.debug "Redirecting to CAS login..."
|
183
|
-
controller.redirect_to "https://#{CAS.cas_server_host}:#{CAS.cas_server_port}/cas/login?service=#{service}"
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
# The following callbacks are meant to allow you to inject your business logic into the CAS filter
|
188
|
-
# without the need to modify the original code.
|
189
|
-
|
190
|
-
# This method is called after we've obtained the authenticated username from CAS.
|
191
|
-
# It takes the username and returns some data based on that username. The data will be stored
|
192
|
-
# under session[:user]. Here you may want to load your User model. i.e., something like this:
|
193
|
-
#
|
194
|
-
# module CAS
|
195
|
-
# def self.load_user_data(username, cas_payload)
|
196
|
-
# return User.find_by_username(username)
|
197
|
-
# end
|
198
|
-
# end
|
199
|
-
#
|
200
|
-
# The method also takes the cas_payload object containing data returned from CAS. You can use
|
201
|
-
# this to set some additional data for your user object. By default, however, cas_payload doesn't
|
202
|
-
# contain anything interesting -- only an array with the username. You can modify this by changing
|
203
|
-
# the behaviour or CAS#process_cas_xml
|
204
|
-
#
|
205
|
-
def self.load_user_data(username, cas_payload)
|
206
|
-
user = User.find_by_username(username)
|
207
|
-
if user.nil?
|
208
|
-
user = User.new(:username => username)
|
209
|
-
else
|
210
|
-
user.update_from_directory
|
211
|
-
end
|
212
|
-
|
213
|
-
user.last_login = Time.now
|
214
|
-
|
215
|
-
return user
|
216
|
-
end
|
217
|
-
|
218
|
-
# This method is called after load_user_data has done its business and returned the user object.
|
219
|
-
# Here you will probably want to call user.save (or something similar) to commit any changes
|
220
|
-
# made to the user object during authentication.
|
221
|
-
#
|
222
|
-
# Example:
|
223
|
-
#
|
224
|
-
# module CAS
|
225
|
-
# def self.load_user_data(user)
|
226
|
-
# user.save
|
227
|
-
# end
|
228
|
-
# end
|
229
|
-
#
|
230
|
-
def self.save_user_data(user)
|
231
|
-
user.save
|
232
|
-
end
|
233
|
-
|
234
|
-
# This method is called to parse the REXML doc returned by a CAS ticket validation request.
|
235
|
-
# By default, the method returns a hash with a :username key containing the authenticated
|
236
|
-
# username. However if your CAS server returns other information in its response, you may want
|
237
|
-
# to modify this to have the cas_payload hash include other data (cas_payload is later fed
|
238
|
-
# into the CAS#load_user_data callback).
|
239
|
-
# Note that whatever you do, you almost definetly should return a hash with a :username key
|
240
|
-
# since load_user_data expects its first parameter to be a username derived from this.
|
241
|
-
#
|
242
|
-
# Example:
|
243
|
-
#
|
244
|
-
# module CAS
|
245
|
-
# def self.process_cas_xml(doc)
|
246
|
-
# cas_payload = {}
|
247
|
-
# cas_payload[:username] = REXML::XPath.first(doc,
|
248
|
-
# 'cas:serviceResponse/cas:authenticationSuccess/cas:user',
|
249
|
-
# 'cas:serviceResponse' => 'http://www.yale.edu/tp/cas').get_text.value
|
250
|
-
# end
|
251
|
-
# end
|
252
|
-
#
|
253
|
-
def self.process_cas_xml(doc)
|
254
|
-
cas_payload = {}
|
255
|
-
cas_payload[:username] = REXML::XPath.first(doc,
|
256
|
-
'cas:serviceResponse/cas:authenticationSuccess/cas:user',
|
257
|
-
'cas:serviceResponse' => 'http://www.yale.edu/tp/cas').get_text.value
|
258
|
-
#cas_payload << REXML::XPath.first(doc,
|
259
|
-
# 'cas:serviceResponse/cas:authenticationSuccess/cas:UIN',
|
260
|
-
# 'cas:serviceResponse' => 'http://www.yale.edu/tp/cas').get_text.value
|
261
|
-
|
262
|
-
return cas_payload
|
263
|
-
end
|
264
|
-
|
265
|
-
# Returns the full URI to the CAS logout page.
|
266
|
-
# +service+ can be a full URI where the user should be re-directed after logging out and logging in again.
|
267
|
-
# (However it is up to your CAS server's implementation whether to use this parameter for anything)
|
268
|
-
def self.cas_logout_uri(service = nil)
|
269
|
-
"https://#{CAS.cas_server_host}:#{CAS.cas_server_port}/cas/logout?service=#{service}"
|
270
|
-
end
|
271
|
-
|
272
|
-
# Setter for @@cas_server_host.
|
273
|
-
def self.cas_server_host; @@cas_server_host; end
|
274
|
-
|
275
|
-
# Setter for @@cas_server_port.
|
276
|
-
def self.cas_server_port; @@cas_server_port; end
|
277
|
-
|
278
|
-
# Setter for @@refresh_ticket_interval.
|
279
|
-
def self.refresh_ticket_interval; @@refresh_ticket_interval; end
|
280
|
-
|
281
|
-
# Getter for @@cas_server_host.
|
282
|
-
def self.cas_server_host=(url); @@cas_server_host = url; end
|
283
|
-
|
284
|
-
# Getter for @@cas_server_port.
|
285
|
-
def self.cas_server_port=(port); @@cas_server_port = port; end
|
286
|
-
|
287
|
-
# Getter for @@refresh_ticket_interval.
|
288
|
-
def self.refresh_ticket_interval=(interval); @@refresh_ticket_interval = interval; end
|
289
|
-
end
|