eriko-omniauth-cas 1.0.4
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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +112 -0
- data/Rakefile +15 -0
- data/lib/omniauth/cas/version.rb +5 -0
- data/lib/omniauth/cas.rb +2 -0
- data/lib/omniauth/strategies/cas/logout_request.rb +65 -0
- data/lib/omniauth/strategies/cas/service_ticket_validator.rb +100 -0
- data/lib/omniauth/strategies/cas.rb +206 -0
- data/lib/omniauth-cas.rb +1 -0
- data/omniauth-cas.gemspec +29 -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/cas/logout_request_spec.rb +123 -0
- data/spec/omniauth/strategies/cas/service_ticket_validator_spec.rb +32 -0
- data/spec/omniauth/strategies/cas_spec.rb +250 -0
- data/spec/spec_helper.rb +19 -0
- metadata +199 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 38e39ed4f218491f1a36012d8ff4251a51412aa0
|
|
4
|
+
data.tar.gz: 373a1424d1cc93b5ba445f492c0137b17042a909
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a011844945919e0c7c026ee4911dd30812f94da7073c1eacb53b8fda94074e29c1a3df01bb510ca058002e9938d6eac8671b95f1c2003c0ab18ccfdfe45c29ff
|
|
7
|
+
data.tar.gz: e5a94f7882809980c417a1925f07116ea9bf62382aa05341a60105a265ecc08dbf483b2283340bba67785f17ae7021c9fdb37b44f3078853a7fb64ba1ddc173c
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.0.0-p247
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2011 Derek Lindahl and CustomInk, LLC
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# OmniAuth CAS Strategy [![Gem Version][version_badge]][version] [![Build Status][travis_status]][travis]
|
|
2
|
+
|
|
3
|
+
[version_badge]: https://badge.fury.io/rb/omniauth-cas.png
|
|
4
|
+
[version]: http://badge.fury.io/rb/omniauth-cas
|
|
5
|
+
[travis]: http://travis-ci.org/dlindahl/omniauth-cas
|
|
6
|
+
[travis_status]: https://secure.travis-ci.org/dlindahl/omniauth-cas.png
|
|
7
|
+
[releases]: https://github.com/dlindahl/omniauth-cas/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.
|
|
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-cas'
|
|
20
|
+
|
|
21
|
+
And then execute:
|
|
22
|
+
|
|
23
|
+
$ bundle
|
|
24
|
+
|
|
25
|
+
Or install it yourself as:
|
|
26
|
+
|
|
27
|
+
$ gem install omniauth-cas
|
|
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 :cas, 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
|
+
* `logout_url` - The URL to use to logout a user. Defaults to `'/logout'`.
|
|
56
|
+
* `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.
|
|
57
|
+
* `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).
|
|
58
|
+
* `ca_path` - Optional when `ssl` is `true`. Sets path of a CA certification directory. See [Net::HTTP][net_http] for more details.
|
|
59
|
+
* `disable_ssl_verification` - Optional when `ssl` is true. Disables verification.
|
|
60
|
+
* `on_single_sign_out` - Callback used when a [CAS 3.1 Single Sign Out][sso]
|
|
61
|
+
request is received.
|
|
62
|
+
|
|
63
|
+
Configurable options for values returned by CAS:
|
|
64
|
+
|
|
65
|
+
* `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).
|
|
66
|
+
* `name_key` - The data attribute containing user first and last name. Defaults to `'name'`.
|
|
67
|
+
* `email_key` - The data attribute containing user email address. Defaults to `'email'`.
|
|
68
|
+
* `first_name_key` - The data attribute containing user first name. Defaults to `'first_name'`.
|
|
69
|
+
* `last_name_key` - The data attribute containing user last name. Defaults to `'last_name'`.
|
|
70
|
+
* `location_key` - The data attribute containing user location/address. Defaults to `'location'`.
|
|
71
|
+
* `image_key` - The data attribute containing user image/picture. Defaults to `'image'`.
|
|
72
|
+
* `phone_key` - The data attribute containing user contact phone number. Defaults to `'phone'`.
|
|
73
|
+
|
|
74
|
+
## Migrating from OmniAuth 0.3
|
|
75
|
+
|
|
76
|
+
Given the following OmniAuth 0.3 configuration:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
provider :CAS, cas_server: 'https://cas.example.com/cas/'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Your new settings should look similar to this:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
provider :cas,
|
|
86
|
+
host: 'cas.example.com',
|
|
87
|
+
login_url: '/cas/login',
|
|
88
|
+
service_validate_url: '/cas/serviceValidate'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If you encounter problems wih SSL certificates you may want to set the `ca_path` parameter or activate `disable_ssl_verification` (not recommended).
|
|
92
|
+
|
|
93
|
+
## Contributing
|
|
94
|
+
|
|
95
|
+
1. Fork it
|
|
96
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
97
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
98
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
99
|
+
5. Create new Pull Request
|
|
100
|
+
|
|
101
|
+
## Thanks
|
|
102
|
+
|
|
103
|
+
Special thanks go out to the following people
|
|
104
|
+
|
|
105
|
+
* Phillip Aldridge (@iterateNZ) and JB Barth (@jbbarth) for helping out with Issue #3
|
|
106
|
+
* Elber Ribeiro (@dynaum) for Ubuntu SSL configuration support
|
|
107
|
+
* @rbq for README updates and OmniAuth 0.3 migration guide
|
|
108
|
+
|
|
109
|
+
[old_omniauth_cas]: https://github.com/intridea/omniauth/blob/0-3-stable/oa-enterprise/lib/omniauth/strategies/cas.rb
|
|
110
|
+
[document_up]: http://dlindahl.github.com/omniauth-cas/
|
|
111
|
+
[net_http]: http://ruby-doc.org/stdlib-1.9.3/libdoc/net/http/rdoc/Net/HTTP.html
|
|
112
|
+
[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
|
data/lib/omniauth/cas.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module OmniAuth
|
|
2
|
+
module Strategies
|
|
3
|
+
class CAS
|
|
4
|
+
class LogoutRequest
|
|
5
|
+
def initialize(strategy, request)
|
|
6
|
+
@strategy = strategy
|
|
7
|
+
@request = request
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(options = {})
|
|
11
|
+
@options = options
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
result = single_sign_out_callback.call *logout_request
|
|
15
|
+
rescue StandardError => err
|
|
16
|
+
return @strategy.fail! :logout_request, err
|
|
17
|
+
else
|
|
18
|
+
result = [200,{},'OK'] if result == true || result.nil?
|
|
19
|
+
ensure
|
|
20
|
+
return unless result
|
|
21
|
+
|
|
22
|
+
# TODO: Why does ActionPack::Response return [status,headers,body]
|
|
23
|
+
# when Rack::Response#new wants [body,status,headers]? Additionally,
|
|
24
|
+
# why does Rack::Response differ in argument order from the usual
|
|
25
|
+
# Rack-like [status,headers,body] array?
|
|
26
|
+
return Rack::Response.new(result[2],result[0],result[1]).finish
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def logout_request
|
|
33
|
+
@logout_request ||= begin
|
|
34
|
+
saml = Nokogiri.parse @request.params['logoutRequest']
|
|
35
|
+
name_id = saml.xpath('//saml:NameID').text
|
|
36
|
+
sess_idx = saml.xpath('//samlp:SessionIndex').text
|
|
37
|
+
|
|
38
|
+
inject_params name_id:name_id, session_index:sess_idx
|
|
39
|
+
|
|
40
|
+
@request
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def inject_params(new_params)
|
|
45
|
+
rack_input = @request.env['rack.input'].read
|
|
46
|
+
params = Rack::Utils.parse_query(rack_input, '&').merge new_params
|
|
47
|
+
@request.env['rack.input'] = StringIO.new(Rack::Utils.build_query(params))
|
|
48
|
+
rescue
|
|
49
|
+
# A no-op intended to ensure that the ensure block is run
|
|
50
|
+
raise
|
|
51
|
+
ensure
|
|
52
|
+
@request.env['rack.input'].rewind
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def single_sign_out_callback
|
|
56
|
+
@options.fetch :on_single_sign_out, _fallback_callback_
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def _fallback_callback_
|
|
60
|
+
Proc.new {}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'net/https'
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
|
|
5
|
+
module OmniAuth
|
|
6
|
+
module Strategies
|
|
7
|
+
class CAS
|
|
8
|
+
class ServiceTicketValidator
|
|
9
|
+
|
|
10
|
+
VALIDATION_REQUEST_HEADERS = { 'Accept' => '*/*' }
|
|
11
|
+
|
|
12
|
+
# Build a validator from a +configuration+, a
|
|
13
|
+
# +return_to+ URL, and a +ticket+.
|
|
14
|
+
#
|
|
15
|
+
# @param [Hash] options the OmniAuth Strategy options
|
|
16
|
+
# @param [String] return_to_url the URL of this CAS client service
|
|
17
|
+
# @param [String] ticket the service ticket to validate
|
|
18
|
+
def initialize(strategy, options, return_to_url, ticket)
|
|
19
|
+
@options = options
|
|
20
|
+
@uri = URI.parse(strategy.service_validate_url(return_to_url, ticket))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Request validation of the ticket from the CAS server's
|
|
24
|
+
# serviceValidate (CAS 2.0) function.
|
|
25
|
+
#
|
|
26
|
+
# Swallows all XML parsing errors (and returns +nil+ in those cases).
|
|
27
|
+
#
|
|
28
|
+
# @return [Hash, nil] a user information hash if the response is valid; +nil+ otherwise.
|
|
29
|
+
#
|
|
30
|
+
# @raise any connection errors encountered.
|
|
31
|
+
def user_info
|
|
32
|
+
parse_user_info( find_authentication_success( get_service_response_body ) )
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# turns an `<cas:authenticationSuccess>` node into a Hash;
|
|
38
|
+
# returns nil if given nil
|
|
39
|
+
def parse_user_info(node)
|
|
40
|
+
return nil if node.nil?
|
|
41
|
+
|
|
42
|
+
{}.tap do |hash|
|
|
43
|
+
node.children.each do |e|
|
|
44
|
+
node_name = e.name.sub(/^cas:/, '')
|
|
45
|
+
unless e.kind_of?(Nokogiri::XML::Text) ||
|
|
46
|
+
node_name == 'proxies'
|
|
47
|
+
# There are no child elements
|
|
48
|
+
if e.element_children.count == 0
|
|
49
|
+
hash[node_name] = e.content
|
|
50
|
+
elsif e.element_children.count
|
|
51
|
+
# JASIG style extra attributes
|
|
52
|
+
if node_name == 'attributes'
|
|
53
|
+
hash.merge! parse_user_info e
|
|
54
|
+
else
|
|
55
|
+
hash[node_name] = [] if hash[node_name].nil?
|
|
56
|
+
hash[node_name].push parse_user_info e
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# finds an `<cas:authenticationSuccess>` node in
|
|
65
|
+
# a `<cas:serviceResponse>` body if present; returns nil
|
|
66
|
+
# if the passed body is nil or if there is no such node.
|
|
67
|
+
def find_authentication_success(body)
|
|
68
|
+
return nil if body.nil? || body == ''
|
|
69
|
+
begin
|
|
70
|
+
doc = Nokogiri::XML(body)
|
|
71
|
+
begin
|
|
72
|
+
doc.xpath('/cas:serviceResponse/cas:authenticationSuccess')
|
|
73
|
+
rescue Nokogiri::XML::XPath::SyntaxError
|
|
74
|
+
doc.xpath('/serviceResponse/authenticationSuccess')
|
|
75
|
+
end
|
|
76
|
+
rescue Nokogiri::XML::XPath::SyntaxError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# retrieves the `<cas:serviceResponse>` XML from the CAS server
|
|
82
|
+
def get_service_response_body
|
|
83
|
+
result = ''
|
|
84
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
|
85
|
+
http.use_ssl = @uri.port == 443 || @uri.instance_of?(URI::HTTPS)
|
|
86
|
+
if http.use_ssl?
|
|
87
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @options.disable_ssl_verification?
|
|
88
|
+
http.ca_path = @options.ca_path
|
|
89
|
+
end
|
|
90
|
+
http.start do |c|
|
|
91
|
+
response = c.get "#{@uri.path}?#{@uri.query}", VALIDATION_REQUEST_HEADERS.dup
|
|
92
|
+
result = response.body
|
|
93
|
+
end
|
|
94
|
+
result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
require 'omniauth/strategy'
|
|
2
|
+
require 'addressable/uri'
|
|
3
|
+
|
|
4
|
+
module OmniAuth
|
|
5
|
+
module Strategies
|
|
6
|
+
class CAS
|
|
7
|
+
include OmniAuth::Strategy
|
|
8
|
+
|
|
9
|
+
# Custom Exceptions
|
|
10
|
+
class MissingCASTicket < StandardError; end
|
|
11
|
+
class InvalidCASTicket < StandardError; end
|
|
12
|
+
|
|
13
|
+
autoload :ServiceTicketValidator, 'omniauth/strategies/cas/service_ticket_validator'
|
|
14
|
+
autoload :LogoutRequest, 'omniauth/strategies/cas/logout_request'
|
|
15
|
+
|
|
16
|
+
attr_accessor :raw_info
|
|
17
|
+
alias_method :user_info, :raw_info
|
|
18
|
+
|
|
19
|
+
option :name, :cas # 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, '/serviceValidate'
|
|
26
|
+
option :login_url, '/login'
|
|
27
|
+
option :logout_url, '/logout'
|
|
28
|
+
# Make all the keys configurable with some defaults set here
|
|
29
|
+
option :uid_field, 'user'
|
|
30
|
+
option :name_key, 'name'
|
|
31
|
+
option :email_key, 'email'
|
|
32
|
+
option :first_name_key, 'first_name'
|
|
33
|
+
option :last_name_key, 'last_name'
|
|
34
|
+
option :location_key, 'location'
|
|
35
|
+
option :image_key, 'image'
|
|
36
|
+
option :phone_key, 'phone'
|
|
37
|
+
|
|
38
|
+
# As required by https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema
|
|
39
|
+
AuthHashSchemaKeys = %w{name email first_name last_name location image phone}
|
|
40
|
+
info do
|
|
41
|
+
prune!({
|
|
42
|
+
:name => raw_info[ @options[:name_key].to_s ],
|
|
43
|
+
:email => raw_info[ @options[:email_key].to_s ],
|
|
44
|
+
:first_name => raw_info[ @options[:first_name_key].to_s ],
|
|
45
|
+
:last_name => raw_info[ @options[:last_name_key].to_s ],
|
|
46
|
+
:location => raw_info[ @options[:location_key].to_s ],
|
|
47
|
+
:image => raw_info[ @options[:image_key].to_s ],
|
|
48
|
+
:phone => raw_info[ @options[:phone_key].to_s ]
|
|
49
|
+
})
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
extra do
|
|
53
|
+
prune! raw_info.delete_if{ |k,v| AuthHashSchemaKeys.include?(k) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
uid do
|
|
57
|
+
raw_info[ @options[:uid_field].to_s ]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
credentials do
|
|
61
|
+
prune!({
|
|
62
|
+
:ticket => @ticket
|
|
63
|
+
})
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def callback_phase
|
|
68
|
+
if on_sso_path?
|
|
69
|
+
single_sign_out_phase
|
|
70
|
+
else
|
|
71
|
+
@ticket = request.params['ticket']
|
|
72
|
+
return fail!(:no_ticket, MissingCASTicket.new('No CAS Ticket')) unless @ticket
|
|
73
|
+
|
|
74
|
+
self.raw_info = ServiceTicketValidator.new(self, @options, callback_url, @ticket).user_info
|
|
75
|
+
|
|
76
|
+
return fail!(:invalid_ticket, InvalidCASTicket.new('Invalid CAS Ticket')) if raw_info.empty?
|
|
77
|
+
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def request_phase
|
|
83
|
+
service_url = append_params( callback_url, return_url )
|
|
84
|
+
|
|
85
|
+
[
|
|
86
|
+
302,
|
|
87
|
+
{
|
|
88
|
+
'Location' => login_url( service_url ),
|
|
89
|
+
'Content-Type' => 'text/plain'
|
|
90
|
+
},
|
|
91
|
+
["You are being redirected to CAS for sign-in."]
|
|
92
|
+
]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def on_sso_path?
|
|
96
|
+
request.post? && request.params.has_key?( 'logoutRequest' )
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def single_sign_out_phase
|
|
100
|
+
logout_request_service.new(self, request).call @options
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Build a CAS host with protocol and port
|
|
104
|
+
#
|
|
105
|
+
#
|
|
106
|
+
def cas_url
|
|
107
|
+
extract_url if @options['url']
|
|
108
|
+
|
|
109
|
+
validate_cas_setup
|
|
110
|
+
|
|
111
|
+
@cas_url ||= begin
|
|
112
|
+
uri = Addressable::URI.new
|
|
113
|
+
uri.host = @options.host
|
|
114
|
+
uri.scheme = @options.ssl ? 'https' : 'http'
|
|
115
|
+
uri.port = @options.port
|
|
116
|
+
uri.path = @options.path
|
|
117
|
+
|
|
118
|
+
uri.to_s
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def extract_url
|
|
123
|
+
url = Addressable::URI.parse( @options.delete('url') )
|
|
124
|
+
|
|
125
|
+
@options.merge!(
|
|
126
|
+
'host' => url.host,
|
|
127
|
+
'port' => url.port,
|
|
128
|
+
'path' => url.path,
|
|
129
|
+
'ssl' => url.scheme == 'https'
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def validate_cas_setup
|
|
134
|
+
if @options.host.nil? || @options.login_url.nil?
|
|
135
|
+
raise ArgumentError.new(":host and :login_url MUST be provided")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Build a service-validation URL from +service+ and +ticket+.
|
|
140
|
+
# If +service+ has a ticket param, first remove it. URL-encode
|
|
141
|
+
# +service+ and add it and the +ticket+ as paraemters to the
|
|
142
|
+
# CAS serviceValidate URL.
|
|
143
|
+
#
|
|
144
|
+
# @param [String] service the service (a.k.a. return-to) URL
|
|
145
|
+
# @param [String] ticket the ticket to validate
|
|
146
|
+
#
|
|
147
|
+
# @return [String] a URL like `http://cas.mycompany.com/serviceValidate?service=...&ticket=...`
|
|
148
|
+
def service_validate_url(service_url, ticket)
|
|
149
|
+
service_url = Addressable::URI.parse( service_url )
|
|
150
|
+
service_url.query_values = service_url.query_values.tap { |qs| qs.delete('ticket') }
|
|
151
|
+
|
|
152
|
+
cas_url + append_params(@options.service_validate_url, { :service => service_url.to_s, :ticket => ticket })
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Build a CAS login URL from +service+.
|
|
156
|
+
#
|
|
157
|
+
# @param [String] service the service (a.k.a. return-to) URL
|
|
158
|
+
#
|
|
159
|
+
# @return [String] a URL like `http://cas.mycompany.com/login?service=...`
|
|
160
|
+
def login_url(service)
|
|
161
|
+
cas_url + append_params( @options.login_url, { :service => service })
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Adds URL-escaped +parameters+ to +base+.
|
|
165
|
+
#
|
|
166
|
+
# @param [String] base the base URL
|
|
167
|
+
# @param [String] params the parameters to append to the URL
|
|
168
|
+
#
|
|
169
|
+
# @return [String] the new joined URL.
|
|
170
|
+
def append_params(base, params)
|
|
171
|
+
params = params.each { |k,v| v = Rack::Utils.escape(v) }
|
|
172
|
+
|
|
173
|
+
Addressable::URI.parse(base).tap do |base_uri|
|
|
174
|
+
base_uri.query_values = (base_uri.query_values || {}).merge( params )
|
|
175
|
+
end.to_s
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
# Deletes Hash pairs with `nil` values.
|
|
181
|
+
# From https://github.com/mkdynamic/omniauth-facebook/blob/972ed5e3456bcaed7df1f55efd7c05c216c8f48e/lib/omniauth/strategies/facebook.rb#L122-127
|
|
182
|
+
def prune!(hash)
|
|
183
|
+
hash.delete_if do |_, value|
|
|
184
|
+
prune!(value) if value.is_a?(Hash)
|
|
185
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def return_url
|
|
190
|
+
# If the request already has a `url` parameter, then it will already be appended to the callback URL.
|
|
191
|
+
if request.params and request.params['url']
|
|
192
|
+
{}
|
|
193
|
+
else
|
|
194
|
+
{ :url => request.referer }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def logout_request_service
|
|
199
|
+
LogoutRequest
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
OmniAuth.config.add_camelization 'cas', 'CAS'
|
data/lib/omniauth-cas.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "omniauth/cas"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require File.expand_path('../lib/omniauth/cas/version', __FILE__)
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.authors = ["Derek Lindahl","Erik Ordway"]
|
|
6
|
+
gem.email = ["dlindahl@customink.com","eriko@jumpsuit.org"]
|
|
7
|
+
gem.summary = %q{CAS Strategy for OmniAuth}
|
|
8
|
+
gem.description = gem.summary
|
|
9
|
+
gem.homepage = "https://github.com/eriko/omniauth-cas"
|
|
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 = "eriko-omniauth-cas"
|
|
15
|
+
gem.require_paths = ["lib"]
|
|
16
|
+
gem.version = Omniauth::Cas::VERSION
|
|
17
|
+
|
|
18
|
+
gem.add_dependency 'omniauth', '~> 1.1.0'
|
|
19
|
+
gem.add_dependency 'nokogiri', '~> 1.5'
|
|
20
|
+
gem.add_dependency 'addressable', '~> 2.3'
|
|
21
|
+
|
|
22
|
+
gem.add_development_dependency 'rake', '~> 0.9'
|
|
23
|
+
gem.add_development_dependency 'webmock', '~> 1.16.0'
|
|
24
|
+
gem.add_development_dependency 'simplecov', '~> 0.8.1'
|
|
25
|
+
gem.add_development_dependency 'rspec', '~> 2.99.0.beta1'
|
|
26
|
+
gem.add_development_dependency 'rack-test', '~> 0.6'
|
|
27
|
+
|
|
28
|
+
gem.add_development_dependency 'awesome_print'
|
|
29
|
+
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,123 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe OmniAuth::Strategies::CAS::LogoutRequest do
|
|
4
|
+
let(:strategy) { double('strategy') }
|
|
5
|
+
|
|
6
|
+
let(:env) do
|
|
7
|
+
{ 'rack.input' => StringIO.new('','r') }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
let(:request) { double('request', params:params, env:env) }
|
|
11
|
+
|
|
12
|
+
let(:params) { { 'url' => url, 'logoutRequest' => logoutRequest } }
|
|
13
|
+
|
|
14
|
+
let(:url) { 'http://notes.dev/signed_in' }
|
|
15
|
+
|
|
16
|
+
let(:logoutRequest) do
|
|
17
|
+
%Q[
|
|
18
|
+
<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}">
|
|
19
|
+
<saml:NameID>@NOT_USED@</saml:NameID>
|
|
20
|
+
<samlp:SessionIndex>ST-123456-123abc456def</samlp:SessionIndex>
|
|
21
|
+
</samlp:LogoutRequest>
|
|
22
|
+
]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
subject { described_class.new(strategy, request).call(options) }
|
|
26
|
+
|
|
27
|
+
describe 'with no configured SSO callback' do
|
|
28
|
+
let(:options) { {} }
|
|
29
|
+
|
|
30
|
+
it 'responds with OK' do
|
|
31
|
+
expect(subject[0]).to eq 200
|
|
32
|
+
expect(subject[2].body).to eq ['OK']
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe 'SAML attributes' do
|
|
37
|
+
let(:callback) { Proc.new{} }
|
|
38
|
+
|
|
39
|
+
let(:options) do
|
|
40
|
+
{
|
|
41
|
+
on_single_sign_out: callback
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
before do
|
|
46
|
+
@rack_input = nil
|
|
47
|
+
|
|
48
|
+
callback.stub(:call) do |req|
|
|
49
|
+
@rack_input = req.env['rack.input'].read
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
subject
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'are parsed and injected into the Rack Request parameters' do
|
|
57
|
+
expect(@rack_input).to eq 'name_id=%40NOT_USED%40&session_index=ST-123456-123abc456def'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context 'that raise when parsed' do
|
|
61
|
+
let(:env) { { 'rack.input' => nil } }
|
|
62
|
+
|
|
63
|
+
before do
|
|
64
|
+
strategy.stub :fail!
|
|
65
|
+
subject
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'responds with an error' do
|
|
69
|
+
expect(strategy).to have_received(:fail!)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe 'with a configured callback' do
|
|
75
|
+
let(:options) do
|
|
76
|
+
{
|
|
77
|
+
on_single_sign_out: callback
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'that returns TRUE' do
|
|
82
|
+
let(:callback) { Proc.new{true} }
|
|
83
|
+
|
|
84
|
+
it 'responds with OK' do
|
|
85
|
+
expect(subject[0]).to eq 200
|
|
86
|
+
expect(subject[2].body).to eq ['OK']
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
context 'that returns Nil' do
|
|
91
|
+
let(:callback) { Proc.new{} }
|
|
92
|
+
|
|
93
|
+
it 'responds with OK' do
|
|
94
|
+
expect(subject[0]).to eq 200
|
|
95
|
+
expect(subject[2].body).to eq ['OK']
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
context 'that returns a tuple' do
|
|
100
|
+
let(:callback) { Proc.new{ [400,{},'Bad Request'] } }
|
|
101
|
+
|
|
102
|
+
it 'responds with OK' do
|
|
103
|
+
expect(subject[0]).to eq 400
|
|
104
|
+
expect(subject[2].body).to eq ['Bad Request']
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
context 'that raises an error' do
|
|
109
|
+
let(:exception) { RuntimeError.new('error' )}
|
|
110
|
+
let(:callback) { Proc.new{raise exception} }
|
|
111
|
+
|
|
112
|
+
before do
|
|
113
|
+
strategy.stub :fail!
|
|
114
|
+
subject
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'responds with an error' do
|
|
118
|
+
expect(strategy).to have_received(:fail!)
|
|
119
|
+
.with(:logout_request, exception)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe OmniAuth::Strategies::CAS::ServiceTicketValidator do
|
|
4
|
+
let(:strategy) do
|
|
5
|
+
double('strategy',
|
|
6
|
+
service_validate_url: 'https://example.org/serviceValidate'
|
|
7
|
+
)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
let(:provider_options) do
|
|
11
|
+
double('provider_options',
|
|
12
|
+
disable_ssl_verification?: false,
|
|
13
|
+
ca_path: '/etc/ssl/certsZOMG'
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:validator) do
|
|
18
|
+
OmniAuth::Strategies::CAS::ServiceTicketValidator.new( strategy, provider_options, '/foo', nil )
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '#user_info' do
|
|
22
|
+
before do
|
|
23
|
+
stub_request(:get, 'https://example.org/serviceValidate?')
|
|
24
|
+
.to_return(status: 200, body: '')
|
|
25
|
+
validator.user_info
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'uses the configured CA path' do
|
|
29
|
+
expect(provider_options).to have_received :ca_path
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe OmniAuth::Strategies::CAS, type: :strategy do
|
|
4
|
+
include Rack::Test::Methods
|
|
5
|
+
|
|
6
|
+
let(:my_cas_provider) { Class.new(OmniAuth::Strategies::CAS) }
|
|
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: :cas, 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/cas/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
|
+
|
|
36
|
+
let(:provider) { MyCasProvider.new(nil, params) }
|
|
37
|
+
|
|
38
|
+
subject { provider.cas_url }
|
|
39
|
+
|
|
40
|
+
it 'raises an ArgumentError' do
|
|
41
|
+
expect{subject}.to raise_error ArgumentError, %r{:host and :login_url MUST be provided}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'with an explicit :url option' do
|
|
45
|
+
let(:url) { 'https://example.org:8080/my_cas' }
|
|
46
|
+
|
|
47
|
+
let(:params) { super().merge url:url }
|
|
48
|
+
|
|
49
|
+
before { subject }
|
|
50
|
+
|
|
51
|
+
it { should eq url }
|
|
52
|
+
|
|
53
|
+
it 'parses the URL into it the appropriate strategy options' do
|
|
54
|
+
expect(provider.options).to include ssl:true
|
|
55
|
+
expect(provider.options).to include host:'example.org'
|
|
56
|
+
expect(provider.options).to include port:8080
|
|
57
|
+
expect(provider.options).to include path:'/my_cas'
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context 'with explicit URL component' do
|
|
62
|
+
let(:params) { super().merge host:'example.org', port:1234, ssl:true, path:'/a/path' }
|
|
63
|
+
|
|
64
|
+
before { subject }
|
|
65
|
+
|
|
66
|
+
it { should eq 'https://example.org:1234/a/path' }
|
|
67
|
+
|
|
68
|
+
it 'parses the URL into it the appropriate strategy options' do
|
|
69
|
+
expect(provider.options).to include ssl:true
|
|
70
|
+
expect(provider.options).to include host:'example.org'
|
|
71
|
+
expect(provider.options).to include port:1234
|
|
72
|
+
expect(provider.options).to include path:'/a/path'
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe 'defaults' do
|
|
78
|
+
subject { MyCasProvider.default_options.to_hash }
|
|
79
|
+
|
|
80
|
+
it { should include('ssl' => true) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe 'GET /auth/cas' do
|
|
84
|
+
let(:return_url) { 'http://myapp.com/admin/foo' }
|
|
85
|
+
|
|
86
|
+
context 'with a referer' do
|
|
87
|
+
let(:url) { '/auth/cas' }
|
|
88
|
+
|
|
89
|
+
let(:request_env) { { 'HTTP_REFERER' => return_url } }
|
|
90
|
+
|
|
91
|
+
it_behaves_like 'a CAS redirect response'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
context 'with an explicit return URL' do
|
|
95
|
+
let(:url) { "/auth/cas?url=#{return_url}" }
|
|
96
|
+
|
|
97
|
+
let(:request_env) { {} }
|
|
98
|
+
|
|
99
|
+
it_behaves_like 'a CAS redirect response'
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe 'GET /auth/cas/callback' do
|
|
104
|
+
context 'without a ticket' do
|
|
105
|
+
before { get '/auth/cas/callback' }
|
|
106
|
+
|
|
107
|
+
subject { last_response }
|
|
108
|
+
|
|
109
|
+
it { should be_redirect }
|
|
110
|
+
|
|
111
|
+
it 'redirects with a failure message' do
|
|
112
|
+
expect(subject.headers).to include 'Location' => '/auth/failure?message=no_ticket&strategy=cas'
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
context 'with an invalid ticket' do
|
|
117
|
+
before do
|
|
118
|
+
stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?ticket=9391d/).
|
|
119
|
+
to_return( body: File.read('spec/fixtures/cas_failure.xml') )
|
|
120
|
+
get '/auth/cas/callback?ticket=9391d'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
subject { last_response }
|
|
124
|
+
|
|
125
|
+
it { should be_redirect }
|
|
126
|
+
|
|
127
|
+
it 'redirects with a failure message' do
|
|
128
|
+
expect(subject.headers).to include 'Location' => '/auth/failure?message=invalid_ticket&strategy=cas'
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe 'with a valid ticket' do
|
|
133
|
+
shared_examples :successful_validation do
|
|
134
|
+
before do
|
|
135
|
+
stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?ticket=593af/)
|
|
136
|
+
.with { |request| @request_uri = request.uri.to_s }
|
|
137
|
+
.to_return( body: File.read("spec/fixtures/#{xml_file_name}") )
|
|
138
|
+
|
|
139
|
+
get "/auth/cas/callback?ticket=593af&url=#{return_url}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'strips the ticket parameter from the callback URL' do
|
|
143
|
+
expect(@request_uri.scan('ticket=').size).to eq 1
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'properly encodes the service URL' do
|
|
147
|
+
WebMock.should have_requested(:get, 'http://cas.example.org:8080/serviceValidate')
|
|
148
|
+
.with(query: {
|
|
149
|
+
ticket: '593af',
|
|
150
|
+
service: 'http://example.org/auth/cas/callback?url=' + Rack::Utils.escape('http://127.0.0.10/?some=parameter')
|
|
151
|
+
})
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
context "request.env['omniauth.auth']" do
|
|
155
|
+
subject { last_request.env['omniauth.auth'] }
|
|
156
|
+
|
|
157
|
+
it { should be_kind_of Hash }
|
|
158
|
+
|
|
159
|
+
it 'identifes the provider' do
|
|
160
|
+
expect(subject.provider).to eq :cas
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'returns the UID of the user' do
|
|
164
|
+
expect(subject.uid).to eq '54'
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
context 'the info hash' do
|
|
168
|
+
subject { last_request.env['omniauth.auth']['info'] }
|
|
169
|
+
|
|
170
|
+
it 'includes user info attributes' do
|
|
171
|
+
expect(subject.name).to eq 'Peter Segel'
|
|
172
|
+
expect(subject.first_name).to eq 'Peter'
|
|
173
|
+
expect(subject.last_name).to eq 'Segel'
|
|
174
|
+
expect(subject.email).to eq 'psegel@intridea.com'
|
|
175
|
+
expect(subject.location).to eq 'Washington, D.C.'
|
|
176
|
+
expect(subject.image).to eq '/images/user.jpg'
|
|
177
|
+
expect(subject.phone).to eq '555-555-5555'
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
context 'the extra hash' do
|
|
182
|
+
subject { last_request.env['omniauth.auth']['extra'] }
|
|
183
|
+
|
|
184
|
+
it 'includes additional user attributes' do
|
|
185
|
+
expect(subject.user).to eq 'psegel'
|
|
186
|
+
expect(subject.employeeid).to eq '54'
|
|
187
|
+
expect(subject.hire_date).to eq '2004-07-13'
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
context 'the credentials hash' do
|
|
192
|
+
subject { last_request.env['omniauth.auth']['credentials'] }
|
|
193
|
+
|
|
194
|
+
it 'has a ticket value' do
|
|
195
|
+
expect(subject.ticket).to eq '593af'
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'calls through to the master app' do
|
|
201
|
+
expect(last_response.body).to eq 'true'
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
let(:return_url) { 'http://127.0.0.10/?some=parameter' }
|
|
206
|
+
|
|
207
|
+
context 'with JASIG flavored XML' do
|
|
208
|
+
let(:xml_file_name) { 'cas_success_jasig.xml' }
|
|
209
|
+
|
|
210
|
+
it_behaves_like :successful_validation
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
context 'with classic XML' do
|
|
214
|
+
let(:xml_file_name) { 'cas_success.xml' }
|
|
215
|
+
|
|
216
|
+
it_behaves_like :successful_validation
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
describe 'POST /auth/cas/callback' do
|
|
222
|
+
describe 'with a Single Sign-Out logoutRequest' do
|
|
223
|
+
let(:logoutRequest) do
|
|
224
|
+
%Q[
|
|
225
|
+
<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}">
|
|
226
|
+
<saml:NameID>@NOT_USED@</saml:NameID>
|
|
227
|
+
<samlp:SessionIndex>ST-123456-123abc456def</samlp:SessionIndex>
|
|
228
|
+
</samlp:LogoutRequest>
|
|
229
|
+
]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
let(:logout_request) { double('logout_request', call:[200,{},'OK']) }
|
|
233
|
+
|
|
234
|
+
subject do
|
|
235
|
+
post 'auth/cas/callback', logoutRequest:logoutRequest
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
before do
|
|
239
|
+
MyCasProvider.any_instance.stub(: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,19 @@
|
|
|
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
|
+
c.treat_symbols_as_metadata_keys_with_true_values = true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require 'simplecov'
|
|
11
|
+
SimpleCov.start do
|
|
12
|
+
add_filter '.bundle'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require 'rack/test'
|
|
16
|
+
require 'webmock/rspec'
|
|
17
|
+
require 'omniauth-cas'
|
|
18
|
+
|
|
19
|
+
OmniAuth.config.logger = Logger.new( '/dev/null' )
|
metadata
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: eriko-omniauth-cas
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Derek Lindahl
|
|
8
|
+
- Erik Ordway
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2014-02-10 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: omniauth
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - ~>
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: 1.1.0
|
|
21
|
+
type: :runtime
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - ~>
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: 1.1.0
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: nokogiri
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - ~>
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '1.5'
|
|
35
|
+
type: :runtime
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ~>
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '1.5'
|
|
42
|
+
- !ruby/object:Gem::Dependency
|
|
43
|
+
name: addressable
|
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ~>
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '2.3'
|
|
49
|
+
type: :runtime
|
|
50
|
+
prerelease: false
|
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ~>
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '2.3'
|
|
56
|
+
- !ruby/object:Gem::Dependency
|
|
57
|
+
name: rake
|
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ~>
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0.9'
|
|
63
|
+
type: :development
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ~>
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0.9'
|
|
70
|
+
- !ruby/object:Gem::Dependency
|
|
71
|
+
name: webmock
|
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ~>
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: 1.16.0
|
|
77
|
+
type: :development
|
|
78
|
+
prerelease: false
|
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ~>
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: 1.16.0
|
|
84
|
+
- !ruby/object:Gem::Dependency
|
|
85
|
+
name: simplecov
|
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ~>
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: 0.8.1
|
|
91
|
+
type: :development
|
|
92
|
+
prerelease: false
|
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ~>
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: 0.8.1
|
|
98
|
+
- !ruby/object:Gem::Dependency
|
|
99
|
+
name: rspec
|
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ~>
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: 2.99.0.beta1
|
|
105
|
+
type: :development
|
|
106
|
+
prerelease: false
|
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ~>
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: 2.99.0.beta1
|
|
112
|
+
- !ruby/object:Gem::Dependency
|
|
113
|
+
name: rack-test
|
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ~>
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '0.6'
|
|
119
|
+
type: :development
|
|
120
|
+
prerelease: false
|
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ~>
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0.6'
|
|
126
|
+
- !ruby/object:Gem::Dependency
|
|
127
|
+
name: awesome_print
|
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
|
129
|
+
requirements:
|
|
130
|
+
- - '>='
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
version: '0'
|
|
133
|
+
type: :development
|
|
134
|
+
prerelease: false
|
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - '>='
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: '0'
|
|
140
|
+
description: CAS Strategy for OmniAuth
|
|
141
|
+
email:
|
|
142
|
+
- dlindahl@customink.com
|
|
143
|
+
- eriko@jumpsuit.org
|
|
144
|
+
executables: []
|
|
145
|
+
extensions: []
|
|
146
|
+
extra_rdoc_files: []
|
|
147
|
+
files:
|
|
148
|
+
- .gitignore
|
|
149
|
+
- .ruby-version
|
|
150
|
+
- .travis.yml
|
|
151
|
+
- Gemfile
|
|
152
|
+
- LICENSE
|
|
153
|
+
- README.md
|
|
154
|
+
- Rakefile
|
|
155
|
+
- lib/omniauth-cas.rb
|
|
156
|
+
- lib/omniauth/cas.rb
|
|
157
|
+
- lib/omniauth/cas/version.rb
|
|
158
|
+
- lib/omniauth/strategies/cas.rb
|
|
159
|
+
- lib/omniauth/strategies/cas/logout_request.rb
|
|
160
|
+
- lib/omniauth/strategies/cas/service_ticket_validator.rb
|
|
161
|
+
- omniauth-cas.gemspec
|
|
162
|
+
- spec/fixtures/cas_failure.xml
|
|
163
|
+
- spec/fixtures/cas_success.xml
|
|
164
|
+
- spec/fixtures/cas_success_jasig.xml
|
|
165
|
+
- spec/omniauth/strategies/cas/logout_request_spec.rb
|
|
166
|
+
- spec/omniauth/strategies/cas/service_ticket_validator_spec.rb
|
|
167
|
+
- spec/omniauth/strategies/cas_spec.rb
|
|
168
|
+
- spec/spec_helper.rb
|
|
169
|
+
homepage: https://github.com/eriko/omniauth-cas
|
|
170
|
+
licenses: []
|
|
171
|
+
metadata: {}
|
|
172
|
+
post_install_message:
|
|
173
|
+
rdoc_options: []
|
|
174
|
+
require_paths:
|
|
175
|
+
- lib
|
|
176
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - '>='
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0'
|
|
181
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
182
|
+
requirements:
|
|
183
|
+
- - '>='
|
|
184
|
+
- !ruby/object:Gem::Version
|
|
185
|
+
version: '0'
|
|
186
|
+
requirements: []
|
|
187
|
+
rubyforge_project:
|
|
188
|
+
rubygems_version: 2.0.14
|
|
189
|
+
signing_key:
|
|
190
|
+
specification_version: 4
|
|
191
|
+
summary: CAS Strategy for OmniAuth
|
|
192
|
+
test_files:
|
|
193
|
+
- spec/fixtures/cas_failure.xml
|
|
194
|
+
- spec/fixtures/cas_success.xml
|
|
195
|
+
- spec/fixtures/cas_success_jasig.xml
|
|
196
|
+
- spec/omniauth/strategies/cas/logout_request_spec.rb
|
|
197
|
+
- spec/omniauth/strategies/cas/service_ticket_validator_spec.rb
|
|
198
|
+
- spec/omniauth/strategies/cas_spec.rb
|
|
199
|
+
- spec/spec_helper.rb
|