omniauth-cas3 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +16 -0
- data/.gitignore +22 -0
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/LICENSE +23 -0
- data/README.md +124 -0
- data/Rakefile +15 -0
- data/lib/omniauth-cas3.rb +1 -0
- data/lib/omniauth/cas3.rb +2 -0
- data/lib/omniauth/cas3/version.rb +5 -0
- data/lib/omniauth/strategies/cas3.rb +222 -0
- data/lib/omniauth/strategies/cas3/logout_request.rb +75 -0
- data/lib/omniauth/strategies/cas3/service_ticket_validator.rb +103 -0
- data/omniauth-cas3.gemspec +28 -0
- data/spec/fixtures/cas_failure.xml +4 -0
- data/spec/fixtures/cas_success.xml +14 -0
- data/spec/fixtures/cas_success_jasig.xml +16 -0
- data/spec/omniauth/strategies/cas3/logout_request_spec.rb +127 -0
- data/spec/omniauth/strategies/cas3/service_ticket_validator_spec.rb +55 -0
- data/spec/omniauth/strategies/cas3_spec.rb +250 -0
- data/spec/spec_helper.rb +13 -0
- metadata +184 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fb755a8d3fdc733fb3a0eb368844ea4386e86666
|
4
|
+
data.tar.gz: 42b6db5b805dd203b90b764b8a169ec656c79aa2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3efeb0d5520267a5cd42577584b7df5332ff7466806eea54d790f3489bb9de7c1f0897df07640af90843c5429c55cbc6bfa2b2654832c5d4c0cd62fd3daca880
|
7
|
+
data.tar.gz: 4a6b2ca7ca3b8b3b56ab681a853992baa49a7307bccfac3e39790581d2daf61833bbdafa5d8a8debd47729b59ca0b40fd8e492cf3a396b50d0d101b88354a4c6
|
data/.editorconfig
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# EditorConfig helps developers define and maintain consistent
|
2
|
+
# coding styles between different editors and IDEs
|
3
|
+
# editorconfig.org
|
4
|
+
|
5
|
+
root = true
|
6
|
+
|
7
|
+
[*]
|
8
|
+
# Change these settings to your own preference
|
9
|
+
indent_style = space
|
10
|
+
indent_size = 2
|
11
|
+
|
12
|
+
# We recommend you to keep these unchanged
|
13
|
+
end_of_line = lf
|
14
|
+
charset = utf-8
|
15
|
+
trim_trailing_whitespace = true
|
16
|
+
insert_final_newline = true
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2011 Derek Lindahl and CustomInk, LLC
|
2
|
+
Copyright (c) 2015 tduehr
|
3
|
+
|
4
|
+
MIT License
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# OmniAuth CAS Strategy [![Gem Version][version_badge]][version] [![Build Status][travis_status]][travis]
|
2
|
+
|
3
|
+
[version_badge]: https://badge.fury.io/rb/omniauth-cas3.png
|
4
|
+
[version]: http://badge.fury.io/rb/omniauth-cas3
|
5
|
+
[travis]: http://travis-ci.org/tduehr/omniauth-cas3
|
6
|
+
[travis_status]: https://secure.travis-ci.org/dlindahl/omniauth-cas3.png
|
7
|
+
[releases]: https://github.com/tduehr/omniauth-cas3/releases
|
8
|
+
|
9
|
+
This is a OmniAuth 1.0 compatible port of the previously available
|
10
|
+
[OmniAuth CAS strategy][old_omniauth_cas] that was bundled with OmniAuth 0.3. This strategy haas also been updated for CAS protocol version 3.0 and patched to deal with namespace issues.
|
11
|
+
|
12
|
+
* [View the documentation][document_up]
|
13
|
+
* [Changelog][releases]
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
gem 'omniauth-cas3'
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install omniauth-cas3
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
Use like any other OmniAuth strategy:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
35
|
+
provider :cas3, host: 'cas.yourdomain.com'
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
### Configuration Options
|
40
|
+
|
41
|
+
#### Required
|
42
|
+
|
43
|
+
OmniAuth CAS requires at least one of the following two configuration options:
|
44
|
+
|
45
|
+
* `url` - Defines the URL of your CAS server (i.e. `http://example.org:8080`)
|
46
|
+
* `host` - Defines the host of your CAS server (i.e. `example.org`).
|
47
|
+
|
48
|
+
#### Optional
|
49
|
+
|
50
|
+
Other configuration options:
|
51
|
+
|
52
|
+
* `port` - The port to use for your configured CAS `host`. Optional if using `url`.
|
53
|
+
* `ssl` - TRUE to connect to your CAS server over SSL. Optional if using `url`.
|
54
|
+
* `service_validate_url` - The URL to use to validate a user. Defaults to `'/serviceValidate'`.
|
55
|
+
* `callback_url` - The URL custom URL path which CAS uses to call back to the service. Defaults to `/users/auth/cas3/callback`.
|
56
|
+
* `logout_url` - The URL to use to logout a user. Defaults to `'/logout'`.
|
57
|
+
* `login_url` - Defines the URL used to prompt users for their login information. Defaults to `/login` If no `host` is configured, the host application's domain will be used.
|
58
|
+
* `uid_field` - The user data attribute to use as your user's unique identifier. Defaults to `'user'` (which usually contains the user's login name).
|
59
|
+
* `ca_path` - Optional when `ssl` is `true`. Sets path of a CA certification directory. See [Net::HTTP][net_http] for more details.
|
60
|
+
* `disable_ssl_verification` - Optional when `ssl` is true. Disables verification.
|
61
|
+
* `on_single_sign_out` - Optional. Callback used when a [CAS 3.1 Single Sign Out][sso]
|
62
|
+
request is received.
|
63
|
+
* `fetch_raw_info` - Optional. Callback used to return additional "raw" user
|
64
|
+
info from other sources.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
provider :cas3,
|
68
|
+
fetch_raw_info: lambda { |strategy, options, ticket, user_info|
|
69
|
+
ExternalService.get(user_info[:user]).attributes
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
Configurable options for values returned by CAS:
|
74
|
+
|
75
|
+
* `uid_key` - The user ID data attribute to use as your user's unique identifier. Defaults to `'user'` (which usually contains the user's login name).
|
76
|
+
* `name_key` - The data attribute containing user first and last name. Defaults to `'name'`.
|
77
|
+
* `email_key` - The data attribute containing user email address. Defaults to `'email'`.
|
78
|
+
* `nickname_key` - The data attribute containing user's nickname. Defaults to `'user'`.
|
79
|
+
* `first_name_key` - The data attribute containing user first name. Defaults to `'first_name'`.
|
80
|
+
* `last_name_key` - The data attribute containing user last name. Defaults to `'last_name'`.
|
81
|
+
* `location_key` - The data attribute containing user location/address. Defaults to `'location'`.
|
82
|
+
* `image_key` - The data attribute containing user image/picture. Defaults to `'image'`.
|
83
|
+
* `phone_key` - The data attribute containing user contact phone number. Defaults to `'phone'`.
|
84
|
+
|
85
|
+
## Migrating from OmniAuth 0.3
|
86
|
+
|
87
|
+
Given the following OmniAuth 0.3 configuration:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
provider :CAS, cas_server: 'https://cas.example.com/cas/'
|
91
|
+
```
|
92
|
+
|
93
|
+
Your new settings should look similar to this:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
provider :cas3,
|
97
|
+
host: 'cas.example.com',
|
98
|
+
login_url: '/cas/login',
|
99
|
+
service_validate_url: '/cas/p3/serviceValidate'
|
100
|
+
```
|
101
|
+
|
102
|
+
If you encounter problems wih SSL certificates you may want to set the `ca_path` parameter or activate `disable_ssl_verification` (not recommended).
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
1. Fork it
|
107
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
108
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
109
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
110
|
+
5. Create new Pull Request
|
111
|
+
|
112
|
+
## Thanks
|
113
|
+
|
114
|
+
Special thanks go out to the following people
|
115
|
+
|
116
|
+
* @dlindahl For the original work in porting this from OmniAuth 0.3
|
117
|
+
* Phillip Aldridge (@iterateNZ) and JB Barth (@jbbarth) for helping out with Issue #3
|
118
|
+
* Elber Ribeiro (@dynaum) for Ubuntu SSL configuration support
|
119
|
+
* @rbq for README updates and OmniAuth 0.3 migration guide
|
120
|
+
|
121
|
+
[old_omniauth_cas]: https://github.com/intridea/omniauth/blob/0-3-stable/oa-enterprise/lib/omniauth/strategies/cas.rb
|
122
|
+
[document_up]: http://tduehr.github.com/omniauth-cas3/
|
123
|
+
[net_http]: http://ruby-doc.org/stdlib-1.9.3/libdoc/net/http/rdoc/Net/HTTP.html
|
124
|
+
[sso]: https://wiki.jasig.org/display/CASUM/Single+Sign+Out
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
desc 'Default: run specs.'
|
6
|
+
task default: :spec
|
7
|
+
|
8
|
+
desc 'Run specs'
|
9
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
10
|
+
t.rspec_opts = '--require spec_helper --color --order rand'
|
11
|
+
end
|
12
|
+
|
13
|
+
task :test do
|
14
|
+
fail %q{This application uses RSpec. Try running "rake spec"}
|
15
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'omniauth/cas3'
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'omniauth'
|
2
|
+
require 'addressable/uri'
|
3
|
+
|
4
|
+
module OmniAuth
|
5
|
+
module Strategies
|
6
|
+
class CAS3
|
7
|
+
include OmniAuth::Strategy
|
8
|
+
|
9
|
+
# Custom Exceptions
|
10
|
+
class MissingCASTicket < StandardError; end
|
11
|
+
class InvalidCASTicket < StandardError; end
|
12
|
+
|
13
|
+
autoload :ServiceTicketValidator, 'omniauth/strategies/cas3/service_ticket_validator'
|
14
|
+
autoload :LogoutRequest, 'omniauth/strategies/cas3/logout_request'
|
15
|
+
|
16
|
+
attr_accessor :raw_info
|
17
|
+
alias_method :user_info, :raw_info
|
18
|
+
|
19
|
+
option :name, :cas3 # Required property by OmniAuth::Strategy
|
20
|
+
|
21
|
+
option :host, nil
|
22
|
+
option :port, nil
|
23
|
+
option :path, nil
|
24
|
+
option :ssl, true
|
25
|
+
option :service_validate_url, '/p3/serviceValidate'
|
26
|
+
option :login_url, '/login'
|
27
|
+
option :logout_url, '/logout'
|
28
|
+
option :on_single_sign_out, Proc.new {}
|
29
|
+
# A Proc or lambda that returns a Hash of additional user info to be
|
30
|
+
# merged with the info returned by the CAS server.
|
31
|
+
#
|
32
|
+
# @param [Object] An instance of OmniAuth::Strategies::CAS for the current request
|
33
|
+
# @param [String] The user's Service Ticket value
|
34
|
+
# @param [Hash] The user info for the Service Ticket returned by the CAS server
|
35
|
+
#
|
36
|
+
# @return [Hash] Extra user info
|
37
|
+
option :fetch_raw_info, Proc.new { Hash.new }
|
38
|
+
# Make all the keys configurable with some defaults set here
|
39
|
+
option :uid_field, 'user'
|
40
|
+
option :name_key, 'name'
|
41
|
+
option :email_key, 'email'
|
42
|
+
option :nickname_key, 'user'
|
43
|
+
option :first_name_key, 'first_name'
|
44
|
+
option :last_name_key, 'last_name'
|
45
|
+
option :location_key, 'location'
|
46
|
+
option :image_key, 'image'
|
47
|
+
option :phone_key, 'phone'
|
48
|
+
|
49
|
+
# As required by https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema
|
50
|
+
AuthHashSchemaKeys = %w{name email nickname first_name last_name location image phone}
|
51
|
+
info do
|
52
|
+
prune!({
|
53
|
+
name: raw_info[options[:name_key].to_s],
|
54
|
+
email: raw_info[options[:email_key].to_s],
|
55
|
+
nickname: raw_info[options[:nickname_key].to_s],
|
56
|
+
first_name: raw_info[options[:first_name_key].to_s],
|
57
|
+
last_name: raw_info[options[:last_name_key].to_s],
|
58
|
+
location: raw_info[options[:location_key].to_s],
|
59
|
+
image: raw_info[options[:image_key].to_s],
|
60
|
+
phone: raw_info[options[:phone_key].to_s]
|
61
|
+
})
|
62
|
+
end
|
63
|
+
|
64
|
+
extra do
|
65
|
+
prune!(
|
66
|
+
raw_info.delete_if{ |k,v| AuthHashSchemaKeys.include?(k) }
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
uid do
|
71
|
+
raw_info[options[:uid_field].to_s]
|
72
|
+
end
|
73
|
+
|
74
|
+
credentials do
|
75
|
+
prune!({ ticket: @ticket })
|
76
|
+
end
|
77
|
+
|
78
|
+
def callback_phase
|
79
|
+
if on_sso_path?
|
80
|
+
single_sign_out_phase
|
81
|
+
else
|
82
|
+
@ticket = request.params['ticket']
|
83
|
+
return fail!(:no_ticket, MissingCASTicket.new('No CAS Ticket')) unless @ticket
|
84
|
+
fetch_raw_info(@ticket)
|
85
|
+
return fail!(:invalid_ticket, InvalidCASTicket.new('Invalid CAS Ticket')) if raw_info.empty?
|
86
|
+
super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def request_phase
|
91
|
+
service_url = append_params(callback_url, return_url)
|
92
|
+
|
93
|
+
[
|
94
|
+
302,
|
95
|
+
{
|
96
|
+
'Location' => login_url(service_url),
|
97
|
+
'Content-Type' => 'text/plain'
|
98
|
+
},
|
99
|
+
["You are being redirected to CAS for sign-in."]
|
100
|
+
]
|
101
|
+
end
|
102
|
+
|
103
|
+
def on_sso_path?
|
104
|
+
request.post? && request.params.has_key?('logoutRequest')
|
105
|
+
end
|
106
|
+
|
107
|
+
def single_sign_out_phase
|
108
|
+
logout_request_service.new(self, request).call(options)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Build a CAS host with protocol and port
|
112
|
+
#
|
113
|
+
#
|
114
|
+
def cas_url
|
115
|
+
extract_url if options['url']
|
116
|
+
validate_cas_setup
|
117
|
+
@cas_url ||= begin
|
118
|
+
uri = Addressable::URI.new
|
119
|
+
uri.host = options.host
|
120
|
+
uri.scheme = options.ssl ? 'https' : 'http'
|
121
|
+
uri.port = options.port
|
122
|
+
uri.path = options.path
|
123
|
+
uri.to_s
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def extract_url
|
128
|
+
url = Addressable::URI.parse(options.delete('url'))
|
129
|
+
options.merge!(
|
130
|
+
'host' => url.host,
|
131
|
+
'port' => url.port,
|
132
|
+
'path' => url.path,
|
133
|
+
'ssl' => url.scheme == 'https'
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
137
|
+
def validate_cas_setup
|
138
|
+
if options.host.nil? || options.login_url.nil?
|
139
|
+
raise ArgumentError.new(":host and :login_url MUST be provided")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Build a service-validation URL from +service+ and +ticket+.
|
144
|
+
# If +service+ has a ticket param, first remove it. URL-encode
|
145
|
+
# +service+ and add it and the +ticket+ as paraemters to the
|
146
|
+
# CAS serviceValidate URL.
|
147
|
+
#
|
148
|
+
# @param [String] service the service (a.k.a. return-to) URL
|
149
|
+
# @param [String] ticket the ticket to validate
|
150
|
+
#
|
151
|
+
# @return [String] a URL like `http://cas.mycompany.com/serviceValidate?service=...&ticket=...`
|
152
|
+
def service_validate_url(service_url, ticket)
|
153
|
+
service_url = Addressable::URI.parse(service_url)
|
154
|
+
service_url.query_values = service_url.query_values.tap { |qs| qs.delete('ticket') }
|
155
|
+
cas_url + append_params(options.service_validate_url, {
|
156
|
+
service: service_url.to_s,
|
157
|
+
ticket: ticket
|
158
|
+
})
|
159
|
+
end
|
160
|
+
|
161
|
+
# Build a CAS login URL from +service+.
|
162
|
+
#
|
163
|
+
# @param [String] service the service (a.k.a. return-to) URL
|
164
|
+
#
|
165
|
+
# @return [String] a URL like `http://cas.mycompany.com/login?service=...`
|
166
|
+
def login_url(service)
|
167
|
+
cas_url + append_params(options.login_url, { service: service })
|
168
|
+
end
|
169
|
+
|
170
|
+
# Adds URL-escaped +parameters+ to +base+.
|
171
|
+
#
|
172
|
+
# @param [String] base the base URL
|
173
|
+
# @param [String] params the parameters to append to the URL
|
174
|
+
#
|
175
|
+
# @return [String] the new joined URL.
|
176
|
+
def append_params(base, params)
|
177
|
+
params = params.each { |k,v| v = Rack::Utils.escape(v) }
|
178
|
+
Addressable::URI.parse(base).tap do |base_uri|
|
179
|
+
base_uri.query_values = (base_uri.query_values || {}).merge(params)
|
180
|
+
end.to_s
|
181
|
+
end
|
182
|
+
|
183
|
+
# Validate the Service Ticket
|
184
|
+
# @return [Object] the validated Service Ticket
|
185
|
+
def validate_service_ticket(ticket)
|
186
|
+
ServiceTicketValidator.new(self, options, callback_url, ticket).call
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def fetch_raw_info(ticket)
|
192
|
+
ticket_user_info = validate_service_ticket(ticket).user_info
|
193
|
+
custom_user_info = options.fetch_raw_info.call(self, options, ticket, ticket_user_info)
|
194
|
+
self.raw_info = ticket_user_info.merge(custom_user_info)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Deletes Hash pairs with `nil` values.
|
198
|
+
# From https://github.com/mkdynamic/omniauth-facebook/blob/972ed5e3456bcaed7df1f55efd7c05c216c8f48e/lib/omniauth/strategies/facebook.rb#L122-127
|
199
|
+
def prune!(hash)
|
200
|
+
hash.delete_if do |_, value|
|
201
|
+
prune!(value) if value.is_a?(Hash)
|
202
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def return_url
|
207
|
+
# If the request already has a `url` parameter, then it will already be appended to the callback URL.
|
208
|
+
if request.params && request.params['url']
|
209
|
+
{}
|
210
|
+
else
|
211
|
+
{ url: request.referer }
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def logout_request_service
|
216
|
+
LogoutRequest
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
OmniAuth.config.add_camelization 'cas3', 'CAS3'
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module OmniAuth
|
2
|
+
module Strategies
|
3
|
+
class CAS3
|
4
|
+
class LogoutRequest
|
5
|
+
def initialize(strategy, request)
|
6
|
+
@strategy, @request = strategy, request
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(options = {})
|
10
|
+
@options = options
|
11
|
+
|
12
|
+
begin
|
13
|
+
result = single_sign_out_callback.call(*logout_request)
|
14
|
+
rescue StandardError => err
|
15
|
+
return @strategy.fail! :logout_request, err
|
16
|
+
else
|
17
|
+
result = [200,{},'OK'] if result == true || result.nil?
|
18
|
+
ensure
|
19
|
+
return unless result
|
20
|
+
|
21
|
+
# TODO: Why does ActionPack::Response return [status,headers,body]
|
22
|
+
# when Rack::Response#new wants [body,status,headers]? Additionally,
|
23
|
+
# why does Rack::Response differ in argument order from the usual
|
24
|
+
# Rack-like [status,headers,body] array?
|
25
|
+
return Rack::Response.new(result[2],result[0],result[1]).finish
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def logout_request
|
32
|
+
@logout_request ||= begin
|
33
|
+
saml = parse_and_ensure_namespaces(@request.params['logoutRequest'])
|
34
|
+
ns = saml.collect_namespaces
|
35
|
+
name_id = saml.xpath('//saml:NameID', ns).text
|
36
|
+
sess_idx = saml.xpath('//samlp:SessionIndex', ns).text
|
37
|
+
inject_params(name_id:name_id, session_index:sess_idx)
|
38
|
+
@request
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_and_ensure_namespaces(logout_request_xml)
|
43
|
+
doc = Nokogiri.parse(logout_request_xml)
|
44
|
+
ns = doc.collect_namespaces
|
45
|
+
if ns.include?('xmlns:samlp') && ns.include?('xmlns:saml')
|
46
|
+
doc
|
47
|
+
else
|
48
|
+
add_namespaces(doc)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_namespaces(logout_request_doc)
|
53
|
+
root = logout_request_doc.root
|
54
|
+
root.add_namespace('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol')
|
55
|
+
root.add_namespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion\\')
|
56
|
+
|
57
|
+
# In order to add namespaces properly we need to re-parse the document
|
58
|
+
Nokogiri.parse(logout_request_doc.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
def inject_params(new_params)
|
62
|
+
rack_input = @request.env['rack.input'].read
|
63
|
+
params = Rack::Utils.parse_query(rack_input).merge new_params
|
64
|
+
@request.env['rack.input'] = StringIO.new(Rack::Utils.build_query(params))
|
65
|
+
ensure
|
66
|
+
@request.env['rack.input'].rewind
|
67
|
+
end
|
68
|
+
|
69
|
+
def single_sign_out_callback
|
70
|
+
@options[:on_single_sign_out]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module OmniAuth
|
6
|
+
module Strategies
|
7
|
+
class CAS3
|
8
|
+
class ServiceTicketValidator
|
9
|
+
VALIDATION_REQUEST_HEADERS = { 'Accept' => '*/*' }
|
10
|
+
|
11
|
+
# Build a validator from a +configuration+, a
|
12
|
+
# +return_to+ URL, and a +ticket+.
|
13
|
+
#
|
14
|
+
# @param [Hash] options the OmniAuth Strategy options
|
15
|
+
# @param [String] return_to_url the URL of this CAS client service
|
16
|
+
# @param [String] ticket the service ticket to validate
|
17
|
+
def initialize(strategy, options, return_to_url, ticket)
|
18
|
+
@options = options
|
19
|
+
@uri = URI.parse(strategy.service_validate_url(return_to_url, ticket))
|
20
|
+
end
|
21
|
+
|
22
|
+
# Executes a network request to process the CAS Service Response
|
23
|
+
def call
|
24
|
+
@response_body = get_service_response_body
|
25
|
+
@success_body = find_authentication_success(@response_body)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# Request validation of the ticket from the CAS server's
|
30
|
+
# serviceValidate (CAS 2.0) function.
|
31
|
+
#
|
32
|
+
# Swallows all XML parsing errors (and returns +nil+ in those cases).
|
33
|
+
#
|
34
|
+
# @return [Hash, nil] a user information hash if the response is valid; +nil+ otherwise.
|
35
|
+
#
|
36
|
+
# @raise any connection errors encountered.
|
37
|
+
def user_info
|
38
|
+
parse_user_info(@success_body)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# turns an `<cas:authenticationSuccess>` node into a Hash;
|
44
|
+
# returns nil if given nil
|
45
|
+
def parse_user_info(node)
|
46
|
+
return nil if node.nil?
|
47
|
+
{}.tap do |hash|
|
48
|
+
node.children.each do |e|
|
49
|
+
node_name = e.name.sub(/^cas:/, '')
|
50
|
+
unless e.kind_of?(Nokogiri::XML::Text) || node_name == 'proxies'
|
51
|
+
# There are no child elements
|
52
|
+
if e.element_children.count == 0
|
53
|
+
hash[node_name] = e.content
|
54
|
+
elsif e.element_children.count
|
55
|
+
# JASIG style extra attributes
|
56
|
+
if node_name == 'attributes'
|
57
|
+
hash.merge!(parse_user_info(e))
|
58
|
+
else
|
59
|
+
hash[node_name] = [] if hash[node_name].nil?
|
60
|
+
hash[node_name].push(parse_user_info(e))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# finds an `<cas:authenticationSuccess>` node in
|
69
|
+
# a `<cas:serviceResponse>` body if present; returns nil
|
70
|
+
# if the passed body is nil or if there is no such node.
|
71
|
+
def find_authentication_success(body)
|
72
|
+
return nil if body.nil? || body == ''
|
73
|
+
begin
|
74
|
+
doc = Nokogiri::XML(body)
|
75
|
+
begin
|
76
|
+
doc.xpath('/cas:serviceResponse/cas:authenticationSuccess')
|
77
|
+
rescue Nokogiri::XML::XPath::SyntaxError
|
78
|
+
doc.xpath('/serviceResponse/authenticationSuccess')
|
79
|
+
end
|
80
|
+
rescue Nokogiri::XML::XPath::SyntaxError
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# retrieves the `<cas:serviceResponse>` XML from the CAS server
|
86
|
+
def get_service_response_body
|
87
|
+
result = ''
|
88
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
89
|
+
http.use_ssl = @uri.port == 443 || @uri.instance_of?(URI::HTTPS)
|
90
|
+
if http.use_ssl?
|
91
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @options.disable_ssl_verification?
|
92
|
+
http.ca_path = @options.ca_path
|
93
|
+
end
|
94
|
+
http.start do |c|
|
95
|
+
response = c.get "#{@uri.path}?#{@uri.query}", VALIDATION_REQUEST_HEADERS.dup
|
96
|
+
result = response.body
|
97
|
+
end
|
98
|
+
result
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/omniauth/cas3/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Derek Lindahl, tduehr"]
|
6
|
+
gem.email = ["td@matasano.com"]
|
7
|
+
gem.summary = %q{CAS 3.0 Strategy for OmniAuth}
|
8
|
+
gem.description = gem.summary
|
9
|
+
gem.homepage = "https://github.com/tduehr/omniauth-cas3"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "omniauth-cas3"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Omniauth::Cas3::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'omniauth', '~> 1.2'
|
19
|
+
gem.add_dependency 'nokogiri', '~> 1.6.6'
|
20
|
+
gem.add_dependency 'addressable', '~> 2.3'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rake', '~> 10.0'
|
23
|
+
gem.add_development_dependency 'webmock', '~> 1.19.0'
|
24
|
+
gem.add_development_dependency 'rspec', '~> 3.1.0'
|
25
|
+
gem.add_development_dependency 'rack-test', '~> 0.6'
|
26
|
+
|
27
|
+
gem.add_development_dependency 'awesome_print'
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
2
|
+
<cas:authenticationSuccess>
|
3
|
+
<cas:user>psegel</cas:user>
|
4
|
+
<cas:employeeid>54</cas:employeeid>
|
5
|
+
<cas:first_name>P. Segel</cas:first_name>
|
6
|
+
<cas:first_name>Peter</cas:first_name>
|
7
|
+
<cas:last_name>Segel</cas:last_name>
|
8
|
+
<cas:email>psegel@intridea.com</cas:email>
|
9
|
+
<cas:location>Washington, D.C.</cas:location>
|
10
|
+
<cas:image>/images/user.jpg</cas:image>
|
11
|
+
<cas:phone>555-555-5555</cas:phone>
|
12
|
+
<cas:hire_date>2004-07-13</cas:hire_date>
|
13
|
+
</cas:authenticationSuccess>
|
14
|
+
</cas:serviceResponse>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
2
|
+
<cas:authenticationSuccess>
|
3
|
+
<cas:user>psegel</cas:user>
|
4
|
+
<cas:attributes>
|
5
|
+
<cas:employeeid>54</cas:employeeid>
|
6
|
+
<cas:first_name>P. Segel</cas:first_name>
|
7
|
+
<cas:first_name>Peter</cas:first_name>
|
8
|
+
<cas:last_name>Segel</cas:last_name>
|
9
|
+
<cas:email>psegel@intridea.com</cas:email>
|
10
|
+
<cas:location>Washington, D.C.</cas:location>
|
11
|
+
<cas:image>/images/user.jpg</cas:image>
|
12
|
+
<cas:phone>555-555-5555</cas:phone>
|
13
|
+
<cas:hire_date>2004-07-13</cas:hire_date>
|
14
|
+
</cas:attributes>
|
15
|
+
</cas:authenticationSuccess>
|
16
|
+
</cas:serviceResponse>
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe OmniAuth::Strategies::CAS3::LogoutRequest do
|
4
|
+
let(:strategy) { double('strategy') }
|
5
|
+
let(:env) do
|
6
|
+
{ 'rack.input' => StringIO.new('','r') }
|
7
|
+
end
|
8
|
+
let(:request) { double('request', params:params, env:env) }
|
9
|
+
let(:params) { { 'url' => url, 'logoutRequest' => logoutRequest } }
|
10
|
+
let(:url) { 'http://notes.dev/signed_in' }
|
11
|
+
let(:logoutRequest) do
|
12
|
+
%Q[
|
13
|
+
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion\" ID="123abc-1234-ab12-cd34-1234abcd" Version="2.0" IssueInstant="#{Time.now.to_s}">
|
14
|
+
<saml:NameID>@NOT_USED@</saml:NameID>
|
15
|
+
<samlp:SessionIndex>ST-123456-123abc456def</samlp:SessionIndex>
|
16
|
+
</samlp:LogoutRequest>
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
subject { described_class.new(strategy, request).call(options) }
|
21
|
+
|
22
|
+
describe 'SAML attributes' do
|
23
|
+
let(:callback) { Proc.new{} }
|
24
|
+
let(:options) do
|
25
|
+
{ on_single_sign_out: callback }
|
26
|
+
end
|
27
|
+
|
28
|
+
before do
|
29
|
+
@rack_input = nil
|
30
|
+
allow(callback).to receive(:call) do |req|
|
31
|
+
@rack_input = req.env['rack.input'].read
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'are parsed and injected into the Rack Request parameters' do
|
37
|
+
subject
|
38
|
+
expect(@rack_input).to eq 'name_id=%40NOT_USED%40&session_index=ST-123456-123abc456def'
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'are parsed and injected even if saml defined inside NameID' do
|
42
|
+
request.params['logoutRequest'] =
|
43
|
+
%Q[
|
44
|
+
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="foobarbaz" Version="2.0" IssueInstant="2014-10-19T17:13:50Z">
|
45
|
+
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID>
|
46
|
+
<samlp:SessionIndex>ST-foo-bar</samlp:SessionIndex>
|
47
|
+
</samlp:LogoutRequest>
|
48
|
+
]
|
49
|
+
subject
|
50
|
+
expect(@rack_input).to eq 'name_id=%40NOT_USED%40&session_index=ST-foo-bar'
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'are parsed and injected even if saml and samlp namespaces not defined' do
|
54
|
+
request.params['logoutRequest'] =
|
55
|
+
%Q[
|
56
|
+
<samlp:LogoutRequest ID="123abc-1234-ab12-cd34-1234abcd" Version="2.0" IssueInstant="#{Time.now.to_s}">
|
57
|
+
<saml:NameID>@NOT_USED@</saml:NameID>
|
58
|
+
<samlp:SessionIndex>ST-789000-456def789ghi</samlp:SessionIndex>
|
59
|
+
</samlp:LogoutRequest>
|
60
|
+
]
|
61
|
+
subject
|
62
|
+
expect(@rack_input).to eq 'name_id=%40NOT_USED%40&session_index=ST-789000-456def789ghi'
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'that raise when parsed' do
|
66
|
+
let(:env) { { 'rack.input' => nil } }
|
67
|
+
|
68
|
+
before do
|
69
|
+
allow(strategy).to receive(:fail!)
|
70
|
+
subject
|
71
|
+
expect(strategy).to have_received(:fail!)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'responds with an error' do
|
75
|
+
expect(strategy).to have_received(:fail!)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'with a configured callback' do
|
81
|
+
let(:options) do
|
82
|
+
{ on_single_sign_out: callback }
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'that returns TRUE' do
|
86
|
+
let(:callback) { Proc.new{true} }
|
87
|
+
|
88
|
+
it 'responds with OK' do
|
89
|
+
expect(subject[0]).to eq 200
|
90
|
+
expect(subject[2].body).to eq ['OK']
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'that returns Nil' do
|
95
|
+
let(:callback) { Proc.new{} }
|
96
|
+
|
97
|
+
it 'responds with OK' do
|
98
|
+
expect(subject[0]).to eq 200
|
99
|
+
expect(subject[2].body).to eq ['OK']
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'that returns a tuple' do
|
104
|
+
let(:callback) { Proc.new{ [400,{},'Bad Request'] } }
|
105
|
+
|
106
|
+
it 'responds with OK' do
|
107
|
+
expect(subject[0]).to eq 400
|
108
|
+
expect(subject[2].body).to eq ['Bad Request']
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'that raises an error' do
|
113
|
+
let(:exception) { RuntimeError.new('error' )}
|
114
|
+
let(:callback) { Proc.new{raise exception} }
|
115
|
+
|
116
|
+
before do
|
117
|
+
allow(strategy).to receive(:fail!)
|
118
|
+
subject
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'responds with an error' do
|
122
|
+
expect(strategy).to have_received(:fail!)
|
123
|
+
.with(:logout_request, exception)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe OmniAuth::Strategies::CAS3::ServiceTicketValidator do
|
4
|
+
let(:strategy) do
|
5
|
+
double('strategy',
|
6
|
+
service_validate_url: 'https://example.org/serviceValidate'
|
7
|
+
)
|
8
|
+
end
|
9
|
+
let(:provider_options) do
|
10
|
+
double('provider_options',
|
11
|
+
disable_ssl_verification?: false,
|
12
|
+
ca_path: '/etc/ssl/certsZOMG'
|
13
|
+
)
|
14
|
+
end
|
15
|
+
let(:validator) do
|
16
|
+
OmniAuth::Strategies::CAS3::ServiceTicketValidator.new( strategy, provider_options, '/foo', nil )
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#call' do
|
20
|
+
before do
|
21
|
+
stub_request(:get, 'https://example.org/serviceValidate?')
|
22
|
+
.to_return(status: 200, body: '')
|
23
|
+
end
|
24
|
+
|
25
|
+
subject { validator.call }
|
26
|
+
|
27
|
+
it 'returns itself' do
|
28
|
+
expect(subject).to eq validator
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'uses the configured CA path' do
|
32
|
+
subject
|
33
|
+
expect(provider_options).to have_received :ca_path
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#user_info' do
|
38
|
+
let(:ok_fixture) do
|
39
|
+
File.expand_path(File.join(File.dirname(__FILE__), '../../../fixtures/cas_success.xml'))
|
40
|
+
end
|
41
|
+
let(:service_response) { File.read(ok_fixture) }
|
42
|
+
|
43
|
+
before do
|
44
|
+
stub_request(:get, 'https://example.org/serviceValidate?')
|
45
|
+
.to_return(status: 200, body:service_response)
|
46
|
+
validator.call
|
47
|
+
end
|
48
|
+
|
49
|
+
subject { validator.user_info }
|
50
|
+
|
51
|
+
it 'parses user info from the response' do
|
52
|
+
expect(subject).to include 'user' => 'psegel'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe OmniAuth::Strategies::CAS3, type: :strategy do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
let(:my_cas_provider) { Class.new(OmniAuth::Strategies::CAS3) }
|
7
|
+
before do
|
8
|
+
stub_const 'MyCasProvider', my_cas_provider
|
9
|
+
end
|
10
|
+
let(:app) do
|
11
|
+
Rack::Builder.new {
|
12
|
+
use OmniAuth::Test::PhonySession
|
13
|
+
use MyCasProvider, name: :cas3, host: 'cas.example.org', ssl: false, port: 8080, uid_field: :employeeid
|
14
|
+
run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] }
|
15
|
+
}.to_app
|
16
|
+
end
|
17
|
+
|
18
|
+
# TODO: Verify that these are even useful tests
|
19
|
+
shared_examples_for 'a CAS redirect response' do
|
20
|
+
let(:redirect_params) { 'service=' + Rack::Utils.escape("http://example.org/auth/cas3/callback?url=#{Rack::Utils.escape(return_url)}") }
|
21
|
+
|
22
|
+
before { get url, nil, request_env }
|
23
|
+
|
24
|
+
subject { last_response }
|
25
|
+
|
26
|
+
it { should be_redirect }
|
27
|
+
|
28
|
+
it 'redirects to the CAS server' do
|
29
|
+
expect(subject.headers).to include 'Location' => "http://cas.example.org:8080/login?#{redirect_params}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#cas_url' do
|
34
|
+
let(:params) { Hash.new }
|
35
|
+
let(:provider) { MyCasProvider.new(nil, params) }
|
36
|
+
|
37
|
+
subject { provider.cas_url }
|
38
|
+
|
39
|
+
it 'raises an ArgumentError' do
|
40
|
+
expect{subject}.to raise_error ArgumentError, %r{:host and :login_url MUST be provided}
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with an explicit :url option' do
|
44
|
+
let(:url) { 'https://example.org:8080/my_cas' }
|
45
|
+
let(:params) { super().merge url:url }
|
46
|
+
|
47
|
+
before { subject }
|
48
|
+
|
49
|
+
it { should eq url }
|
50
|
+
|
51
|
+
it 'parses the URL into it the appropriate strategy options' do
|
52
|
+
expect(provider.options).to include ssl:true
|
53
|
+
expect(provider.options).to include host:'example.org'
|
54
|
+
expect(provider.options).to include port:8080
|
55
|
+
expect(provider.options).to include path:'/my_cas'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with explicit URL component' do
|
60
|
+
let(:params) { super().merge host:'example.org', port:1234, ssl:true, path:'/a/path' }
|
61
|
+
|
62
|
+
before { subject }
|
63
|
+
|
64
|
+
it { should eq 'https://example.org:1234/a/path' }
|
65
|
+
|
66
|
+
it 'parses the URL into it the appropriate strategy options' do
|
67
|
+
expect(provider.options).to include ssl:true
|
68
|
+
expect(provider.options).to include host:'example.org'
|
69
|
+
expect(provider.options).to include port:1234
|
70
|
+
expect(provider.options).to include path:'/a/path'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe 'defaults' do
|
76
|
+
subject { MyCasProvider.default_options.to_hash }
|
77
|
+
|
78
|
+
it { should include('ssl' => true) }
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'GET /auth/cas3' do
|
82
|
+
let(:return_url) { 'http://myapp.com/admin/foo' }
|
83
|
+
|
84
|
+
context 'with a referer' do
|
85
|
+
let(:url) { '/auth/cas3' }
|
86
|
+
|
87
|
+
let(:request_env) { { 'HTTP_REFERER' => return_url } }
|
88
|
+
|
89
|
+
it_behaves_like 'a CAS redirect response'
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'with an explicit return URL' do
|
93
|
+
let(:url) { "/auth/cas3?url=#{return_url}" }
|
94
|
+
|
95
|
+
let(:request_env) { {} }
|
96
|
+
|
97
|
+
it_behaves_like 'a CAS redirect response'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe 'GET /auth/cas3/callback' do
|
102
|
+
context 'without a ticket' do
|
103
|
+
before { get '/auth/cas3/callback' }
|
104
|
+
|
105
|
+
subject { last_response }
|
106
|
+
|
107
|
+
it { should be_redirect }
|
108
|
+
|
109
|
+
it 'redirects with a failure message' do
|
110
|
+
expect(subject.headers).to include 'Location' => '/auth/failure?message=no_ticket&strategy=cas3'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'with an invalid ticket' do
|
115
|
+
before do
|
116
|
+
stub_request(:get, /^http:\/\/cas.example.org:8080?\/p3\/serviceValidate\?([^&]+&)?ticket=9391d/).
|
117
|
+
to_return( body: File.read('spec/fixtures/cas_failure.xml') )
|
118
|
+
get '/auth/cas3/callback?ticket=9391d'
|
119
|
+
end
|
120
|
+
|
121
|
+
subject { last_response }
|
122
|
+
|
123
|
+
it { should be_redirect }
|
124
|
+
|
125
|
+
it 'redirects with a failure message' do
|
126
|
+
expect(subject.headers).to include 'Location' => '/auth/failure?message=invalid_ticket&strategy=cas3'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe 'with a valid ticket' do
|
131
|
+
shared_examples :successful_validation do
|
132
|
+
before do
|
133
|
+
stub_request(:get, /^http:\/\/cas.example.org:8080?\/p3\/serviceValidate\?([^&]+&)?ticket=593af/)
|
134
|
+
.with { |request| @request_uri = request.uri.to_s }
|
135
|
+
.to_return( body: File.read("spec/fixtures/#{xml_file_name}") )
|
136
|
+
|
137
|
+
get "/auth/cas3/callback?ticket=593af&url=#{return_url}"
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'strips the ticket parameter from the callback URL' do
|
141
|
+
expect(@request_uri.scan('ticket=').size).to eq 1
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'properly encodes the service URL' do
|
145
|
+
expect(WebMock).to have_requested(:get, 'http://cas.example.org:8080/p3/serviceValidate')
|
146
|
+
.with(query: {
|
147
|
+
ticket: '593af',
|
148
|
+
service: 'http://example.org/auth/cas3/callback?url=' + Rack::Utils.escape('http://127.0.0.10/?some=parameter')
|
149
|
+
})
|
150
|
+
end
|
151
|
+
|
152
|
+
context "request.env['omniauth.auth']" do
|
153
|
+
subject { last_request.env['omniauth.auth'] }
|
154
|
+
|
155
|
+
it { should be_kind_of Hash }
|
156
|
+
|
157
|
+
it 'identifes the provider' do
|
158
|
+
expect(subject.provider).to eq :cas3
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'returns the UID of the user' do
|
162
|
+
expect(subject.uid).to eq '54'
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'the info hash' do
|
166
|
+
subject { last_request.env['omniauth.auth']['info'] }
|
167
|
+
|
168
|
+
it 'includes user info attributes' do
|
169
|
+
expect(subject.name).to eq 'Peter Segel'
|
170
|
+
expect(subject.first_name).to eq 'Peter'
|
171
|
+
expect(subject.last_name).to eq 'Segel'
|
172
|
+
expect(subject.nickname).to eq 'psegel'
|
173
|
+
expect(subject.email).to eq 'psegel@intridea.com'
|
174
|
+
expect(subject.location).to eq 'Washington, D.C.'
|
175
|
+
expect(subject.image).to eq '/images/user.jpg'
|
176
|
+
expect(subject.phone).to eq '555-555-5555'
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context 'the extra hash' do
|
181
|
+
subject { last_request.env['omniauth.auth']['extra'] }
|
182
|
+
|
183
|
+
it 'includes additional user attributes' do
|
184
|
+
expect(subject.user).to eq 'psegel'
|
185
|
+
expect(subject.employeeid).to eq '54'
|
186
|
+
expect(subject.hire_date).to eq '2004-07-13'
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'the credentials hash' do
|
191
|
+
subject { last_request.env['omniauth.auth']['credentials'] }
|
192
|
+
|
193
|
+
it 'has a ticket value' do
|
194
|
+
expect(subject.ticket).to eq '593af'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'calls through to the master app' do
|
200
|
+
expect(last_response.body).to eq 'true'
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
let(:return_url) { 'http://127.0.0.10/?some=parameter' }
|
205
|
+
|
206
|
+
context 'with JASIG flavored XML' do
|
207
|
+
let(:xml_file_name) { 'cas_success_jasig.xml' }
|
208
|
+
|
209
|
+
it_behaves_like :successful_validation
|
210
|
+
end
|
211
|
+
|
212
|
+
context 'with classic XML' do
|
213
|
+
let(:xml_file_name) { 'cas_success.xml' }
|
214
|
+
|
215
|
+
it_behaves_like :successful_validation
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe 'POST /auth/cas3/callback' do
|
221
|
+
describe 'with a Single Sign-Out logoutRequest' do
|
222
|
+
let(:logoutRequest) do
|
223
|
+
%Q[
|
224
|
+
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion\" ID="123abc-1234-ab12-cd34-1234abcd" Version="2.0" IssueInstant="#{Time.now.to_s}">
|
225
|
+
<saml:NameID>@NOT_USED@</saml:NameID>
|
226
|
+
<samlp:SessionIndex>ST-123456-123abc456def</samlp:SessionIndex>
|
227
|
+
</samlp:LogoutRequest>
|
228
|
+
]
|
229
|
+
end
|
230
|
+
|
231
|
+
let(:logout_request) { double('logout_request', call:[200,{},'OK']) }
|
232
|
+
|
233
|
+
subject do
|
234
|
+
post 'auth/cas3/callback', logoutRequest:logoutRequest
|
235
|
+
end
|
236
|
+
|
237
|
+
before do
|
238
|
+
allow_any_instance_of(MyCasProvider)
|
239
|
+
.to receive(:logout_request_service)
|
240
|
+
.and_return double('LogoutRequest', new:logout_request)
|
241
|
+
|
242
|
+
subject
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'initializes a LogoutRequest' do
|
246
|
+
expect(logout_request).to have_received :call
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'awesome_print'
|
3
|
+
|
4
|
+
RSpec.configure do |c|
|
5
|
+
c.filter_run focus: true
|
6
|
+
c.run_all_when_everything_filtered = true
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rack/test'
|
10
|
+
require 'webmock/rspec'
|
11
|
+
require 'omniauth-cas3'
|
12
|
+
|
13
|
+
OmniAuth.config.logger = Logger.new( '/dev/null' )
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: omniauth-cas3
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Derek Lindahl, tduehr
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: omniauth
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.6.6
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.6.6
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: addressable
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.3'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: webmock
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.19.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.19.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.1.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.1.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rack-test
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.6'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.6'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: awesome_print
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: CAS 3.0 Strategy for OmniAuth
|
126
|
+
email:
|
127
|
+
- td@matasano.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".editorconfig"
|
133
|
+
- ".gitignore"
|
134
|
+
- ".travis.yml"
|
135
|
+
- Gemfile
|
136
|
+
- LICENSE
|
137
|
+
- README.md
|
138
|
+
- Rakefile
|
139
|
+
- lib/omniauth-cas3.rb
|
140
|
+
- lib/omniauth/cas3.rb
|
141
|
+
- lib/omniauth/cas3/version.rb
|
142
|
+
- lib/omniauth/strategies/cas3.rb
|
143
|
+
- lib/omniauth/strategies/cas3/logout_request.rb
|
144
|
+
- lib/omniauth/strategies/cas3/service_ticket_validator.rb
|
145
|
+
- omniauth-cas3.gemspec
|
146
|
+
- spec/fixtures/cas_failure.xml
|
147
|
+
- spec/fixtures/cas_success.xml
|
148
|
+
- spec/fixtures/cas_success_jasig.xml
|
149
|
+
- spec/omniauth/strategies/cas3/logout_request_spec.rb
|
150
|
+
- spec/omniauth/strategies/cas3/service_ticket_validator_spec.rb
|
151
|
+
- spec/omniauth/strategies/cas3_spec.rb
|
152
|
+
- spec/spec_helper.rb
|
153
|
+
homepage: https://github.com/tduehr/omniauth-cas3
|
154
|
+
licenses: []
|
155
|
+
metadata: {}
|
156
|
+
post_install_message:
|
157
|
+
rdoc_options: []
|
158
|
+
require_paths:
|
159
|
+
- lib
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
166
|
+
requirements:
|
167
|
+
- - ">="
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
requirements: []
|
171
|
+
rubyforge_project:
|
172
|
+
rubygems_version: 2.4.8
|
173
|
+
signing_key:
|
174
|
+
specification_version: 4
|
175
|
+
summary: CAS 3.0 Strategy for OmniAuth
|
176
|
+
test_files:
|
177
|
+
- spec/fixtures/cas_failure.xml
|
178
|
+
- spec/fixtures/cas_success.xml
|
179
|
+
- spec/fixtures/cas_success_jasig.xml
|
180
|
+
- spec/omniauth/strategies/cas3/logout_request_spec.rb
|
181
|
+
- spec/omniauth/strategies/cas3/service_ticket_validator_spec.rb
|
182
|
+
- spec/omniauth/strategies/cas3_spec.rb
|
183
|
+
- spec/spec_helper.rb
|
184
|
+
has_rdoc:
|