rubycas-client 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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