rack-cas-client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +35 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +58 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/config.ru +14 -0
- data/lib/rack/cas_client.rb +437 -0
- data/lib/rack-cas-client.rb +0 -0
- data/rack-cas-client.gemspec +68 -0
- data/spec/rack-cas-client_spec.rb +7 -0
- data/spec/spec_helper.rb +12 -0
- metadata +130 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
gem 'rubycas-client', "~> 2.2.1"
|
3
|
+
gem 'activesupport', '>= 2'
|
4
|
+
|
5
|
+
# Include everything needed to run rake, tests, features, etc.
|
6
|
+
group :development do
|
7
|
+
gem "rspec", "~> 2.3.0"
|
8
|
+
gem "bundler", "~> 1.0.0"
|
9
|
+
gem "jeweler", "~> 1.6.4"
|
10
|
+
gem "rcov", ">= 0"
|
11
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (3.1.0)
|
5
|
+
multi_json (~> 1.0)
|
6
|
+
diff-lcs (1.1.3)
|
7
|
+
git (1.2.5)
|
8
|
+
jeweler (1.6.4)
|
9
|
+
bundler (~> 1.0)
|
10
|
+
git (>= 1.2.5)
|
11
|
+
rake
|
12
|
+
multi_json (1.0.3)
|
13
|
+
rake (0.9.2)
|
14
|
+
rcov (0.9.10)
|
15
|
+
rspec (2.3.0)
|
16
|
+
rspec-core (~> 2.3.0)
|
17
|
+
rspec-expectations (~> 2.3.0)
|
18
|
+
rspec-mocks (~> 2.3.0)
|
19
|
+
rspec-core (2.3.1)
|
20
|
+
rspec-expectations (2.3.0)
|
21
|
+
diff-lcs (~> 1.1.2)
|
22
|
+
rspec-mocks (2.3.0)
|
23
|
+
rubycas-client (2.2.1)
|
24
|
+
activesupport
|
25
|
+
|
26
|
+
PLATFORMS
|
27
|
+
ruby
|
28
|
+
|
29
|
+
DEPENDENCIES
|
30
|
+
activesupport (>= 2)
|
31
|
+
bundler (~> 1.0.0)
|
32
|
+
jeweler (~> 1.6.4)
|
33
|
+
rcov
|
34
|
+
rspec (~> 2.3.0)
|
35
|
+
rubycas-client (~> 2.2.1)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Jerome Riga
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
= rack-cas-client
|
2
|
+
This middleware will redirect all unauthenticated requests to the specified cas server. After a successful authentication the cas server will redirect the user back to your application with a service ticket. This ticket is used to check the validity of the user's session on subsequent calls.
|
3
|
+
|
4
|
+
By default the user's login username is store in the session (session['cas']['user']). If your cas server is configured to send extra attributes (http://code.google.com/p/rubycas-server/wiki/HowToSendExtraUserAttributes), then they are accessible at (session['cas']['user_extra']).
|
5
|
+
|
6
|
+
To log user out, send a DELETE to /logout and the middleware will delete the user's session.
|
7
|
+
|
8
|
+
This middleware was inspired by the 'rubycas-client-rails' gem
|
9
|
+
|
10
|
+
== Basic Usage
|
11
|
+
require 'rack/cas_client'
|
12
|
+
|
13
|
+
app = Rack::Builder.new {
|
14
|
+
use Rack::Session::Cookie
|
15
|
+
use Rack::Cas::Client, :cas_base_url => 'https://cas.myserver:443/', :session_dir => './tmp/sessions'
|
16
|
+
run lambda {|env|
|
17
|
+
request = Rack::Request.new(env)
|
18
|
+
[200, {'Content-Type' => 'text/html'}, ['<p>You are in '+"#{request.session['cas']['user']}"+' </p><p>Extra '+ "#{request.session['cas']['user_extra'].inspect}" +'</p><form action="/logout" method="post"><input type="hidden" name="_method" value="delete" /><input type="submit" value="out"/></form> ']]
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
run app
|
23
|
+
|
24
|
+
== Rails
|
25
|
+
In config/application.rb or in an environment file
|
26
|
+
|
27
|
+
config.middleware.insert_after ActionDispatch::Session::CookieStore, Rack::Cas::Client, :cas_base_url => 'https://cas.server:443/', :session_dir => './tmp/sessions'
|
28
|
+
|
29
|
+
== Sinatra
|
30
|
+
require 'sinatra/base'
|
31
|
+
|
32
|
+
class App < Sinatra::Base
|
33
|
+
use Rack::Session::Cookie
|
34
|
+
use Rack::Cas::Client, :cas_base_url => 'https://cas.myserver:443/', :session_dir => './tmp/sessions'
|
35
|
+
end
|
36
|
+
|
37
|
+
run App.new
|
38
|
+
|
39
|
+
|
40
|
+
== Todo
|
41
|
+
* Add tests
|
42
|
+
* cleanup code
|
43
|
+
|
44
|
+
== Contributing to rack-cas-client
|
45
|
+
|
46
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
47
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
48
|
+
* Fork the project
|
49
|
+
* Start a feature/bugfix branch
|
50
|
+
* Commit and push until you are happy with your contribution
|
51
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
52
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
53
|
+
|
54
|
+
== Copyright
|
55
|
+
|
56
|
+
Copyright (c) 2011 Jerome Riga. See LICENSE.txt for
|
57
|
+
further details.
|
58
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "rack-cas-client"
|
18
|
+
gem.homepage = "http://github.com/zemis/rack-cas-client"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Rack middleware for authentication with CAS server}
|
21
|
+
gem.description = %Q{Rack middleware that handles user authentication against a cas server}
|
22
|
+
gem.email = "jriga@zemis.co.uk"
|
23
|
+
gem.authors = ["Jerome Riga"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'rake/rdoctask'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
44
|
+
|
45
|
+
rdoc.rdoc_dir = 'rdoc'
|
46
|
+
rdoc.title = "rack-cas-client #{version}"
|
47
|
+
rdoc.rdoc_files.include('README*')
|
48
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
49
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/config.ru
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require './lib/rack/cas_client.rb'
|
3
|
+
|
4
|
+
|
5
|
+
app = Rack::Builder.new {
|
6
|
+
use Rack::Session::Cookie
|
7
|
+
use Rack::Cas::Client, :cas_base_url => 'https://localhost:44300/', :session_dir => './tmp/sessions', :enable_single_sign_out => true
|
8
|
+
run lambda {|env|
|
9
|
+
request = Rack::Request.new(env)
|
10
|
+
[200, {'Content-Type' => 'text/html'}, ['<p>You are in '+"#{request.session['cas']['user']}"+' </p><p>Extra '+ "#{request.session['cas']['user_extra'].inspect}" +'</p><form action="/logout" method="post"><input type="hidden" name="_method" value="delete" /><input type="submit" value="out"/></form> ']]
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
run app
|
@@ -0,0 +1,437 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'uri'
|
3
|
+
require 'casclient'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
# TODO: remove dependency on activesuppor/core_ext
|
7
|
+
# rubycas-client-2.2.1/lib/casclient/responses.rb:40 => String.blank?
|
8
|
+
# rubycas-client-2.2.1/lib/casclient/responses.rb:68 => Hash.from_xml(xml)
|
9
|
+
require 'active_support/core_ext'
|
10
|
+
|
11
|
+
module Rack
|
12
|
+
module Cas
|
13
|
+
|
14
|
+
module SessionStore
|
15
|
+
module FileSystem
|
16
|
+
# Creates a file in tmp/sessions linking a SessionTicket
|
17
|
+
# with the local Rails session id. The file is named
|
18
|
+
# cas_sess.<session ticket> and its text contents is the corresponding
|
19
|
+
# Rails session id.
|
20
|
+
# Returns the filename of the lookup file created.
|
21
|
+
def store_service_session_lookup(st, sid)
|
22
|
+
st = st.ticket if st.kind_of? CASClient::ServiceTicket
|
23
|
+
f = ::File.new(filename_of_service_session_lookup(st), 'w')
|
24
|
+
f.write(sid)
|
25
|
+
f.close
|
26
|
+
return f.path
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the local Rails session ID corresponding to the given
|
30
|
+
# ServiceTicket. This is done by reading the contents of the
|
31
|
+
# cas_sess.<session ticket> file created in a prior call to
|
32
|
+
# #store_service_session_lookup.
|
33
|
+
def read_service_session_lookup(st)
|
34
|
+
st = st.ticket if st.kind_of? CASClient::ServiceTicket
|
35
|
+
ssl_filename = filename_of_service_session_lookup(st)
|
36
|
+
return ::File.exists?(ssl_filename) && IO.read(ssl_filename)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Removes a stored relationship between a ServiceTicket and a local
|
40
|
+
# Rails session id. This should be called when the session is being
|
41
|
+
# closed.
|
42
|
+
#
|
43
|
+
# See #store_service_session_lookup.
|
44
|
+
def delete_service_session_lookup(st)
|
45
|
+
st = st.ticket if st.kind_of? CASClient::ServiceTicket
|
46
|
+
ssl_filename = filename_of_service_session_lookup(st)
|
47
|
+
::File.delete(ssl_filename) if ::File.exists?(ssl_filename)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the path and filename of the service session lookup file.
|
51
|
+
def filename_of_service_session_lookup(st)
|
52
|
+
st = st.ticket if st.kind_of? CASClient::ServiceTicket
|
53
|
+
return "#{config[:session_dir]}/cas_sess.#{st}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
class Client < Struct.new :app, :options
|
60
|
+
include SessionStore::FileSystem
|
61
|
+
attr_reader :mem
|
62
|
+
|
63
|
+
def call(env)
|
64
|
+
if assets_request?(env); return app.call(env); end
|
65
|
+
if logout_options = logout_request?(env); return logout(*logout_options) end
|
66
|
+
if request = sso_request?(env); return single_sign_out(request) end
|
67
|
+
if valid_session_options = authenticated?(env); return valid_session(*valid_session_options) end
|
68
|
+
if xml_request?(env); return unauthorized_request end
|
69
|
+
|
70
|
+
redirect_to_cas_for_authentication(env)
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
def client
|
75
|
+
@client ||= CASClient::Client.new(config)
|
76
|
+
end
|
77
|
+
|
78
|
+
def log
|
79
|
+
@logger ||= Rails.logger rescue ::Logger.new(STDOUT)
|
80
|
+
end
|
81
|
+
|
82
|
+
def config
|
83
|
+
@config ||= {
|
84
|
+
:cas_base_url => nil,
|
85
|
+
:cas_destination_logout_param_name => nil,
|
86
|
+
:logger => log,
|
87
|
+
:username_session_key => nil,
|
88
|
+
:extra_attributes_session_key => nil,
|
89
|
+
:ticket_store => nil,
|
90
|
+
:login_url => nil,
|
91
|
+
:validate_url => nil,
|
92
|
+
:proxy_url => nil,
|
93
|
+
:logout_url => nil,
|
94
|
+
:service_url => nil,
|
95
|
+
:proxy_callback_url => nil,
|
96
|
+
:proxy_retrieval_url => nil,
|
97
|
+
:tmp_dir => nil
|
98
|
+
}.merge(options)
|
99
|
+
raise "You must provide the location " if @config[:enable_single_sign_out] && @config[:session_dir].nil?
|
100
|
+
@config
|
101
|
+
end
|
102
|
+
|
103
|
+
def assets_request?(env)
|
104
|
+
Rack::Request.new(env).path =~ /.*\.(js|css|png|jpg|jpeg|gif|ico)$/i
|
105
|
+
end
|
106
|
+
|
107
|
+
def logout_request?(env)
|
108
|
+
request = Rack::Request.new(env)
|
109
|
+
if request.path == '/logout' && request.delete?
|
110
|
+
# st = last_service_ticket
|
111
|
+
st = request.session['cas']['last_valid_ticket']
|
112
|
+
[st, request]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def logout(st, request)
|
117
|
+
log.debug("Logging out!!")
|
118
|
+
log.debug("looking up st for deletion #{st.inspect}")
|
119
|
+
|
120
|
+
delete_service_session_lookup(st) if st
|
121
|
+
request.session.delete('cas')
|
122
|
+
|
123
|
+
response = Rack::Response.new
|
124
|
+
response.redirect(client.logout_url(request.referer))
|
125
|
+
response.finish
|
126
|
+
end
|
127
|
+
|
128
|
+
def sso_request?(env)
|
129
|
+
request = Rack::Request.new(env)
|
130
|
+
request if request.post? && request.params['logoutRequest']
|
131
|
+
end
|
132
|
+
|
133
|
+
def single_sign_out(request)
|
134
|
+
log.debug("SINGLE SIGN OUT")
|
135
|
+
logoutRequest = URI.unescape(request.params['logoutRequest'])
|
136
|
+
|
137
|
+
md = logoutRequest.match( %r{^<samlp:LogoutRequest.*?<samlp:SessionIndex>(.*)</samlp:SessionIndex>}m )
|
138
|
+
if md && md[1]
|
139
|
+
ticket = md[1]
|
140
|
+
done = delete_service_session_lookup(ticket)
|
141
|
+
if done
|
142
|
+
[200,{'Content-type' => 'text/plain'},['session deleted']]
|
143
|
+
else
|
144
|
+
[404,{'Content-type' => 'text/plain'},['session not found']]
|
145
|
+
end
|
146
|
+
else
|
147
|
+
[400,{'Content-type' => 'text/plain'},['missing service ticket in request']]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def unauthorized_request
|
152
|
+
log.debug("Unauthorized request")
|
153
|
+
if vr
|
154
|
+
[401, {'Content-type' => 'application/xml'}, ["<errors><error>#{vr.failure_message}</error></errors>"]]
|
155
|
+
else
|
156
|
+
[401, {'Content-type' => 'text/html'}, [vr.failure_message]]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def xml_request?(env)
|
161
|
+
Rack::Request.new(env).params[:format] == "xml"
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
def authenticated?(env)
|
166
|
+
request = Rack::Request.new(env)
|
167
|
+
@mem = request.session['cas'] || {}
|
168
|
+
|
169
|
+
current_service_ticket = nil
|
170
|
+
user = nil
|
171
|
+
user_extra = nil
|
172
|
+
new_session = true
|
173
|
+
|
174
|
+
case check_service_ticket(env)
|
175
|
+
when :identical
|
176
|
+
log.warn("Re-using previously validated ticket since the ticket id and service are the same.")
|
177
|
+
new_session = false
|
178
|
+
current_service_ticket = last_service_ticket
|
179
|
+
|
180
|
+
when :different
|
181
|
+
log.debug "Existing local CAS session detected for #{client_username_session_key.inspect}. "+"Previous ticket #{last_service_ticket.ticket.inspect} will be re-used."
|
182
|
+
new_session = false
|
183
|
+
current_service_ticket = last_service_ticket
|
184
|
+
|
185
|
+
else
|
186
|
+
log.debug("New session!")
|
187
|
+
current_service_ticket = service_ticket(env)
|
188
|
+
end
|
189
|
+
|
190
|
+
if current_service_ticket
|
191
|
+
unless current_service_ticket.has_been_validated?
|
192
|
+
log.debug("VALIDATING SERVICE TICKET")
|
193
|
+
client.validate_service_ticket(current_service_ticket)
|
194
|
+
end
|
195
|
+
vr = current_service_ticket.response
|
196
|
+
|
197
|
+
if current_service_ticket.is_valid?
|
198
|
+
work_for_vr_pgt_iou(vr,env) if vr.pgt_iou
|
199
|
+
|
200
|
+
return [env, request, new_session, current_service_ticket]
|
201
|
+
else
|
202
|
+
|
203
|
+
log.warn("Ticket #{current_service_ticket.ticket.inspect} failed validation -- #{vr.failure_code}: #{vr.failure_message}")
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
else
|
207
|
+
|
208
|
+
# no service ticket was present in the request
|
209
|
+
if gateway
|
210
|
+
log.info "Returning from CAS gateway without authentication."
|
211
|
+
|
212
|
+
# unset, to allow for the next request to be authenticated if necessary
|
213
|
+
sent_to_gateway = false
|
214
|
+
|
215
|
+
if config[:use_gatewaying]
|
216
|
+
log.info "This CAS client is configured to use gatewaying, so we will permit the user to continue without authentication."
|
217
|
+
client_username_session_key = nil
|
218
|
+
return true
|
219
|
+
else
|
220
|
+
log.warn "The CAS client is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
return false
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
rescue OpenSSL::SSL::SSLError
|
230
|
+
log.error("SSL Error: hostname was not match with the server certificate. You can try to disable the ssl verification with a :force_ssl_verification => false in your configurations file.")
|
231
|
+
|
232
|
+
return false
|
233
|
+
end
|
234
|
+
|
235
|
+
def valid_session(env, request, new_session, current_service_ticket)
|
236
|
+
cas_resp = current_service_ticket.response
|
237
|
+
log.info("Ticket #{current_service_ticket.ticket.inspect} for service #{current_service_ticket.service.inspect} belonging to user #{cas_resp.user.inspect} is VALID.")
|
238
|
+
env['rack.cas.client.user'] = cas_resp.user
|
239
|
+
env['rack.cas.client.user_extra'] = cas_resp.extra_attributes.dup
|
240
|
+
|
241
|
+
# TODO: remove ticket params from env
|
242
|
+
|
243
|
+
status, headers, body = app.call(env)
|
244
|
+
|
245
|
+
response = Rack::Response.new(body, status, headers)
|
246
|
+
# only modify the session when it's a new_session
|
247
|
+
if new_session
|
248
|
+
session = request.session
|
249
|
+
session['cas'] = {'last_valid_ticket' => current_service_ticket, 'filteruser' => cas_resp.user, 'username_session_key' => cas_resp.user}
|
250
|
+
|
251
|
+
if config[:enable_single_sign_out]
|
252
|
+
f = store_service_session_lookup(current_service_ticket, session)
|
253
|
+
log.debug("Wrote service session lookup file to #{f.inspect} with session id #{session.inspect}.")
|
254
|
+
end
|
255
|
+
|
256
|
+
response.delete_cookie(request.session_options[:key], {})
|
257
|
+
response.set_cookie(request.session_options[:key], session)
|
258
|
+
end
|
259
|
+
response.finish
|
260
|
+
end
|
261
|
+
|
262
|
+
def work_for_vr_pgt_iou(vr,env)
|
263
|
+
log.debug("CALLING work_for_vr_pgt_iou with vr #{vr.inspect}")
|
264
|
+
request = Rack::Request.new(env)
|
265
|
+
unless request.session[:cas_pgt] && request.session[:cas_pgt].ticket && request.session[:cas_pgt].iou == vr.pgt_iou
|
266
|
+
log.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...")
|
267
|
+
pgt = client.retrieve_proxy_granting_ticket(vr.pgt_iou)
|
268
|
+
|
269
|
+
if pgt
|
270
|
+
log.debug("Got PGT #{pgt.ticket.inspect} for PGT IOU #{pgt.iou.inspect}. This will be stored in the session.")
|
271
|
+
request.session[:cas_pgt] = pgt
|
272
|
+
# For backwards compatibility with RubyCAS-Client 1.x configurations...
|
273
|
+
request.session[:casfilterpgt] = pgt
|
274
|
+
else
|
275
|
+
log.error("Failed to retrieve a PGT for PGT IOU #{vr.pgt_iou}!")
|
276
|
+
end
|
277
|
+
else
|
278
|
+
log.info("PGT is present in session and PGT IOU #{vr.pgt_iou} matches the saved PGT IOU. Not retrieving new PGT.")
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
def check_service_ticket(env)
|
284
|
+
st, last_st = [service_ticket(env), last_service_ticket]
|
285
|
+
|
286
|
+
return :identical if st && last_st && last_st.ticket == st.ticket && last_st.service == st.service
|
287
|
+
return :different if last_st && !config[:authenticate_on_every_request] && client_username_session_key
|
288
|
+
end
|
289
|
+
|
290
|
+
def service_ticket(env)
|
291
|
+
request = Rack::Request.new(env)
|
292
|
+
|
293
|
+
ticket = request.params['ticket']
|
294
|
+
return unless ticket
|
295
|
+
|
296
|
+
if ticket =~ /^PT-/
|
297
|
+
CASClient::ProxyTicket.new(ticket, service_url(env), request.params.delete('renew'))
|
298
|
+
else
|
299
|
+
CASClient::ServiceTicket.new(ticket, service_url(env), request.params.delete('renew'))
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def last_service_ticket
|
304
|
+
@mem['last_valid_ticket']
|
305
|
+
end
|
306
|
+
def last_service_ticket=(value)
|
307
|
+
@mem['last_valid_ticket'] = value
|
308
|
+
end
|
309
|
+
|
310
|
+
def client_username_session_key
|
311
|
+
@mem['username_session_key']
|
312
|
+
end
|
313
|
+
def client_username_session_key=(value)
|
314
|
+
@mem['username_session_key'] = value
|
315
|
+
end
|
316
|
+
|
317
|
+
def client_extra_attributes_session_key
|
318
|
+
@mem['user_extra']
|
319
|
+
end
|
320
|
+
def client_extra_attributes_session_key=(value)
|
321
|
+
@mem['user_extra'] = value
|
322
|
+
end
|
323
|
+
def casfilteruser=(value)
|
324
|
+
@mem['filteruser'] = value
|
325
|
+
end
|
326
|
+
def gateway=(value)
|
327
|
+
@mem['sent_to_gateway'] = value
|
328
|
+
end
|
329
|
+
def gateway
|
330
|
+
@mem['sent_to_gateway']
|
331
|
+
end
|
332
|
+
def previous_redirect_to_cas
|
333
|
+
@mem['previous_redirect']
|
334
|
+
end
|
335
|
+
def previous_redirect_to_cas=(value)
|
336
|
+
@mem['cas']['previous_redirect'] = value
|
337
|
+
end
|
338
|
+
def validation_retry
|
339
|
+
@mem['validation_retry'] || 0
|
340
|
+
end
|
341
|
+
def validation_retry=(value)
|
342
|
+
@mem['validation_retry'] = value
|
343
|
+
end
|
344
|
+
|
345
|
+
def vr
|
346
|
+
@vr
|
347
|
+
end
|
348
|
+
|
349
|
+
def vr=(value)
|
350
|
+
@vr = value
|
351
|
+
end
|
352
|
+
|
353
|
+
def service_url(env)
|
354
|
+
return @service_url if @service_url
|
355
|
+
|
356
|
+
if config[:service_url]
|
357
|
+
log.debug("Using explicitly set service url: #{config[:service_url]}")
|
358
|
+
return @service_url = config[:service_url]
|
359
|
+
end
|
360
|
+
request = Rack::Request.new(env)
|
361
|
+
|
362
|
+
params = request.params.dup
|
363
|
+
params.delete(:ticket)
|
364
|
+
url = URI.const_get(request.scheme.upcase).build(:host => request.host, :port => request.port, :path => request.path, :query => request.query_string)
|
365
|
+
@service_url = url.to_s
|
366
|
+
log.debug("Guessed service url: #{@service_url}")
|
367
|
+
@service_url
|
368
|
+
end
|
369
|
+
|
370
|
+
|
371
|
+
def redirect_to_cas_for_authentication(env)
|
372
|
+
redirect_url = login_url(env)
|
373
|
+
|
374
|
+
if config[:use_gatewaying]
|
375
|
+
gateway = true
|
376
|
+
redirect_url << "&gateway=true"
|
377
|
+
else
|
378
|
+
gateway = false
|
379
|
+
end
|
380
|
+
|
381
|
+
if previous_redirect_to_cas && previous_redirect_to_cas > (Time.now - 1.second)
|
382
|
+
log.warn("Previous redirect to the CAS server was less than a second ago. The client at #{controller.request.remote_ip.inspect} may be stuck in a redirection loop!")
|
383
|
+
|
384
|
+
if validation_retry > 3
|
385
|
+
log.error("Redirection loop intercepted. Client at #{controller.request.remote_ip.inspect} will be redirected back to login page and forced to renew authentication.")
|
386
|
+
redirect_url += "&renew=1&redirection_loop_intercepted=1"
|
387
|
+
end
|
388
|
+
|
389
|
+
validation_retry = validation_retry + 1
|
390
|
+
else
|
391
|
+
validation_retry = 0
|
392
|
+
end
|
393
|
+
previous_redirect_to_cas = Time.now
|
394
|
+
|
395
|
+
request = Rack::Request.new(env)
|
396
|
+
response = Rack::Response.new(["redirect to #{redirect_url}"],302, {'Location' => redirect_url, 'Content-Type' => 'text/plain'})
|
397
|
+
|
398
|
+
return response.finish
|
399
|
+
end
|
400
|
+
|
401
|
+
|
402
|
+
# Returns the login URL for the current controller.
|
403
|
+
# Useful when you want to provide a "Login" link in a GatewayFilter'ed
|
404
|
+
# action.
|
405
|
+
def login_url(env)
|
406
|
+
url = client.add_service_to_login_url(service_url(env))
|
407
|
+
log.debug("Generated login url: #{url}")
|
408
|
+
return url
|
409
|
+
end
|
410
|
+
|
411
|
+
end
|
412
|
+
|
413
|
+
|
414
|
+
module ClientHelpers
|
415
|
+
|
416
|
+
module Sinatra
|
417
|
+
def current_user
|
418
|
+
return @current_user if @current_user
|
419
|
+
user_data = {:username => request.env['rack.cas.client.user']}
|
420
|
+
user_data.merge!(request.env['rack.cas.client.user_extra'])
|
421
|
+
@current_user = OpenStruct.new(user_data)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
module Rails
|
426
|
+
def current_user
|
427
|
+
return @current_user if @current_user
|
428
|
+
user_data = {:username => request.env['rack.cas.client.user']}
|
429
|
+
user_data.merge!(request.env['rack.cas.client.user_extra'])
|
430
|
+
@current_user = OpenStruct.new(user_data)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
end
|
435
|
+
|
436
|
+
end
|
437
|
+
end
|
File without changes
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{rack-cas-client}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = [%q{Jerome Riga}]
|
12
|
+
s.date = %q{2011-10-10}
|
13
|
+
s.description = %q{Rack middleware that handles user authentication against a cas server}
|
14
|
+
s.email = %q{jriga@zemis.co.uk}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".rspec",
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"LICENSE.txt",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"config.ru",
|
29
|
+
"lib/rack-cas-client.rb",
|
30
|
+
"lib/rack/cas_client.rb",
|
31
|
+
"rack-cas-client.gemspec",
|
32
|
+
"spec/rack-cas-client_spec.rb",
|
33
|
+
"spec/spec_helper.rb"
|
34
|
+
]
|
35
|
+
s.homepage = %q{http://github.com/zemis/rack-cas-client}
|
36
|
+
s.licenses = [%q{MIT}]
|
37
|
+
s.require_paths = [%q{lib}]
|
38
|
+
s.rubygems_version = %q{1.8.6}
|
39
|
+
s.summary = %q{Rack middleware for authentication with CAS server}
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
s.specification_version = 3
|
43
|
+
|
44
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
45
|
+
s.add_runtime_dependency(%q<rubycas-client>, ["~> 2.2.1"])
|
46
|
+
s.add_runtime_dependency(%q<activesupport>, [">= 2"])
|
47
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
|
48
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
49
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
|
50
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
51
|
+
else
|
52
|
+
s.add_dependency(%q<rubycas-client>, ["~> 2.2.1"])
|
53
|
+
s.add_dependency(%q<activesupport>, [">= 2"])
|
54
|
+
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
55
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
56
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
57
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
58
|
+
end
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<rubycas-client>, ["~> 2.2.1"])
|
61
|
+
s.add_dependency(%q<activesupport>, [">= 2"])
|
62
|
+
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
63
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
64
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
65
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'rack-cas-client'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-cas-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jerome Riga
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-10 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rubycas-client
|
16
|
+
requirement: &2156465080 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.2.1
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2156465080
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activesupport
|
27
|
+
requirement: &2156464560 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2156464560
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &2156464080 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.3.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2156464080
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: bundler
|
49
|
+
requirement: &2156463600 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.0.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2156463600
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: jeweler
|
60
|
+
requirement: &2156463120 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.6.4
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2156463120
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rcov
|
71
|
+
requirement: &2156462640 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *2156462640
|
80
|
+
description: Rack middleware that handles user authentication against a cas server
|
81
|
+
email: jriga@zemis.co.uk
|
82
|
+
executables: []
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files:
|
85
|
+
- LICENSE.txt
|
86
|
+
- README.rdoc
|
87
|
+
files:
|
88
|
+
- .document
|
89
|
+
- .rspec
|
90
|
+
- Gemfile
|
91
|
+
- Gemfile.lock
|
92
|
+
- LICENSE.txt
|
93
|
+
- README.rdoc
|
94
|
+
- Rakefile
|
95
|
+
- VERSION
|
96
|
+
- config.ru
|
97
|
+
- lib/rack-cas-client.rb
|
98
|
+
- lib/rack/cas_client.rb
|
99
|
+
- rack-cas-client.gemspec
|
100
|
+
- spec/rack-cas-client_spec.rb
|
101
|
+
- spec/spec_helper.rb
|
102
|
+
homepage: http://github.com/zemis/rack-cas-client
|
103
|
+
licenses:
|
104
|
+
- MIT
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ! '>='
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
segments:
|
116
|
+
- 0
|
117
|
+
hash: -4063731964327134772
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 1.8.6
|
127
|
+
signing_key:
|
128
|
+
specification_version: 3
|
129
|
+
summary: Rack middleware for authentication with CAS server
|
130
|
+
test_files: []
|