omniauth-iu-cas 1.1.1.1

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