lastobelus-rubycas-client 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,230 @@
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, :service_url
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
+ @load_ticket_url = conf[:load_ticket_url]
26
+
27
+ @username_session_key = conf[:username_session_key] || :cas_user
28
+ @extra_attributes_session_key = conf[:extra_attributes_session_key] || :cas_extra_attributes
29
+
30
+ @log = CASClient::LoggerWrapper.new
31
+ @log.set_real_logger(conf[:logger]) if conf[:logger]
32
+ end
33
+
34
+ def login_url
35
+ @login_url || (cas_base_url + "/login")
36
+ end
37
+
38
+ def validate_url
39
+ @validate_url || (cas_base_url + "/proxyValidate")
40
+ end
41
+
42
+ # calls the loadTicket service of cas server to load a TGT
43
+ # (retrived by Rest for example) into the browser cookie. This allows an
44
+ # implementation of autologin on signup:
45
+ # 1. create user
46
+ # 2. cas_client.get_ticket_granting_ticket_resource(...credentials...)
47
+ # 3. redirect to redirect_to load_ticket_url, passing service if you don't
48
+ # have one set globally, and passing the ticket from get_ticket_granting_ticket_resource
49
+
50
+ def load_ticket_url(ticket_id, l_service_url = nil, back_url = nil)
51
+ url = @load_ticket_url || (cas_base_url + "/loadTicket")
52
+ l_service_url ||= self.service_url
53
+ uri = URI.parse(url)
54
+ h = uri.query ? query_to_hash(uri.query) : {}
55
+ h['tgt'] = ticket_id.to_s
56
+ h['service'] = l_service_url.to_s if l_service_url
57
+ h['url'] = back_url.to_s if back_url
58
+ uri.query = hash_to_query(h)
59
+ uri.to_s
60
+ end
61
+
62
+
63
+ # Returns the CAS server's logout url.
64
+ #
65
+ # If a logout_url has not been explicitly configured,
66
+ # the default is cas_base_url + "/logout".
67
+ #
68
+ # service_url:: Set this if you want the user to be
69
+ # able to immediately log back in. Generally
70
+ # you'll want to use something like <tt>request.referer</tt>.
71
+ # Note that this only works with RubyCAS-Server.
72
+ # back_url:: This satisfies section 2.3.1 of the CAS protocol spec.
73
+ # See http://www.ja-sig.org/products/cas/overview/protocol
74
+ def logout_url(l_service_url = nil, back_url = nil)
75
+ url = @logout_url || (cas_base_url + "/logout")
76
+ l_service_url ||= self.service_url
77
+ if l_service_url || back_url
78
+ uri = URI.parse(url)
79
+ h = uri.query ? query_to_hash(uri.query) : {}
80
+ h['service'] = l_service_url if l_service_url
81
+ h['url'] = back_url if back_url
82
+ uri.query = hash_to_query(h)
83
+ uri.to_s
84
+ else
85
+ url
86
+ end
87
+ end
88
+
89
+ def proxy_url
90
+ @proxy_url || (cas_base_url + "/proxy")
91
+ end
92
+
93
+ def validate_service_ticket(st)
94
+ uri = URI.parse(validate_url)
95
+ h = uri.query ? query_to_hash(uri.query) : {}
96
+ h['service'] = st.service
97
+ h['ticket'] = st.ticket
98
+ h['renew'] = 1 if st.renew
99
+ h['pgtUrl'] = proxy_callback_url if proxy_callback_url
100
+ uri.query = hash_to_query(h)
101
+
102
+ st.response = request_cas_response(uri, ValidationResponse)
103
+
104
+ return st
105
+ end
106
+ alias validate_proxy_ticket validate_service_ticket
107
+
108
+ # Requests a login using the given credentials for the given service;
109
+ # returns a LoginResponse object.
110
+ def login_to_service(credentials, service)
111
+ lt = request_login_ticket
112
+
113
+ data = credentials.merge(
114
+ :lt => lt,
115
+ :service => service
116
+ )
117
+
118
+ res = submit_data_to_cas(login_url, data)
119
+ CASClient::LoginResponse.new(res)
120
+ end
121
+
122
+ def http_connection(uri)
123
+ https = Net::HTTP.new(uri.host, uri.port)
124
+ https.use_ssl = (uri.scheme == 'https')
125
+ https
126
+ end
127
+
128
+ # Requests a login ticket from the CAS server for use in a login request;
129
+ # returns a LoginTicket object.
130
+ #
131
+ # This only works with RubyCAS-Server, since obtaining login
132
+ # tickets in this manner is not part of the official CAS spec.
133
+ def request_login_ticket
134
+ uri = URI.parse(login_url+'Ticket')
135
+ https = http_connection(uri)
136
+ res = https.post(uri.path, ';')
137
+
138
+ raise CASException, res.body unless res.kind_of? Net::HTTPSuccess
139
+
140
+ res.body.strip
141
+ end
142
+
143
+ # Requests a proxy ticket from the CAS server for the given service
144
+ # using the given pgt (proxy granting ticket); returns a ProxyTicket
145
+ # object.
146
+ #
147
+ # The pgt required to request a proxy ticket is obtained as part of
148
+ # a ValidationResponse.
149
+ def request_proxy_ticket(pgt, target_service)
150
+ uri = URI.parse(proxy_url)
151
+ h = uri.query ? query_to_hash(uri.query) : {}
152
+ h['pgt'] = pgt.ticket
153
+ h['targetService'] = target_service
154
+ uri.query = hash_to_query(h)
155
+
156
+ pr = request_cas_response(uri, ProxyResponse)
157
+
158
+ pt = ProxyTicket.new(pr.proxy_ticket, target_service)
159
+ pt.response = pr
160
+
161
+ return pt
162
+ end
163
+
164
+ def retrieve_proxy_granting_ticket(pgt_iou)
165
+ uri = URI.parse(proxy_retrieval_url)
166
+ uri.query = (uri.query ? uri.query + "&" : "") + "pgtIou=#{CGI.escape(pgt_iou)}"
167
+ retrieve_url = uri.to_s
168
+
169
+ log.debug "Retrieving PGT for PGT IOU #{pgt_iou.inspect} from #{retrieve_url.inspect}"
170
+
171
+ uri = URI.parse(uri) unless uri.kind_of? URI
172
+ https = http_connection(uri)
173
+ res = https.start do |conn|
174
+ conn.get("#{uri.path}?#{uri.query}")
175
+ end
176
+
177
+
178
+ raise CASException, res.body unless res.kind_of? Net::HTTPSuccess
179
+
180
+ ProxyGrantingTicket.new(res.body.strip, pgt_iou)
181
+ end
182
+
183
+ def add_service_to_login_url(service_url)
184
+ uri = URI.parse(login_url)
185
+ uri.query = (uri.query ? uri.query + "&" : "") + "service=#{CGI.escape(service_url)}"
186
+ uri.to_s
187
+ end
188
+
189
+ private
190
+ # Fetches a CAS response of the given type from the given URI.
191
+ # Type should be either ValidationResponse or ProxyResponse.
192
+ def request_cas_response(uri, type)
193
+ log.debug "Requesting CAS response form URI #{uri.inspect}"
194
+
195
+ uri = URI.parse(uri) unless uri.kind_of? URI
196
+ https = http_connection(uri)
197
+ raw_res = https.start do |conn|
198
+ conn.get("#{uri.path}?#{uri.query}")
199
+ end
200
+
201
+ #TODO: check to make sure that response code is 200 and handle errors otherwise
202
+
203
+ log.debug "CAS Responded with #{raw_res.inspect}:\n#{raw_res.body}"
204
+
205
+ type.new(raw_res.body)
206
+ end
207
+
208
+ # Submits some data to the given URI and returns a Net::HTTPResponse.
209
+ def submit_data_to_cas(uri, data, delim=';')
210
+ uri = URI.parse(uri) unless uri.kind_of? URI
211
+ req = Net::HTTP::Post.new(uri.path)
212
+ req.set_form_data(data, delim)
213
+ https = http_connection(uri)
214
+ https.start {|conn| conn.request(req) }
215
+ end
216
+
217
+ def query_to_hash(query)
218
+ CGI.parse(query)
219
+ end
220
+
221
+ def hash_to_query(hash)
222
+ pairs = []
223
+ hash.each do |k, vals|
224
+ vals = [vals] unless vals.kind_of? Array
225
+ vals.each {|v| pairs << "#{CGI.escape(k)}=#{CGI.escape(v)}"}
226
+ end
227
+ pairs.join("&")
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,107 @@
1
+ module CASClient
2
+ module Frameworks
3
+ module Merb
4
+ module Filter
5
+ attr_reader :client
6
+
7
+
8
+ def cas_filter
9
+ @client ||= CASClient::Client.new(CASClient::Frameworks::Merb::Config.config)
10
+
11
+ service_ticket = read_ticket(self)
12
+
13
+ cas_login_url = client.add_service_to_login_url(read_service_url(self))
14
+
15
+ last_service_ticket = session[:cas_last_valid_ticket]
16
+ if (service_ticket &&
17
+ last_service_ticket &&
18
+ last_service_ticket.ticket == service_ticket.ticket &&
19
+ last_service_ticket.service == service_ticket.service)
20
+ # warn() rather than info() because we really shouldn't be re-validating the same ticket.
21
+ # The only time when this is acceptable is if the user manually does a refresh and the ticket
22
+ # happens to be in the URL.
23
+ log.warn("Reusing previously validated ticket since the new ticket and service are the same.")
24
+ service_ticket = last_service_ticket
25
+ elsif last_service_ticket &&
26
+ !config[:authenticate_on_every_request] &&
27
+ session[client.username_session_key]
28
+ # Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
29
+ # previously authenticated for this service). This is to prevent redirection to the CAS server on every
30
+ # request.
31
+ # This behaviour can be disabled (so that every request is routed through the CAS server) by setting
32
+ # the :authenticate_on_every_request config option to false.
33
+ log.debug "Existing local CAS session detected for #{session[client.username_session_key].inspect}. "+
34
+ "Previous ticket #{last_service_ticket.ticket.inspect} will be re-used."
35
+ service_ticket = last_service_ticket
36
+ end
37
+
38
+ if service_ticket
39
+ client.validate_service_ticket(service_ticket) unless service_ticket.has_been_validated?
40
+ validation_response = service_ticket.response
41
+
42
+ if service_ticket.is_valid?
43
+ log.info("Ticket #{service_ticket.inspect} for service #{service_ticket.service.inspect} " +
44
+ "belonging to user #{validation_response.user.inspect} is VALID.")
45
+
46
+ session[client.username_session_key] = validation_response.user
47
+ session[client.extra_attributes_session_key] = validation_response.extra_attributes
48
+
49
+ # Store the ticket in the session to avoid re-validating the same service
50
+ # ticket with the CAS server.
51
+ session[:cas_last_valid_ticket] = service_ticket
52
+ return true
53
+ else
54
+ log.warn("Ticket #{service_ticket.ticket.inspect} failed validation -- " +
55
+ "#{validation_response.failure_code}: #{validation_response.failure_message}")
56
+ redirect cas_login_url
57
+ return false
58
+ end
59
+ else
60
+ log.warn("No ticket -- redirecting to #{cas_login_url}")
61
+ redirect cas_login_url
62
+ return false
63
+ end
64
+ end
65
+
66
+ private
67
+ # Copied from Rails adapter
68
+ def read_ticket(controller)
69
+ ticket = controller.params[:ticket]
70
+
71
+ return nil unless ticket
72
+
73
+ log.debug("Request contains ticket #{ticket.inspect}.")
74
+
75
+ if ticket =~ /^PT-/
76
+ ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
77
+ else
78
+ ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
79
+ end
80
+ end
81
+
82
+ # Also copied from Rails adapter
83
+ def read_service_url(controller)
84
+ if config[:service_url]
85
+ log.debug("Using explicitly set service url: #{config[:service_url]}")
86
+ return config[:service_url]
87
+ end
88
+
89
+ params = controller.params.dup
90
+ params.delete(:ticket)
91
+ service_url = request.protocol + request.host / controller.url(params.to_hash.symbolize_keys!)
92
+ log.debug("Guessed service url: #{service_url.inspect}")
93
+ return service_url
94
+ end
95
+
96
+ def log
97
+ ::Merb.logger
98
+ end
99
+
100
+ def config
101
+ ::Merb::Plugins.config[:"rubycas-client"]
102
+ end
103
+
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,76 @@
1
+ require 'pstore'
2
+
3
+ # Rails 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
+ #FIXME: these checks don't work because REMOTE_HOST doesn't work consistently under all web servers (for example it doesn't work at all under mongrel)
12
+ # ... need to find a reliable way to check if the request came through from a reverse HTTPS proxy -- until then I'm disabling this check
13
+ #render_error "PGTs can be received only via HTTPS or local connections." and return unless
14
+ # request.ssl? or request.env['REMOTE_HOST'] == "127.0.0.1"
15
+
16
+ pgtIou = params['pgtIou']
17
+
18
+ # CAS Protocol spec says that the argument should be called 'pgt', but the JA-SIG CAS server seems to use pgtId.
19
+ # To accomodate this, we check for both parameters, although 'pgt' takes precedence over 'pgtId'.
20
+ pgtId = params['pgt'] || params['pgtId']
21
+
22
+ # We need to render a response with HTTP status code 200 when no pgtIou/pgtId is specified because CAS seems first
23
+ # call the action without any parameters (maybe to check if the server responds correctly)
24
+ render :text => "Okay, the server is up, but please specify a pgtIou and pgtId." and return unless pgtIou and pgtId
25
+
26
+ # TODO: pstore contents should probably be encrypted...
27
+ pstore = open_pstore
28
+
29
+ pstore.transaction do
30
+ pstore[pgtIou] = pgtId
31
+ end
32
+
33
+ render :text => "PGT received. Thank you!" and return
34
+ end
35
+
36
+ # Retreives a proxy granting ticket, sends it to output, and deletes the pgt from session storage.
37
+ # Note that this action should ALWAYS be called via https, otherwise you have a gaping security hole --
38
+ # in fact, the action will not work if the request is not made via SSL or is not local (we allow for local
39
+ # non-SSL requests since this allows for the use of reverse HTTPS proxies like Pound).
40
+ def retrieve_pgt
41
+ #render_error "You can only retrieve PGTs via HTTPS or local connections." and return unless
42
+ # request.ssl? or request.env['REMOTE_HOST'] == "127.0.0.1"
43
+
44
+ pgtIou = params['pgtIou']
45
+
46
+ render_error "No pgtIou specified. Cannot retreive the pgtId." and return unless pgtIou
47
+
48
+ pstore = open_pstore
49
+
50
+ pgt = nil
51
+ pstore.transaction do
52
+ pgt = pstore[pgtIou]
53
+ end
54
+
55
+ if not pgt
56
+ render_error "Invalid pgtIou specified. Perhaps this pgt has already been retrieved?" and return
57
+ end
58
+
59
+ render :text => pgt
60
+
61
+ # TODO: need to periodically clean the storage, otherwise it will just keep growing
62
+ pstore.transaction do
63
+ pstore.delete pgtIou
64
+ end
65
+ end
66
+
67
+ private
68
+ def render_error(msg)
69
+ # Note that the error messages are mostly just for debugging, since the CAS server never reads them.
70
+ render :text => msg, :status => 500
71
+ end
72
+
73
+ def open_pstore
74
+ PStore.new("#{RAILS_ROOT}/tmp/cas_pgt.pstore")
75
+ end
76
+ end
@@ -0,0 +1,163 @@
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
+ last_st = controller.session[:cas_last_valid_ticket]
19
+
20
+ if st &&
21
+ last_st &&
22
+ last_st.ticket == st.ticket &&
23
+ last_st.service == st.service
24
+ # warn() rather than info() because we really shouldn't be re-validating the same ticket.
25
+ # The only time when this is acceptable is if the user manually does a refresh and the ticket
26
+ # happens to be in the URL.
27
+ log.warn("Re-using previously validated ticket since the new ticket and service are the same.")
28
+ st = last_st
29
+ elsif last_st &&
30
+ !config[:authenticate_on_every_request] &&
31
+ controller.session[client.username_session_key]
32
+ # Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
33
+ # previously authenticated for this service). This is to prevent redirection to the CAS server on every
34
+ # request.
35
+ # This behaviour can be disabled (so that every request is routed through the CAS server) by setting
36
+ # the :authenticate_on_every_request config option to false.
37
+ log.debug "Existing local CAS session detected for #{controller.session[client.username_session_key].inspect}. "+
38
+ "Previous ticket #{last_st.ticket.inspect} will be re-used."
39
+ st = last_st
40
+ end
41
+
42
+ if st
43
+ client.validate_service_ticket(st) unless st.has_been_validated?
44
+ vr = st.response
45
+
46
+ if st.is_valid?
47
+ log.info("Ticket #{st.ticket.inspect} for service #{st.service.inspect} belonging to user #{vr.user.inspect} is VALID.")
48
+ controller.session[client.username_session_key] = vr.user
49
+ controller.session[client.extra_attributes_session_key] = vr.extra_attributes
50
+
51
+ # RubyCAS-Client 1.x used :casfilteruser as it's username session key,
52
+ # so we need to set this here to ensure compatibility with configurations
53
+ # built around the old client.
54
+ controller.session[:casfilteruser] = vr.user
55
+
56
+ # Store the ticket in the session to avoid re-validating the same service
57
+ # ticket with the CAS server.
58
+ controller.session[:cas_last_valid_ticket] = st
59
+
60
+ if vr.pgt_iou
61
+ log.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...")
62
+ pgt = client.retrieve_proxy_granting_ticket(vr.pgt_iou)
63
+ if pgt
64
+ log.debug("Got PGT #{pgt.ticket.inspect} for PGT IOU #{pgt.iou.inspect}. This will be stored in the session.")
65
+ controller.session[:cas_pgt] = pgt
66
+ # For backwards compatibility with RubyCAS-Client 1.x configurations...
67
+ controller.session[:casfilterpgt] = pgt
68
+ else
69
+ log.error("Failed to retrieve a PGT for PGT IOU #{vr.pgt_iou}!")
70
+ end
71
+ end
72
+
73
+ return true
74
+ else
75
+ log.warn("Ticket #{st.ticket.inspect} failed validation -- #{vr.failure_code}: #{vr.failure_message}")
76
+ redirect_to_cas_for_authentication(controller)
77
+ return false
78
+ end
79
+ else
80
+ if returning_from_gateway?(controller)
81
+ log.info "Returning from CAS gateway without authentication."
82
+
83
+ if use_gatewaying?
84
+ log.info "This CAS client is configured to use gatewaying, so we will permit the user to continue without authentication."
85
+ return true
86
+ else
87
+ log.warn "The CAS client is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!"
88
+ end
89
+ end
90
+
91
+ redirect_to_cas_for_authentication(controller)
92
+ return false
93
+ end
94
+ end
95
+
96
+ def configure(config)
97
+ @@config = config
98
+ @@config[:logger] = RAILS_DEFAULT_LOGGER unless @@config[:logger]
99
+ @@client = CASClient::Client.new(config)
100
+ @@log = client.log
101
+ end
102
+
103
+ def use_gatewaying?
104
+ @@config[:use_gatewaying]
105
+ end
106
+
107
+ def returning_from_gateway?(controller)
108
+ controller.session[:cas_sent_to_gateway]
109
+ end
110
+
111
+ def redirect_to_cas_for_authentication(controller)
112
+ service_url = read_service_url(controller)
113
+ redirect_url = client.add_service_to_login_url(service_url)
114
+
115
+ if use_gatewaying?
116
+ controller.session[:cas_sent_to_gateway] = true
117
+ redirect_url << "&gateway=true"
118
+ else
119
+ controller.session[:cas_sent_to_gateway] = false
120
+ end
121
+
122
+ log.debug("Redirecting to #{redirect_url.inspect}")
123
+ controller.send(:redirect_to, redirect_url)
124
+ end
125
+
126
+ private
127
+ def read_ticket(controller)
128
+ ticket = controller.params[:ticket]
129
+
130
+ return nil unless ticket
131
+
132
+ log.debug("Request contains ticket #{ticket.inspect}.")
133
+
134
+ if ticket =~ /^PT-/
135
+ ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
136
+ else
137
+ ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
138
+ end
139
+ end
140
+
141
+ def read_service_url(controller)
142
+ if config[:service_url]
143
+ log.debug("Using explicitly set service url: #{config[:service_url]}")
144
+ return config[:service_url]
145
+ end
146
+
147
+ params = controller.params.dup
148
+ params.delete(:ticket)
149
+ service_url = controller.url_for(params)
150
+ log.debug("Guessed service url: #{service_url.inspect}")
151
+ return service_url
152
+ end
153
+ end
154
+ end
155
+
156
+ class GatewayFilter < Filter
157
+ def self.use_gatewaying?
158
+ return true unless @@config[:use_gatewaying] == false
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end