vibes-rubycas-client 2.3.0.alpha

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.
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