omniauth-iu-cas 1.1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ module OmniAuth
2
+ module Strategies
3
+ class CAS
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 = Nokogiri.parse(@request.params['logoutRequest'])
34
+ name_id = saml.xpath('//saml:NameID').text
35
+ sess_idx = saml.xpath('//samlp:SessionIndex').text
36
+ inject_params(name_id:name_id, session_index:sess_idx)
37
+ @request
38
+ end
39
+ end
40
+
41
+ def inject_params(new_params)
42
+ rack_input = @request.env['rack.input'].read
43
+ params = Rack::Utils.parse_query(rack_input, '&').merge new_params
44
+ @request.env['rack.input'] = StringIO.new(Rack::Utils.build_query(params))
45
+ rescue
46
+ # A no-op intended to ensure that the ensure block is run
47
+ raise
48
+ ensure
49
+ @request.env['rack.input'].rewind
50
+ end
51
+
52
+ def single_sign_out_callback
53
+ @options[:on_single_sign_out]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,112 @@
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
+ VALIDATION_REQUEST_HEADERS = { 'Accept' => '*/*' }
10
+
11
+ attr_reader :success_body
12
+
13
+ # Build a validator from a +configuration+, a
14
+ # +return_to+ URL, and a +ticket+.
15
+ #
16
+ # @param [Hash] options the OmniAuth Strategy options
17
+ # @param [String] return_to_url the URL of this CAS client service
18
+ # @param [String] ticket the service ticket to validate
19
+ def initialize(strategy, options, return_to_url, ticket)
20
+ @options = options
21
+ @uri = URI.parse(strategy.service_validate_url(return_to_url, ticket))
22
+ end
23
+
24
+ # Executes a network request to process the CAS Service Response
25
+ def call
26
+ @response_body = get_service_response_body
27
+ @success_body = find_authentication_success(@response_body)
28
+ self
29
+ end
30
+
31
+ # Request validation of the ticket from the CAS server's
32
+ # serviceValidate (CAS 2.0) function.
33
+ #
34
+ # Swallows all XML parsing errors (and returns +nil+ in those cases).
35
+ #
36
+ # @return [Hash, nil] a user information hash if the response is valid; +nil+ otherwise.
37
+ #
38
+ # @raise any connection errors encountered.
39
+ def user_info
40
+ parse_user_info(@success_body)
41
+ end
42
+
43
+ private
44
+
45
+ # turns an `<cas:authenticationSuccess>` node into a Hash;
46
+ # returns nil if given nil
47
+ def parse_user_info(node)
48
+ return nil if node.nil?
49
+ return node if node.is_a? Hash
50
+ {}.tap do |hash|
51
+ node.children.each do |e|
52
+ node_name = e.name.sub(/^cas:/, '')
53
+ unless e.kind_of?(Nokogiri::XML::Text) || node_name == 'proxies'
54
+ # There are no child elements
55
+ if e.element_children.count == 0
56
+ hash[node_name] = e.content
57
+ elsif e.element_children.count
58
+ # JASIG style extra attributes
59
+ if node_name == 'attributes'
60
+ hash.merge!(parse_user_info(e))
61
+ else
62
+ hash[node_name] = [] if hash[node_name].nil?
63
+ hash[node_name].push(parse_user_info(e))
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ # finds an `<cas:authenticationSuccess>` node in
72
+ # a `<cas:serviceResponse>` body if present; returns nil
73
+ # if the passed body is nil or if there is no such node.
74
+ def find_authentication_success(body)
75
+ return nil if body.nil? || body == ''
76
+
77
+ if body =~ /^(yes|no)[[:space:]]+(.*?)[[:space:]]*$/m
78
+ return nil unless $~[1] == 'yes'
79
+ return { @options[:uid_field].to_s => $~[2] }
80
+ end
81
+
82
+ begin
83
+ doc = Nokogiri::XML(body)
84
+ begin
85
+ doc.xpath('/cas:serviceResponse/cas:authenticationSuccess')
86
+ rescue Nokogiri::XML::XPath::SyntaxError
87
+ doc.xpath('/serviceResponse/authenticationSuccess')
88
+ end
89
+ rescue Nokogiri::XML::XPath::SyntaxError
90
+ nil
91
+ end
92
+ end
93
+
94
+ # retrieves the `<cas:serviceResponse>` XML from the CAS server
95
+ def get_service_response_body
96
+ result = ''
97
+ http = Net::HTTP.new(@uri.host, @uri.port)
98
+ http.use_ssl = @uri.port == 443 || @uri.instance_of?(URI::HTTPS)
99
+ if http.use_ssl?
100
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @options.disable_ssl_verification?
101
+ http.ca_path = @options.ca_path
102
+ end
103
+ http.start do |c|
104
+ response = c.get "#{@uri.path}?#{@uri.query}", VALIDATION_REQUEST_HEADERS.dup
105
+ result = response.body
106
+ end
107
+ result
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,28 @@
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", "Daniel Pierce"]
6
+ gem.email = ["dlindahl@customink.com", "dlpierce@indiana.edu"]
7
+ gem.summary = %q{CAS Strategy for OmniAuth with custom IU extensions}
8
+ gem.description = gem.summary
9
+ gem.homepage = "https://github.com/IUBLibTech/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 = "omniauth-iu-cas"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Omniauth::Cas::VERSION
17
+
18
+ gem.add_dependency 'omniauth', '~> 1.2'
19
+ gem.add_dependency 'nokogiri', '~> 1.5'
20
+ gem.add_dependency 'addressable', '~> 2.3'
21
+
22
+ gem.add_development_dependency 'rake', '~> 10.0'
23
+ gem.add_development_dependency 'webmock', '~> 2.3.1'
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,4 @@
1
+ <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
2
+ <cas:authenticationFailure>
3
+ </cas:authenticationFailure>
4
+ </cas:serviceResponse>
@@ -0,0 +1,17 @@
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:roles>senator</cas:roles>
14
+ <cas:roles>lobbyist</cas:roles>
15
+ <cas:roles>financier</cas:roles>
16
+ </cas:authenticationSuccess>
17
+ </cas:serviceResponse>
@@ -0,0 +1,19 @@
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:roles>senator</cas:roles>
15
+ <cas:roles>lobbyist</cas:roles>
16
+ <cas:roles>financier</cas:roles>
17
+ </cas:attributes>
18
+ </cas:authenticationSuccess>
19
+ </cas:serviceResponse>
@@ -0,0 +1 @@
1
+ no
@@ -0,0 +1 @@
1
+ yes
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ describe OmniAuth::Strategies::CAS::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
+ subject
35
+ end
36
+
37
+ it 'are parsed and injected into the Rack Request parameters' do
38
+ expect(@rack_input).to eq 'name_id=%40NOT_USED%40&session_index=ST-123456-123abc456def'
39
+ end
40
+
41
+ context 'that raise when parsed' do
42
+ let(:env) { { 'rack.input' => nil } }
43
+
44
+ before do
45
+ allow(strategy).to receive(:fail!)
46
+ subject
47
+ expect(strategy).to have_received(:fail!)
48
+ end
49
+
50
+ it 'responds with an error' do
51
+ expect(strategy).to have_received(:fail!)
52
+ end
53
+ end
54
+ end
55
+
56
+ describe 'with a configured callback' do
57
+ let(:options) do
58
+ { on_single_sign_out: callback }
59
+ end
60
+
61
+ context 'that returns TRUE' do
62
+ let(:callback) { Proc.new{true} }
63
+
64
+ it 'responds with OK' do
65
+ expect(subject[0]).to eq 200
66
+ expect(subject[2].body).to eq ['OK']
67
+ end
68
+ end
69
+
70
+ context 'that returns Nil' do
71
+ let(:callback) { Proc.new{} }
72
+
73
+ it 'responds with OK' do
74
+ expect(subject[0]).to eq 200
75
+ expect(subject[2].body).to eq ['OK']
76
+ end
77
+ end
78
+
79
+ context 'that returns a tuple' do
80
+ let(:callback) { Proc.new{ [400,{},'Bad Request'] } }
81
+
82
+ it 'responds with OK' do
83
+ expect(subject[0]).to eq 400
84
+ expect(subject[2].body).to eq ['Bad Request']
85
+ end
86
+ end
87
+
88
+ context 'that raises an error' do
89
+ let(:exception) { RuntimeError.new('error' )}
90
+ let(:callback) { Proc.new{raise exception} }
91
+
92
+ before do
93
+ allow(strategy).to receive(:fail!)
94
+ subject
95
+ end
96
+
97
+ it 'responds with an error' do
98
+ expect(strategy).to have_received(:fail!)
99
+ .with(:logout_request, exception)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,55 @@
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
+ 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::CAS::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,339 @@
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,
14
+ name: :cas,
15
+ host: 'cas.example.org',
16
+ ssl: false,
17
+ port: 8080,
18
+ uid_field: :employeeid,
19
+ cassvc: 'ANY',
20
+ fetch_raw_info: Proc.new { |v, opts, ticket, info, node|
21
+ info.nil? or info.empty? ? {} : {
22
+ "roles" => node.xpath('//cas:roles').map(&:text),
23
+ }
24
+ }
25
+ run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] }
26
+ }.to_app
27
+ end
28
+
29
+ # TODO: Verify that these are even useful tests
30
+ shared_examples_for 'a CAS redirect response' do
31
+ let(:redirect_params) { 'cassvc=ANY&casurl=' + Rack::Utils.escape("http://example.org/auth/cas/callback?url=#{Rack::Utils.escape(return_url)}") }
32
+
33
+ before { get url, nil, request_env }
34
+
35
+ subject { last_response }
36
+
37
+ it { should be_redirect }
38
+
39
+ it 'redirects to the CAS server' do
40
+ expect(subject.headers).to include 'Location' => "http://cas.example.org:8080/login?#{redirect_params}"
41
+ end
42
+ end
43
+
44
+ describe '#cas_url' do
45
+ let(:params) { Hash.new }
46
+ let(:provider) { MyCasProvider.new(nil, params) }
47
+
48
+ subject { provider.cas_url }
49
+
50
+ it 'raises an ArgumentError' do
51
+ expect{subject}.to raise_error ArgumentError, %r{:host and :login_url MUST be provided}
52
+ end
53
+
54
+ context 'with an explicit :url option' do
55
+ let(:url) { 'https://example.org:8080/my_cas' }
56
+ let(:params) { super().merge url:url }
57
+
58
+ before { subject }
59
+
60
+ it { should eq url }
61
+
62
+ it 'parses the URL into it the appropriate strategy options' do
63
+ expect(provider.options).to include ssl:true
64
+ expect(provider.options).to include host:'example.org'
65
+ expect(provider.options).to include port:8080
66
+ expect(provider.options).to include path:'/my_cas'
67
+ end
68
+ end
69
+
70
+ context 'with explicit URL component' do
71
+ let(:params) { super().merge host:'example.org', port:1234, ssl:true, path:'/a/path' }
72
+
73
+ before { subject }
74
+
75
+ it { should eq 'https://example.org:1234/a/path' }
76
+
77
+ it 'parses the URL into it the appropriate strategy options' do
78
+ expect(provider.options).to include ssl:true
79
+ expect(provider.options).to include host:'example.org'
80
+ expect(provider.options).to include port:1234
81
+ expect(provider.options).to include path:'/a/path'
82
+ end
83
+ end
84
+ end
85
+
86
+ describe 'defaults' do
87
+ subject { MyCasProvider.default_options.to_hash }
88
+
89
+ it { should include('ssl' => true) }
90
+ end
91
+
92
+ describe 'GET /auth/cas' do
93
+ let(:return_url) { 'http://myapp.com/admin/foo' }
94
+
95
+ context 'with a referer' do
96
+ let(:url) { '/auth/cas' }
97
+
98
+ let(:request_env) { { 'HTTP_REFERER' => return_url } }
99
+
100
+ it_behaves_like 'a CAS redirect response'
101
+ end
102
+
103
+ context 'with an explicit return URL' do
104
+ let(:url) { "/auth/cas?url=#{return_url}" }
105
+
106
+ let(:request_env) { {} }
107
+
108
+ it_behaves_like 'a CAS redirect response'
109
+ end
110
+ end
111
+
112
+ describe 'GET /auth/cas/callback' do
113
+ context 'without a ticket' do
114
+ before { get '/auth/cas/callback' }
115
+
116
+ subject { last_response }
117
+
118
+ it { should be_redirect }
119
+
120
+ it 'redirects with a failure message' do
121
+ expect(subject.headers).to include 'Location' => '/auth/failure?message=no_ticket&strategy=cas'
122
+ end
123
+ end
124
+
125
+ context 'with an invalid ticket' do
126
+ before do
127
+ stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?cassvc=ANY&casticket=9391d/).
128
+ to_return( body: File.read('spec/fixtures/cas_failure.xml') )
129
+ get '/auth/cas/callback?cassvc=ANY&casticket=9391d'
130
+ end
131
+
132
+ subject { last_response }
133
+
134
+ it { should be_redirect }
135
+
136
+ it 'redirects with a failure message' do
137
+ expect(subject.headers).to include 'Location' => '/auth/failure?message=invalid_ticket&strategy=cas'
138
+ end
139
+ end
140
+
141
+ describe 'GET /auth/cas/callback with an invalid ticket at IU' do
142
+ before do
143
+ stub_request(:get, /^http:\/\/cas.example.org(:8080)?\/serviceValidate\?([^&]+&)?cassvc=ANY&casticket=9391d/).
144
+ to_return( :body => File.read('spec/fixtures/iu_cas_failure') )
145
+ get '/auth/cas/callback?cassvc=ANY&casticket=9391d'
146
+ end
147
+
148
+ subject { last_response }
149
+
150
+ it { should be_redirect }
151
+ it 'should have a failure message' do
152
+ subject.headers['Location'].should == "/auth/failure?message=invalid_ticket&strategy=cas"
153
+ end
154
+ end
155
+
156
+ describe 'with a valid ticket' do
157
+ shared_examples :successful_validation do
158
+ before do
159
+ stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?cassvc=ANY&casticket=593af/)
160
+ .with { |request| @request_uri = request.uri.to_s }
161
+ .to_return( body: File.read("spec/fixtures/#{xml_file_name}") )
162
+
163
+ get "/auth/cas/callback?cassvc=ANY&casticket=593af&url=#{return_url}"
164
+ end
165
+
166
+ it 'strips the casticket parameter from the callback URL' do
167
+ expect(@request_uri.scan('casticket=').size).to eq 1
168
+ end
169
+
170
+ it 'strips the cassvc parameter from the callback URL' do
171
+ expect(@request_uri.scan('cassvc=').size).to eq 1
172
+ end
173
+
174
+ it 'properly encodes the service URL' do
175
+ expect(WebMock).to have_requested(:get, 'http://cas.example.org:8080/serviceValidate')
176
+ .with(query: {
177
+ cassvc: 'ANY',
178
+ casticket: '593af',
179
+ casurl: 'http://example.org/auth/cas/callback?url=' + Rack::Utils.escape('http://127.0.0.10/?some=parameter')
180
+ })
181
+ end
182
+
183
+ context "request.env['omniauth.auth']" do
184
+ subject { last_request.env['omniauth.auth'] }
185
+
186
+ it { should be_kind_of Hash }
187
+
188
+ it 'identifes the provider' do
189
+ expect(subject.provider).to eq :cas
190
+ end
191
+
192
+ it 'returns the UID of the user' do
193
+ expect(subject.uid).to eq '54'
194
+ end
195
+
196
+ context 'the info hash' do
197
+ subject { last_request.env['omniauth.auth']['info'] }
198
+
199
+ it 'includes user info attributes' do
200
+ expect(subject.name).to eq 'Peter Segel'
201
+ expect(subject.first_name).to eq 'Peter'
202
+ expect(subject.last_name).to eq 'Segel'
203
+ expect(subject.nickname).to eq 'psegel'
204
+ expect(subject.email).to eq 'psegel@intridea.com'
205
+ expect(subject.location).to eq 'Washington, D.C.'
206
+ expect(subject.image).to eq '/images/user.jpg'
207
+ expect(subject.phone).to eq '555-555-5555'
208
+ end
209
+ end
210
+
211
+ context 'the extra hash' do
212
+ subject { last_request.env['omniauth.auth']['extra'] }
213
+
214
+ it 'includes additional user attributes' do
215
+ expect(subject.user).to eq 'psegel'
216
+ expect(subject.employeeid).to eq '54'
217
+ expect(subject.hire_date).to eq '2004-07-13'
218
+ expect(subject.roles).to eq %w(senator lobbyist financier)
219
+ end
220
+ end
221
+
222
+ context 'the credentials hash' do
223
+ subject { last_request.env['omniauth.auth']['credentials'] }
224
+
225
+ it 'has a ticket value' do
226
+ expect(subject.casticket).to eq '593af'
227
+ end
228
+ end
229
+ end
230
+
231
+ it 'calls through to the master app' do
232
+ expect(last_response.body).to eq 'true'
233
+ end
234
+ end
235
+
236
+ let(:return_url) { 'http://127.0.0.10/?some=parameter' }
237
+
238
+ context 'with JASIG flavored XML' do
239
+ let(:xml_file_name) { 'cas_success_jasig.xml' }
240
+
241
+ it_behaves_like :successful_validation
242
+ end
243
+
244
+ context 'with classic XML' do
245
+ let(:xml_file_name) { 'cas_success.xml' }
246
+
247
+ it_behaves_like :successful_validation
248
+ end
249
+ end
250
+
251
+ describe 'GET /auth/cas/callback with a valid IU CAS 1.0 ticket' do
252
+ class MyCasProvider < OmniAuth::Strategies::CAS; end # TODO: Not really needed. just an alias but it requires the :name option which might confuse users...
253
+ def app
254
+ Rack::Builder.new {
255
+ use OmniAuth::Test::PhonySession
256
+ use MyCasProvider,
257
+ name: :cas,
258
+ host: 'cas.example.org',
259
+ ssl: false,
260
+ port: 8080,
261
+ cassvc: 'ANY'
262
+ run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] }
263
+ }.to_app
264
+ end
265
+
266
+ let(:return_url) { "http://127.0.0.10/?some=parameter" }
267
+ before do
268
+ stub_request(:get, /^http:\/\/cas.example.org(:8080)?\/serviceValidate\?([^&]+&)?cassvc=ANY&casticket=593af/).
269
+ with { |request| @request_uri = request.uri.to_s }.
270
+ to_return( :body => File.read('spec/fixtures/iu_cas_success') )
271
+
272
+ get "/auth/cas/callback?cassvc=ANY&casticket=593af&url=#{return_url}"
273
+ end
274
+
275
+ context "request.env['omniauth.auth']" do
276
+ subject { last_request.env['omniauth.auth'] }
277
+ it { should be_kind_of Hash }
278
+ it 'identifies the provider' do
279
+ expect(subject.provider).to eq :cas
280
+ end
281
+ it 'returns the UID of the user' do
282
+ expect(subject.uid).to eq 'psegel'
283
+ end
284
+ context "the info hash" do
285
+ subject { last_request.env['omniauth.auth']['info'] }
286
+ it "contains only the user" do
287
+ expect(subject.size).to eq 1
288
+ expect(subject.nickname).to eq 'psegel'
289
+ end
290
+ end
291
+ context "the extra hash" do
292
+ subject { last_request.env['omniauth.auth']['extra'] }
293
+ it "contains only the user" do
294
+ expect(subject.size).to eq 1
295
+ expect(subject.user).to eq 'psegel'
296
+ end
297
+ end
298
+ context "the credentials hash" do
299
+ subject { last_request.env['omniauth.auth']['credentials'] }
300
+ it 'contains only the casticket' do
301
+ expect(subject.size).to eq 1
302
+ expect(subject.casticket).to eq '593af'
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
308
+
309
+ describe 'POST /auth/cas/callback' do
310
+ describe 'with a Single Sign-Out logoutRequest' do
311
+ let(:logoutRequest) do
312
+ %Q[
313
+ <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}">
314
+ <saml:NameID>@NOT_USED@</saml:NameID>
315
+ <samlp:SessionIndex>ST-123456-123abc456def</samlp:SessionIndex>
316
+ </samlp:LogoutRequest>
317
+ ]
318
+ end
319
+
320
+ let(:logout_request) { double('logout_request', call:[200,{},'OK']) }
321
+
322
+ subject do
323
+ post 'auth/cas/callback', logoutRequest:logoutRequest
324
+ end
325
+
326
+ before do
327
+ allow_any_instance_of(MyCasProvider)
328
+ .to receive(:logout_request_service)
329
+ .and_return double('LogoutRequest', new:logout_request)
330
+
331
+ subject
332
+ end
333
+
334
+ it 'initializes a LogoutRequest' do
335
+ expect(logout_request).to have_received :call
336
+ end
337
+ end
338
+ end
339
+ end