rubycas-client 0.9.1 → 0.10.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.
- 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
|