vibes-rubycas-client 2.3.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/.rvmrc +1 -0
  2. data/.source_index +0 -0
  3. data/CHANGELOG.txt +1 -0
  4. data/Gemfile +15 -0
  5. data/Gemfile.lock +22 -0
  6. data/History.txt +192 -0
  7. data/LICENSE.txt +26 -0
  8. data/README.rdoc +321 -0
  9. data/Rakefile +53 -0
  10. data/VERSION +1 -0
  11. data/examples/merb/.gitignore +18 -0
  12. data/examples/merb/README.textile +12 -0
  13. data/examples/merb/Rakefile +35 -0
  14. data/examples/merb/merb.thor +2020 -0
  15. data/examples/merb/merb_auth_cas.rb +67 -0
  16. data/examples/merb/spec/spec_helper.rb +24 -0
  17. data/examples/rails/README +16 -0
  18. data/examples/rails/app/controllers/advanced_example_controller.rb +31 -0
  19. data/examples/rails/app/controllers/application.rb +2 -0
  20. data/examples/rails/app/controllers/simple_example_controller.rb +16 -0
  21. data/examples/rails/app/views/advanced_example/index.html.erb +13 -0
  22. data/examples/rails/app/views/advanced_example/my_account.html.erb +11 -0
  23. data/examples/rails/app/views/simple_example/index.html.erb +6 -0
  24. data/examples/rails/config/boot.rb +109 -0
  25. data/examples/rails/config/environment.rb +39 -0
  26. data/examples/rails/config/environments/development.rb +17 -0
  27. data/examples/rails/config/environments/production.rb +22 -0
  28. data/examples/rails/config/environments/test.rb +22 -0
  29. data/examples/rails/config/initializers/inflections.rb +10 -0
  30. data/examples/rails/config/initializers/mime_types.rb +5 -0
  31. data/examples/rails/config/initializers/new_rails_defaults.rb +17 -0
  32. data/examples/rails/config/routes.rb +4 -0
  33. data/examples/rails/log/development.log +946 -0
  34. data/examples/rails/log/production.log +0 -0
  35. data/examples/rails/log/server.log +0 -0
  36. data/examples/rails/log/test.log +0 -0
  37. data/examples/rails/script/about +4 -0
  38. data/examples/rails/script/console +3 -0
  39. data/examples/rails/script/server +3 -0
  40. data/lib/casclient.rb +89 -0
  41. data/lib/casclient/client.rb +271 -0
  42. data/lib/casclient/frameworks/merb/filter.rb +105 -0
  43. data/lib/casclient/frameworks/merb/strategy.rb +110 -0
  44. data/lib/casclient/frameworks/rails/cas_proxy_callback_controller.rb +76 -0
  45. data/lib/casclient/frameworks/rails/filter.rb +415 -0
  46. data/lib/casclient/responses.rb +197 -0
  47. data/lib/casclient/tickets.rb +38 -0
  48. data/lib/vibes-rubycas-client.rb +1 -0
  49. data/vibes-rubycas-client.gemspec +100 -0
  50. metadata +198 -0
File without changes
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ $LOAD_PATH.unshift "#{RAILTIES_PATH}/builtin/rails_info"
4
+ require 'commands/about'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/console'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/server'
@@ -0,0 +1,89 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'logger'
4
+ require 'net/https'
5
+ require 'rexml/document'
6
+
7
+ begin
8
+ require 'active_support'
9
+ rescue LoadError
10
+ require 'rubygems'
11
+ require 'active_support'
12
+ end
13
+
14
+ $: << File.expand_path(File.dirname(__FILE__))
15
+
16
+ module CASClient
17
+ class CASException < Exception
18
+ end
19
+
20
+ # Customized logger for the client.
21
+ # This is useful if you're trying to do logging in Rails, since Rails'
22
+ # clean_logger.rb pretty much completely breaks the base Logger class.
23
+ class Logger < ::Logger
24
+ def initialize(logdev, shift_age = 0, shift_size = 1048576)
25
+ @default_formatter = Formatter.new
26
+ super
27
+ end
28
+
29
+ def format_message(severity, datetime, progrname, msg)
30
+ (@formatter || @default_formatter).call(severity, datetime, progname, msg)
31
+ end
32
+
33
+ def break
34
+ self << $/
35
+ end
36
+
37
+ class Formatter < ::Logger::Formatter
38
+ Format = "[%s#%d] %5s -- %s: %s\n"
39
+
40
+ def call(severity, time, progname, msg)
41
+ Format % [format_datetime(time), $$, severity, progname, msg2str(msg)]
42
+ end
43
+ end
44
+ end
45
+
46
+ # Wraps a real Logger. If no real Logger is set, then this wrapper
47
+ # will quietly swallow any logging calls.
48
+ class LoggerWrapper
49
+ def initialize(real_logger=nil)
50
+ set_logger(real_logger)
51
+ end
52
+ # Assign the 'real' Logger instance that this dummy instance wraps around.
53
+ def set_real_logger(real_logger)
54
+ @real_logger = real_logger
55
+ end
56
+ # Log using the appropriate method if we have a logger
57
+ # if we dont' have a logger, gracefully ignore.
58
+ def method_missing(name, *args)
59
+ if @real_logger && @real_logger.respond_to?(name)
60
+ @real_logger.send(name, *args)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ require 'casclient/tickets'
67
+ require 'casclient/responses'
68
+ require 'casclient/client'
69
+
70
+ # Detect legacy configuration and show appropriate error message
71
+ module CAS
72
+ module Filter
73
+ class << self
74
+ def method_missing(method, *args)
75
+ $stderr.puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
76
+ $stderr.puts
77
+ $stderr.puts "WARNING: Your RubyCAS-Client configuration is no longer valid!!"
78
+ $stderr.puts
79
+ $stderr.puts "For information on the new configuration format please see: "
80
+ $stderr.puts
81
+ $stderr.puts " http://rubycas-client.googlecode.com/svn/trunk/rubycas-client/README.txt"
82
+ $stderr.puts
83
+ $stderr.puts "After upgrading your configuration you should also clear your application's session store."
84
+ $stderr.puts
85
+ $stderr.puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,271 @@
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
+ #TODO: raise error if conf contains unrecognized cas options (this would help detect user typos in the config)
15
+
16
+ raise ArgumentError, "Missing :cas_base_url parameter!" unless conf[:cas_base_url]
17
+
18
+ @cas_base_url = conf[:cas_base_url].gsub(/\/$/, '')
19
+
20
+ @login_url = conf[:login_url]
21
+ @logout_url = conf[:logout_url]
22
+ @validate_url = conf[:validate_url]
23
+ @proxy_url = conf[:proxy_url]
24
+ @service_url = conf[:service_url]
25
+ @force_ssl_verification = conf[:force_ssl_verification]
26
+ @proxy_callback_url = conf[:proxy_callback_url]
27
+ @proxy_retrieval_url = conf[:proxy_retrieval_url]
28
+
29
+ @username_session_key = conf[:username_session_key] || :cas_user
30
+ @extra_attributes_session_key = conf[:extra_attributes_session_key] || :cas_extra_attributes
31
+
32
+ @log = CASClient::LoggerWrapper.new
33
+ @log.set_real_logger(conf[:logger]) if conf[:logger]
34
+ end
35
+
36
+ def login_url
37
+ @login_url || (cas_base_url + "/login")
38
+ end
39
+
40
+ def validate_url
41
+ @validate_url || (cas_base_url + "/proxyValidate")
42
+ end
43
+
44
+ # Returns the CAS server's logout url.
45
+ #
46
+ # If a logout_url has not been explicitly configured,
47
+ # the default is cas_base_url + "/logout".
48
+ #
49
+ # destination_url:: Set this if you want the user to be
50
+ # able to immediately log back in. Generally
51
+ # you'll want to use something like <tt>request.referer</tt>.
52
+ # Note that the above behaviour describes RubyCAS-Server
53
+ # -- other CAS server implementations might use this
54
+ # parameter differently (or not at all).
55
+ # follow_url:: This satisfies section 2.3.1 of the CAS protocol spec.
56
+ # See http://www.ja-sig.org/products/cas/overview/protocol
57
+ def logout_url(destination_url = nil, follow_url = nil)
58
+ url = @logout_url || (cas_base_url + "/logout")
59
+
60
+ if destination_url
61
+ # if present, remove the 'ticket' parameter from the destination_url
62
+ duri = URI.parse(destination_url)
63
+ h = duri.query ? query_to_hash(duri.query) : {}
64
+ h.delete('ticket')
65
+ duri.query = hash_to_query(h)
66
+ destination_url = duri.to_s.gsub(/\?$/, '')
67
+ end
68
+
69
+ if destination_url || follow_url
70
+ uri = URI.parse(url)
71
+ h = uri.query ? query_to_hash(uri.query) : {}
72
+ h['destination'] = destination_url if destination_url
73
+ h['url'] = follow_url if follow_url
74
+ uri.query = hash_to_query(h)
75
+ uri.to_s
76
+ else
77
+ url
78
+ end
79
+ end
80
+
81
+ def proxy_url
82
+ @proxy_url || (cas_base_url + "/proxy")
83
+ end
84
+
85
+ def validate_service_ticket(st)
86
+ uri = URI.parse(validate_url)
87
+ h = uri.query ? query_to_hash(uri.query) : {}
88
+ h['service'] = st.service
89
+ h['ticket'] = st.ticket
90
+ h['renew'] = 1 if st.renew
91
+ h['pgtUrl'] = proxy_callback_url if proxy_callback_url
92
+ uri.query = hash_to_query(h)
93
+
94
+ st.response = request_cas_response(uri, ValidationResponse)
95
+
96
+ return st
97
+ end
98
+ alias validate_proxy_ticket validate_service_ticket
99
+
100
+ # Returns true if the configured CAS server is up and responding;
101
+ # false otherwise.
102
+ def cas_server_is_up?
103
+ uri = URI.parse(login_url)
104
+
105
+ log.debug "Checking if CAS server at URI '#{uri}' is up..."
106
+
107
+ https = Net::HTTP.new(uri.host, uri.port)
108
+ https.use_ssl = (uri.scheme == 'https')
109
+ https.verify_mode = (@force_ssl_verification ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE)
110
+
111
+ begin
112
+ raw_res = https.start do |conn|
113
+ conn.get("#{uri.path}?#{uri.query}")
114
+ end
115
+ rescue Errno::ECONNREFUSED => e
116
+ log.warn "CAS server did not respond! (#{e.inspect})"
117
+ return false
118
+ end
119
+
120
+ log.debug "CAS server responded with #{raw_res.inspect}:\n#{raw_res.body}"
121
+
122
+ return raw_res.kind_of?(Net::HTTPSuccess)
123
+ end
124
+
125
+ # Requests a login using the given credentials for the given service;
126
+ # returns a LoginResponse object.
127
+ def login_to_service(credentials, service)
128
+ lt = request_login_ticket
129
+
130
+ data = credentials.merge(
131
+ :lt => lt,
132
+ :service => service
133
+ )
134
+
135
+ res = submit_data_to_cas(login_url, data)
136
+ response = CASClient::LoginResponse.new(res)
137
+
138
+ if response.is_success?
139
+ log.info("Login was successful for ticket: #{response.ticket.inspect}.")
140
+ end
141
+
142
+ return response
143
+ end
144
+
145
+ # Requests a login ticket from the CAS server for use in a login request;
146
+ # returns a LoginTicket object.
147
+ #
148
+ # This only works with RubyCAS-Server, since obtaining login
149
+ # tickets in this manner is not part of the official CAS spec.
150
+ def request_login_ticket
151
+ uri = URI.parse(login_url+'Ticket')
152
+ https = Net::HTTP.new(uri.host, uri.port)
153
+ https.use_ssl = (uri.scheme == 'https')
154
+ https.verify_mode = (@force_ssl_verification ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE)
155
+ res = https.post(uri.path, ';')
156
+
157
+ raise CASException, res.body unless res.kind_of? Net::HTTPSuccess
158
+
159
+ res.body.strip
160
+ end
161
+
162
+ # Requests a proxy ticket from the CAS server for the given service
163
+ # using the given pgt (proxy granting ticket); returns a ProxyTicket
164
+ # object.
165
+ #
166
+ # The pgt required to request a proxy ticket is obtained as part of
167
+ # a ValidationResponse.
168
+ def request_proxy_ticket(pgt, target_service)
169
+ uri = URI.parse(proxy_url)
170
+ h = uri.query ? query_to_hash(uri.query) : {}
171
+ h['pgt'] = pgt.ticket
172
+ h['targetService'] = target_service
173
+ uri.query = hash_to_query(h)
174
+
175
+ pr = request_cas_response(uri, ProxyResponse)
176
+
177
+ pt = ProxyTicket.new(pr.proxy_ticket, target_service)
178
+ pt.response = pr
179
+
180
+ return pt
181
+ end
182
+
183
+ def retrieve_proxy_granting_ticket(pgt_iou)
184
+ uri = URI.parse(proxy_retrieval_url)
185
+ uri.query = (uri.query ? uri.query + "&" : "") + "pgtIou=#{CGI.escape(pgt_iou)}"
186
+ retrieve_url = uri.to_s
187
+
188
+ log.debug "Retrieving PGT for PGT IOU #{pgt_iou.inspect} from #{retrieve_url.inspect}"
189
+
190
+ # https = Net::HTTP.new(uri.host, uri.port)
191
+ # https.use_ssl = (uri.scheme == 'https')
192
+ # res = https.post(uri.path, ';')
193
+ uri = URI.parse(uri) unless uri.kind_of? URI
194
+ https = Net::HTTP.new(uri.host, uri.port)
195
+ https.use_ssl = (uri.scheme == 'https')
196
+ https.verify_mode = (@force_ssl_verification ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE)
197
+
198
+ res = https.start do |conn|
199
+ conn.get("#{uri.path}?#{uri.query}")
200
+ end
201
+
202
+
203
+ raise CASException, res.body unless res.kind_of? Net::HTTPSuccess
204
+
205
+ ProxyGrantingTicket.new(res.body.strip, pgt_iou)
206
+ end
207
+
208
+ def add_service_to_login_url(service_url)
209
+ uri = URI.parse(login_url)
210
+ uri.query = (uri.query ? uri.query + "&" : "") + "service=#{CGI.escape(service_url)}"
211
+ uri.to_s
212
+ end
213
+
214
+ private
215
+ # Fetches a CAS response of the given type from the given URI.
216
+ # Type should be either ValidationResponse or ProxyResponse.
217
+ def request_cas_response(uri, type)
218
+ log.debug "Requesting CAS response for URI #{uri}"
219
+
220
+ uri = URI.parse(uri) unless uri.kind_of? URI
221
+ https = Net::HTTP.new(uri.host, uri.port)
222
+ https.use_ssl = (uri.scheme == 'https')
223
+ https.verify_mode = (@force_ssl_verification ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE)
224
+
225
+ begin
226
+ raw_res = https.start do |conn|
227
+ conn.get("#{uri.path}?#{uri.query}")
228
+ end
229
+ rescue Errno::ECONNREFUSED => e
230
+ log.error "CAS server did not respond! (#{e.inspect})"
231
+ raise "The CAS authentication server at #{uri} is not responding!"
232
+ end
233
+
234
+ # We accept responses of type 422 since RubyCAS-Server generates these
235
+ # in response to requests from the client that are processable but contain
236
+ # invalid CAS data (for example an invalid service ticket).
237
+ if raw_res.kind_of?(Net::HTTPSuccess) || raw_res.code.to_i == 422
238
+ log.debug "CAS server responded with #{raw_res.inspect}:\n#{raw_res.body}"
239
+ else
240
+ log.error "CAS server responded with an error! (#{raw_res.inspect})"
241
+ raise "The CAS authentication server at #{uri} responded with an error (#{raw_res.inspect})!"
242
+ end
243
+
244
+ type.new(raw_res.body)
245
+ end
246
+
247
+ # Submits some data to the given URI and returns a Net::HTTPResponse.
248
+ def submit_data_to_cas(uri, data)
249
+ uri = URI.parse(uri) unless uri.kind_of? URI
250
+ req = Net::HTTP::Post.new(uri.path)
251
+ req.set_form_data(data, ';')
252
+ https = Net::HTTP.new(uri.host, uri.port)
253
+ https.use_ssl = (uri.scheme == 'https')
254
+ https.verify_mode = (@force_ssl_verification ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE)
255
+ https.start {|conn| conn.request(req) }
256
+ end
257
+
258
+ def query_to_hash(query)
259
+ CGI.parse(query)
260
+ end
261
+
262
+ def hash_to_query(hash)
263
+ pairs = []
264
+ hash.each do |k, vals|
265
+ vals = [vals] unless vals.kind_of? Array
266
+ vals.each {|v| pairs << (v.nil? ? CGI.escape(k) : "#{CGI.escape(k)}=#{CGI.escape(v)}")}
267
+ end
268
+ pairs.join("&")
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,105 @@
1
+ module CASClient
2
+ module Frameworks
3
+ module Merb
4
+ module Filter
5
+ attr_reader :client
6
+
7
+ def cas_filter
8
+ @client ||= CASClient::Client.new(config)
9
+
10
+ service_ticket = read_ticket(self)
11
+
12
+ cas_login_url = client.add_service_to_login_url(read_service_url(self))
13
+
14
+ last_service_ticket = session[:cas_last_valid_ticket]
15
+ if (service_ticket && last_service_ticket &&
16
+ last_service_ticket.ticket == service_ticket.ticket &&
17
+ last_service_ticket.service == service_ticket.service)
18
+
19
+ # warn() rather than info() because we really shouldn't be re-validating the same ticket.
20
+ # The only time when this is acceptable is if the user manually does a refresh and the ticket
21
+ # happens to be in the URL.
22
+ log.warn("Reusing previously validated ticket since the new ticket and service are the same.")
23
+ service_ticket = last_service_ticket
24
+ elsif last_service_ticket &&
25
+ !config[:authenticate_on_every_request] &&
26
+ session[client.username_session_key]
27
+ # Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
28
+ # previously authenticated for this service). This is to prevent redirection to the CAS server on every
29
+ # request.
30
+ # This behaviour can be disabled (so that every request is routed through the CAS server) by setting
31
+ # the :authenticate_on_every_request config option to false.
32
+ log.debug "Existing local CAS session detected for #{session[client.username_session_key].inspect}. "+
33
+ "Previous ticket #{last_service_ticket.ticket.inspect} will be re-used."
34
+ service_ticket = last_service_ticket
35
+ end
36
+
37
+ if service_ticket
38
+ client.validate_service_ticket(service_ticket) unless service_ticket.has_been_validated?
39
+ validation_response = service_ticket.response
40
+
41
+ if service_ticket.is_valid?
42
+ log.info("Ticket #{service_ticket.inspect} for service #{service_ticket.service.inspect} " +
43
+ "belonging to user #{validation_response.user.inspect} is VALID.")
44
+
45
+ session[client.username_session_key] = validation_response.user
46
+ session[client.extra_attributes_session_key] = validation_response.extra_attributes
47
+
48
+ # Store the ticket in the session to avoid re-validating the same service
49
+ # ticket with the CAS server.
50
+ session[:cas_last_valid_ticket] = service_ticket
51
+ return true
52
+ else
53
+ log.warn("Ticket #{service_ticket.ticket.inspect} failed validation -- " +
54
+ "#{validation_response.failure_code}: #{validation_response.failure_message}")
55
+ redirect cas_login_url
56
+ throw :halt
57
+ end
58
+ else
59
+ log.warn("No ticket -- redirecting to #{cas_login_url}")
60
+ redirect cas_login_url
61
+ throw :halt
62
+ end
63
+ end
64
+
65
+ private
66
+ # Copied from Rails adapter
67
+ def read_ticket(controller)
68
+ ticket = controller.params[:ticket]
69
+
70
+ return nil unless ticket
71
+
72
+ log.debug("Request contains ticket #{ticket.inspect}.")
73
+
74
+ if ticket =~ /^PT-/
75
+ ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
76
+ else
77
+ ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
78
+ end
79
+ end
80
+
81
+ # Also copied from Rails adapter
82
+ def read_service_url(controller)
83
+ if config[:service_url]
84
+ log.debug("Using explicitly set service url: #{config[:service_url]}")
85
+ return config[:service_url]
86
+ end
87
+
88
+ params = controller.params.dup
89
+ params.delete(:ticket)
90
+ service_url = request.protocol + '://' + request.host / controller.url(params.to_hash.symbolize_keys!)
91
+ log.debug("Guessed service url: #{service_url.inspect}")
92
+ return service_url
93
+ end
94
+
95
+ def log
96
+ ::Merb.logger
97
+ end
98
+
99
+ def config
100
+ ::Merb::Plugins.config[:"rubycas-client"]
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end