rubycas-client 1.1.0 → 2.0.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/init.rb CHANGED
@@ -1,20 +1,6 @@
1
- require 'cas_auth'
2
- require 'cas_logger'
3
- require 'cas_proxy_callback_controller'
1
+ # This file makes it possible to install RubyCAS-Client as a Rails plugin.
4
2
 
5
- #CAS::Filter.logger = RAILS_DEFAULT_LOGGER if !RAILS_DEFAULT_LOGGER.nil?
6
- #CAS::Filter.logger = config.logger if !config.logger.nil?
3
+ $: << File.expand_path(File.dirname(__FILE__))+'/lib'
7
4
 
8
- CAS::Filter.logger = CAS::Logger.new("#{RAILS_ROOT}/log/cas_client_#{RAILS_ENV}.log")
9
- CAS::Filter.logger.formatter = CAS::Logger::Formatter.new
10
-
11
- #if RAILS_ENV == "production"
12
- # CAS::Filter.logger.level = Logger::WARN
13
- #else
14
- # CAS::Filter.logger.level = Logger::DEBUG
15
- #end
16
-
17
-
18
- #class ActionController::Base
19
- # append_before_filter CAS::Filter
20
- #end
5
+ require 'casclient'
6
+ require 'casclient/frameworks/rails/filter'
data/lib/casclient.rb ADDED
@@ -0,0 +1,79 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'net/https'
4
+ require 'rexml/document'
5
+
6
+ begin
7
+ require 'active_support'
8
+ rescue LoadError
9
+ require 'rubygems'
10
+ require 'active_support'
11
+ end
12
+
13
+ $: << File.expand_path(File.dirname(__FILE__))
14
+
15
+ module CASClient
16
+ class CASException < Exception
17
+ end
18
+
19
+ # Customized logger for the client.
20
+ # This is useful if you're trying to do logging in Rails, since Rails'
21
+ # clean_logger.rb pretty much completely breaks the base Logger class.
22
+ class Logger < ::Logger
23
+ def initialize(logdev, shift_age = 0, shift_size = 1048576)
24
+ @default_formatter = Formatter.new
25
+ super
26
+ end
27
+
28
+ def format_message(severity, datetime, progrname, msg)
29
+ (@formatter || @default_formatter).call(severity, datetime, progname, msg)
30
+ end
31
+
32
+ def break
33
+ self << $/
34
+ end
35
+
36
+ class Formatter < ::Logger::Formatter
37
+ Format = "[%s#%d] %5s -- %s: %s\n"
38
+
39
+ def call(severity, time, progname, msg)
40
+ Format % [format_datetime(time), $$, severity, progname, msg2str(msg)]
41
+ end
42
+ end
43
+ end
44
+
45
+ # Wraps a real Logger. If no real Logger is set, then this wrapper
46
+ # will quietly swallow any logging calls.
47
+ class LoggerWrapper
48
+ def initialize(real_logger=nil)
49
+ set_logger(real_logger)
50
+ end
51
+ # Assign the 'real' Logger instance that this dummy instance wraps around.
52
+ def set_real_logger(real_logger)
53
+ @real_logger = real_logger
54
+ end
55
+ # Log using the appropriate method if we have a logger
56
+ # if we dont' have a logger, gracefully ignore.
57
+ def method_missing(name, *args)
58
+ if @real_logger && @real_logger.respond_to?(name)
59
+ @real_logger.send(name, *args)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ require 'casclient/tickets'
66
+ require 'casclient/responses'
67
+ require 'casclient/client'
68
+ require 'casclient/version'
69
+
70
+ # Detect legacy configuration and show appropriate error message
71
+ module CAS
72
+ module Filter
73
+ def method_missing
74
+ $stderr.puts "Your RubyCAS-Client configuration is no longer valid."
75
+ $stderr.puts "Please see http://rubycas-client.googlecode.com/svn/trunk/rubycas-client/README.txt for information on the new configuration format."
76
+ $stderr.puts "After upgrading your configuration you should also clear your application's session store."
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,209 @@
1
+ module CASClient
2
+ # The client brokers all HTTP transactions with the CAS server.
3
+ class Client
4
+ attr_reader :cas_base_url
5
+ attr_reader :log, :username_session_key, :extra_attributes_session_key
6
+ attr_writer :login_url, :validate_url, :proxy_url, :logout_url, :service_url
7
+ attr_accessor :proxy_callback_url, :proxy_retrieval_url
8
+
9
+ def initialize(conf = nil)
10
+ configure(conf) if conf
11
+ end
12
+
13
+ def configure(conf)
14
+ raise ArgumentError, "Missing :cas_base_url parameter!" unless conf[:cas_base_url]
15
+
16
+ @cas_base_url = conf[:cas_base_url].gsub(/\/$/, '')
17
+
18
+ @login_url = conf[:login_url]
19
+ @logout_url = conf[:logout_url]
20
+ @validate_url = conf[:validate_url]
21
+ @proxy_url = conf[:proxy_url]
22
+ @service_url = conf[:service_url]
23
+ @proxy_callback_url = conf[:proxy_callback_url]
24
+ @proxy_retrieval_url = conf[:proxy_retrieval_url]
25
+
26
+ @username_session_key = conf[:username_session_key] || :cas_user
27
+ @extra_attributes_session_key = conf[:extra_attributes_session_key] || :cas_extra_attributes
28
+
29
+ @log = CASClient::LoggerWrapper.new
30
+ @log.set_real_logger(conf[:logger]) if conf[:logger]
31
+ end
32
+
33
+ def login_url
34
+ @login_url || (cas_base_url + "/login")
35
+ end
36
+
37
+ def validate_url
38
+ @validate_url || (cas_base_url + "/proxyValidate")
39
+ end
40
+
41
+ # Returns the CAS server's logout url.
42
+ #
43
+ # If a logout_url has not been explicitly configured,
44
+ # the default is cas_base_url + "/logout".
45
+ #
46
+ # service_url:: Set this if you want the user to be
47
+ # able to immediately log back in. Generally
48
+ # you'll want to use something like <tt>request.referer</tt>.
49
+ # Note that this only works with RubyCAS-Server.
50
+ # back_url:: This satisfies section 2.3.1 of the CAS protocol spec.
51
+ # See http://www.ja-sig.org/products/cas/overview/protocol
52
+ def logout_url(service_url = nil, back_url = nil)
53
+ url = @logout_url || (cas_base_url + "/logout")
54
+
55
+ if service_url || back_url
56
+ uri = URI.parse(url)
57
+ h = uri.query ? query_to_hash(uri.query) : {}
58
+ h['service'] = service_url if service_url
59
+ h['url'] = back_url if back_url
60
+ uri.query = hash_to_query(h)
61
+ uri.to_s
62
+ else
63
+ url
64
+ end
65
+ end
66
+
67
+ def proxy_url
68
+ @proxy_url || (cas_base_url + "/proxy")
69
+ end
70
+
71
+ def validate_service_ticket(st)
72
+ uri = URI.parse(validate_url)
73
+ h = uri.query ? query_to_hash(uri.query) : {}
74
+ h['service'] = st.service
75
+ h['ticket'] = st.ticket
76
+ h['renew'] = 1 if st.renew
77
+ h['pgtUrl'] = proxy_callback_url if proxy_callback_url
78
+ uri.query = hash_to_query(h)
79
+
80
+ st.response = request_cas_response(uri, ValidationResponse)
81
+
82
+ return st
83
+ end
84
+ alias validate_proxy_ticket validate_service_ticket
85
+
86
+ # Requests a login using the given credentials for the given service;
87
+ # returns a LoginResponse object.
88
+ def login_to_service(credentials, service)
89
+ lt = request_login_ticket
90
+
91
+ data = credentials.merge(
92
+ :lt => lt,
93
+ :service => service
94
+ )
95
+
96
+ res = submit_data_to_cas(login_url, data)
97
+ CASClient::LoginResponse.new(res)
98
+ end
99
+
100
+ # Requests a login ticket from the CAS server for use in a login request;
101
+ # returns a LoginTicket object.
102
+ #
103
+ # This only works with RubyCAS-Server, since obtaining login
104
+ # tickets in this manner is not part of the official CAS spec.
105
+ def request_login_ticket
106
+ uri = URI.parse(login_url+'Ticket')
107
+ https = Net::HTTP.new(uri.host, uri.port)
108
+ https.use_ssl = (uri.scheme == 'https')
109
+ res = https.post(uri.path, ';')
110
+
111
+ raise CASException, res.body unless res.kind_of? Net::HTTPSuccess
112
+
113
+ res.body.strip
114
+ end
115
+
116
+ # Requests a proxy ticket from the CAS server for the given service
117
+ # using the given pgt (proxy granting ticket); returns a ProxyTicket
118
+ # object.
119
+ #
120
+ # The pgt required to request a proxy ticket is obtained as part of
121
+ # a ValidationResponse.
122
+ def request_proxy_ticket(pgt, target_service)
123
+ uri = URI.parse(proxy_url)
124
+ h = uri.query ? query_to_hash(uri.query) : {}
125
+ h['pgt'] = pgt.ticket
126
+ h['targetService'] = target_service
127
+ uri.query = hash_to_query(h)
128
+
129
+ pr = request_cas_response(uri, ProxyResponse)
130
+
131
+ pt = ProxyTicket.new(pr.proxy_ticket, target_service)
132
+ pt.response = pr
133
+
134
+ return pt
135
+ end
136
+
137
+ def retrieve_proxy_granting_ticket(pgt_iou)
138
+ uri = URI.parse(proxy_retrieval_url)
139
+ uri.query = (uri.query ? uri.query + "&" : "") + "pgtIou=#{CGI.escape(pgt_iou)}"
140
+ retrieve_url = uri.to_s
141
+
142
+ log.debug "Retrieving PGT for PGT IOU #{pgt_iou.inspect} from #{retrieve_url.inspect}"
143
+
144
+ # https = Net::HTTP.new(uri.host, uri.port)
145
+ # https.use_ssl = (uri.scheme == 'https')
146
+ # res = https.post(uri.path, ';')
147
+ uri = URI.parse(uri) unless uri.kind_of? URI
148
+ https = Net::HTTP.new(uri.host, uri.port)
149
+ https.use_ssl = (uri.scheme == 'https')
150
+ res = https.start do |conn|
151
+ conn.get("#{uri.path}?#{uri.query}")
152
+ end
153
+
154
+
155
+ raise CASException, res.body unless res.kind_of? Net::HTTPSuccess
156
+
157
+ ProxyGrantingTicket.new(res.body.strip, pgt_iou)
158
+ end
159
+
160
+ def add_service_to_login_url(service_url)
161
+ uri = URI.parse(login_url)
162
+ uri.query = (uri.query ? uri.query + "&" : "") + "service=#{CGI.escape(service_url)}"
163
+ uri.to_s
164
+ end
165
+
166
+ private
167
+ # Fetches a CAS response of the given type from the given URI.
168
+ # Type should be either ValidationResponse or ProxyResponse.
169
+ def request_cas_response(uri, type)
170
+ log.debug "Requesting CAS response form URI #{uri.inspect}"
171
+
172
+ uri = URI.parse(uri) unless uri.kind_of? URI
173
+ https = Net::HTTP.new(uri.host, uri.port)
174
+ https.use_ssl = (uri.scheme == 'https')
175
+ raw_res = https.start do |conn|
176
+ conn.get("#{uri.path}?#{uri.query}")
177
+ end
178
+
179
+ #TODO: check to make sure that response code is 200 and handle errors otherwise
180
+
181
+ log.debug "CAS Responded with #{raw_res.inspect}:\n#{raw_res.body}"
182
+
183
+ type.new(raw_res.body)
184
+ end
185
+
186
+ # Submits some data to the given URI and returns a Net::HTTPResponse.
187
+ def submit_data_to_cas(uri, data)
188
+ uri = URI.parse(uri) unless uri.kind_of? URI
189
+ req = Net::HTTP::Post.new(uri.path)
190
+ req.set_form_data(data, ';')
191
+ https = Net::HTTP.new(uri.host, uri.port)
192
+ https.use_ssl = (uri.scheme == 'https')
193
+ https.start {|conn| conn.request(req) }
194
+ end
195
+
196
+ def query_to_hash(query)
197
+ CGI.parse(query)
198
+ end
199
+
200
+ def hash_to_query(hash)
201
+ pairs = []
202
+ hash.each do |k, vals|
203
+ vals = [vals] unless vals.kind_of? Array
204
+ vals.each {|v| pairs << "#{CGI.escape(k)}=#{CGI.escape(v)}"}
205
+ end
206
+ pairs.join("&")
207
+ end
208
+ end
209
+ end
@@ -1,6 +1,6 @@
1
1
  require 'pstore'
2
2
 
3
- # Controller that responds to proxy generating ticket callbacks from the CAS server and allows
3
+ # Rails controller that responds to proxy generating ticket callbacks from the CAS server and allows
4
4
  # for retrieval of those PGTs.
5
5
  class CasProxyCallbackController < ActionController::Base
6
6
 
@@ -0,0 +1,149 @@
1
+ module CASClient
2
+ module Frameworks
3
+ module Rails
4
+ class Filter
5
+ cattr_reader :config, :log, :client
6
+
7
+ # These are initialized when you call configure.
8
+ @@config = nil
9
+ @@client = nil
10
+ @@log = nil
11
+
12
+ class << self
13
+ def filter(controller)
14
+ raise "Cannot use the CASClient filter because it has not yet been configured." if config.nil?
15
+
16
+ st = read_ticket(controller)
17
+
18
+ lst = controller.session[:cas_last_valid_ticket]
19
+
20
+ if st && lst && lst.ticket == st.ticket && lst.service == st.service
21
+ # warn() rather than info() because we really shouldn't be re-validating the same ticket.
22
+ # The only time when this is acceptable is if the user manually does a refresh and the ticket
23
+ # happens to be in the URL.
24
+ log.warn("Reusing previously validated ticket since the new ticket and service are the same.")
25
+ st = lst
26
+ end
27
+
28
+ if st
29
+ client.validate_service_ticket(st) unless st.has_been_validated?
30
+ vr = st.response
31
+
32
+ if st.is_valid?
33
+ log.info("Ticket #{st.ticket.inspect} for service #{st.service.inspect} belonging to user #{vr.user.inspect} is VALID.")
34
+ controller.session[client.username_session_key] = vr.user
35
+ controller.session[client.extra_attributes_session_key] = vr.extra_attributes
36
+
37
+ # RubyCAS-Client 1.x used :casfilteruser as it's username session key,
38
+ # so we need to set this here to ensure compatibility with configurations
39
+ # built around the old client.
40
+ controller.session[:casfilteruser] = vr.user
41
+
42
+ # Store the ticket in the session to avoid re-validating the same service
43
+ # ticket with the CAS server.
44
+ controller.session[:cas_last_valid_ticket] = st
45
+
46
+ if vr.pgt_iou
47
+ log.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...")
48
+ pgt = client.retrieve_proxy_granting_ticket(vr.pgt_iou)
49
+ if pgt
50
+ log.debug("Got PGT #{pgt.ticket.inspect} for PGT IOU #{pgt.iou.inspect}. This will be stored in the session.")
51
+ controller.session[:cas_pgt] = pgt
52
+ # For backwards compatibility with RubyCAS-Client 1.x configurations...
53
+ controller.session[:casfilterpgt] = pgt
54
+ else
55
+ log.error("Failed to retrieve a PGT for PGT IOU #{vr.pgt_iou}!")
56
+ end
57
+ end
58
+
59
+ return true
60
+ else
61
+ log.warn("Ticket #{st.ticket.inspect} failed validation -- #{vr.failure_code}: #{vr.failure_message}")
62
+ redirect_to_cas_for_authentication(controller)
63
+ return false
64
+ end
65
+ else
66
+ if returning_from_gateway?(controller)
67
+ log.info "Returning from CAS gateway without authentication."
68
+
69
+ if use_gatewaying?
70
+ log.info "This CAS client is configured to use gatewaying, so we will permit the user to continue without authentication."
71
+ return true
72
+ else
73
+ log.warn "The CAS client is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!"
74
+ end
75
+ end
76
+
77
+ redirect_to_cas_for_authentication(controller)
78
+ return false
79
+ end
80
+ end
81
+
82
+ def configure(config)
83
+ @@config = config
84
+ @@config[:logger] = RAILS_DEFAULT_LOGGER unless @@config[:logger]
85
+ @@client = CASClient::Client.new(config)
86
+ @@log = client.log
87
+ end
88
+
89
+ def use_gatewaying?
90
+ @@config[:use_gatewaying]
91
+ end
92
+
93
+ def returning_from_gateway?(controller)
94
+ controller.session[:cas_sent_to_gateway]
95
+ end
96
+
97
+ def redirect_to_cas_for_authentication(controller)
98
+ service_url = read_service_url(controller)
99
+ redirect_url = client.add_service_to_login_url(service_url)
100
+
101
+ if use_gatewaying?
102
+ controller.session[:cas_sent_to_gateway] = true
103
+ redirect_url << "&gateway=true"
104
+ else
105
+ controller.session[:cas_sent_to_gateway] = false
106
+ end
107
+
108
+ log.debug("Redirecting to #{redirect_url.inspect}")
109
+ controller.send(:redirect_to, redirect_url)
110
+ end
111
+
112
+ private
113
+ def read_ticket(controller)
114
+ ticket = controller.params[:ticket]
115
+
116
+ return nil unless ticket
117
+
118
+ log.debug("Request contains ticket #{ticket.inspect}.")
119
+
120
+ if ticket =~ /^PT-/
121
+ ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
122
+ else
123
+ ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
124
+ end
125
+ end
126
+
127
+ def read_service_url(controller)
128
+ if config[:service_url]
129
+ log.debug("Using explicitly set service url: #{config[:service_url]}")
130
+ return config[:service_url]
131
+ end
132
+
133
+ params = controller.params.dup
134
+ params.delete(:ticket)
135
+ service_url = controller.url_for(params)
136
+ log.debug("Guessed service url: #{service_url.inspect}")
137
+ return service_url
138
+ end
139
+ end
140
+ end
141
+
142
+ class GatewayFilter < Filter
143
+ def self.use_gatewaying?
144
+ return true unless @@config[:use_gatewaying] == false
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end