omniauth-cas 1.1.0.beta.1 → 1.1.0.pre.rc.1

Sign up to get free protection for your applications and to get access to all the features.
data/omniauth-cas.gemspec CHANGED
@@ -4,8 +4,8 @@ require File.expand_path('../lib/omniauth/cas/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Derek Lindahl"]
6
6
  gem.email = ["dlindahl@customink.com"]
7
- # gem.description = %q{TODO: Write a gem description}
8
7
  gem.summary = %q{CAS Strategy for OmniAuth}
8
+ gem.description = gem.summary
9
9
  gem.homepage = "https://github.com/dlindahl/omniauth-cas"
10
10
 
11
11
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -15,16 +15,14 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Omniauth::Cas::VERSION
17
17
 
18
- gem.add_dependency 'omniauth', '~> 1.1.0'
18
+ gem.add_dependency 'omniauth', '~> 1.2.0'
19
19
  gem.add_dependency 'nokogiri', '~> 1.5'
20
20
  gem.add_dependency 'addressable', '~> 2.3'
21
21
 
22
- gem.add_development_dependency 'rake', '~> 0.9'
23
- gem.add_development_dependency 'webmock', '~> 1.8.11'
24
- gem.add_development_dependency 'simplecov', '~> 0.7.1'
25
- gem.add_development_dependency 'rspec', '~> 2.11'
22
+ gem.add_development_dependency 'rake', '~> 10.0'
23
+ gem.add_development_dependency 'webmock', '~> 1.19.0'
24
+ gem.add_development_dependency 'rspec', '~> 3.1.0'
26
25
  gem.add_development_dependency 'rack-test', '~> 0.6'
27
26
 
28
27
  gem.add_development_dependency 'awesome_print'
29
-
30
28
  end
@@ -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,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
@@ -1,33 +1,55 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe OmniAuth::Strategies::CAS::ServiceTicketValidator do
4
- let(:strategy_stub) do
5
- stub('strategy stub',
4
+ let(:strategy) do
5
+ double('strategy',
6
6
  service_validate_url: 'https://example.org/serviceValidate'
7
7
  )
8
8
  end
9
-
10
9
  let(:provider_options) do
11
- stub('provider options',
10
+ double('provider_options',
12
11
  disable_ssl_verification?: false,
13
12
  ca_path: '/etc/ssl/certsZOMG'
14
13
  )
15
14
  end
16
-
17
15
  let(:validator) do
18
- OmniAuth::Strategies::CAS::ServiceTicketValidator.new( strategy_stub, provider_options, '/foo', nil )
16
+ OmniAuth::Strategies::CAS::ServiceTicketValidator.new( strategy, provider_options, '/foo', nil )
19
17
  end
20
18
 
21
- describe '#user_info' do
22
- subject do
23
- stub_request(:get, 'https://example.org/serviceValidate?').to_return(status: 200, body: '')
24
- validator.user_info
19
+ describe '#call' do
20
+ before do
21
+ stub_request(:get, 'https://example.org/serviceValidate?')
22
+ .to_return(status: 200, body: '')
25
23
  end
26
24
 
27
- it 'should use the configured CA path' do
28
- provider_options.should_receive :ca_path
25
+ subject { validator.call }
29
26
 
27
+ it 'returns itself' do
28
+ expect(subject).to eq validator
29
+ end
30
+
31
+ it 'uses the configured CA path' do
30
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'
31
53
  end
32
54
  end
33
- end
55
+ end
@@ -3,11 +3,14 @@ require 'spec_helper'
3
3
  describe OmniAuth::Strategies::CAS, type: :strategy do
4
4
  include Rack::Test::Methods
5
5
 
6
- class MyCasProvider < OmniAuth::Strategies::CAS; end # TODO: Not really needed. just an alias but it requires the :name option which might confuse users...
7
- def app
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
8
11
  Rack::Builder.new {
9
12
  use OmniAuth::Test::PhonySession
10
- use MyCasProvider, name: :cas, host: 'cas.example.org', ssl: false, port: 8080, uid_key: :employeeid
13
+ use MyCasProvider, name: :cas, host: 'cas.example.org', ssl: false, port: 8080, uid_field: :employeeid
11
14
  run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] }
12
15
  }.to_app
13
16
  end
@@ -22,13 +25,56 @@ describe OmniAuth::Strategies::CAS, type: :strategy do
22
25
 
23
26
  it { should be_redirect }
24
27
 
25
- it 'should redirect to the CAS server' do
26
- subject.headers['Location'].should == 'http://cas.example.org:8080/login?' + redirect_params
28
+ it 'redirects to the CAS server' do
29
+ expect(subject.headers).to include 'Location' => "http://cas.example.org:8080/login?#{redirect_params}"
30
+ end
31
+ end
32
+
33
+ describe '#cas_url' do
34
+ let(:params) { Hash.new }
35
+ let(:provider) { MyCasProvider.new(nil, params) }
36
+
37
+ subject { provider.cas_url }
38
+
39
+ it 'raises an ArgumentError' do
40
+ expect{subject}.to raise_error ArgumentError, %r{:host and :login_url MUST be provided}
41
+ end
42
+
43
+ context 'with an explicit :url option' do
44
+ let(:url) { 'https://example.org:8080/my_cas' }
45
+ let(:params) { super().merge url:url }
46
+
47
+ before { subject }
48
+
49
+ it { should eq url }
50
+
51
+ it 'parses the URL into it the appropriate strategy options' do
52
+ expect(provider.options).to include ssl:true
53
+ expect(provider.options).to include host:'example.org'
54
+ expect(provider.options).to include port:8080
55
+ expect(provider.options).to include path:'/my_cas'
56
+ end
57
+ end
58
+
59
+ context 'with explicit URL component' do
60
+ let(:params) { super().merge host:'example.org', port:1234, ssl:true, path:'/a/path' }
61
+
62
+ before { subject }
63
+
64
+ it { should eq 'https://example.org:1234/a/path' }
65
+
66
+ it 'parses the URL into it the appropriate strategy options' do
67
+ expect(provider.options).to include ssl:true
68
+ expect(provider.options).to include host:'example.org'
69
+ expect(provider.options).to include port:1234
70
+ expect(provider.options).to include path:'/a/path'
71
+ end
27
72
  end
28
73
  end
29
74
 
30
75
  describe 'defaults' do
31
76
  subject { MyCasProvider.default_options.to_hash }
77
+
32
78
  it { should include('ssl' => true) }
33
79
  end
34
80
 
@@ -52,102 +98,153 @@ describe OmniAuth::Strategies::CAS, type: :strategy do
52
98
  end
53
99
  end
54
100
 
55
- describe 'GET /auth/cas/callback without a ticket' do
56
- before { get '/auth/cas/callback' }
57
-
58
- subject { last_response }
101
+ describe 'GET /auth/cas/callback' do
102
+ context 'without a ticket' do
103
+ before { get '/auth/cas/callback' }
59
104
 
60
- it { should be_redirect }
105
+ subject { last_response }
61
106
 
62
- it 'should have a failure message' do
63
- subject.headers['Location'].should == '/auth/failure?message=no_ticket&strategy=cas'
64
- end
65
- end
107
+ it { should be_redirect }
66
108
 
67
- describe 'GET /auth/cas/callback with an invalid ticket' do
68
- before do
69
- stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?ticket=9391d/).
70
- to_return( body: File.read('spec/fixtures/cas_failure.xml') )
71
- get '/auth/cas/callback?ticket=9391d'
72
- end
73
-
74
- subject { last_response }
75
-
76
- it { should be_redirect }
77
-
78
- it 'should have a failure message' do
79
- subject.headers['Location'].should == '/auth/failure?message=invalid_ticket&strategy=cas'
109
+ it 'redirects with a failure message' do
110
+ expect(subject.headers).to include 'Location' => '/auth/failure?message=no_ticket&strategy=cas'
111
+ end
80
112
  end
81
- end
82
-
83
- describe 'GET /auth/cas/callback with a valid ticket' do
84
- let(:return_url) { 'http://127.0.0.10/?some=parameter' }
85
113
 
86
- before do
87
- stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?ticket=593af/)
88
- .with { |request| @request_uri = request.uri.to_s }
89
- .to_return( body: File.read('spec/fixtures/cas_success.xml') )
114
+ context 'with an invalid ticket' do
115
+ before do
116
+ stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?ticket=9391d/).
117
+ to_return( body: File.read('spec/fixtures/cas_failure.xml') )
118
+ get '/auth/cas/callback?ticket=9391d'
119
+ end
90
120
 
91
- get "/auth/cas/callback?ticket=593af&url=#{return_url}"
92
- end
121
+ subject { last_response }
93
122
 
94
- it 'should strip the ticket parameter from the callback URL' do
95
- @request_uri.scan('ticket=').length.should == 1
96
- end
123
+ it { should be_redirect }
97
124
 
98
- it 'should properly encode the service URL' do
99
- WebMock.should have_requested(:get, 'http://cas.example.org:8080/serviceValidate')
100
- .with(query: {
101
- ticket: '593af',
102
- service: 'http://example.org/auth/cas/callback?url=' + Rack::Utils.escape('http://127.0.0.10/?some=parameter')
103
- })
125
+ it 'redirects with a failure message' do
126
+ expect(subject.headers).to include 'Location' => '/auth/failure?message=invalid_ticket&strategy=cas'
127
+ end
104
128
  end
105
129
 
106
- context "request.env['omniauth.auth']" do
107
- subject { last_request.env['omniauth.auth'] }
108
-
109
- it { should be_kind_of Hash }
130
+ describe 'with a valid ticket' do
131
+ shared_examples :successful_validation do
132
+ before do
133
+ stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?ticket=593af/)
134
+ .with { |request| @request_uri = request.uri.to_s }
135
+ .to_return( body: File.read("spec/fixtures/#{xml_file_name}") )
136
+
137
+ get "/auth/cas/callback?ticket=593af&url=#{return_url}"
138
+ end
139
+
140
+ it 'strips the ticket parameter from the callback URL' do
141
+ expect(@request_uri.scan('ticket=').size).to eq 1
142
+ end
143
+
144
+ it 'properly encodes the service URL' do
145
+ expect(WebMock).to have_requested(:get, 'http://cas.example.org:8080/serviceValidate')
146
+ .with(query: {
147
+ ticket: '593af',
148
+ service: 'http://example.org/auth/cas/callback?url=' + Rack::Utils.escape('http://127.0.0.10/?some=parameter')
149
+ })
150
+ end
151
+
152
+ context "request.env['omniauth.auth']" do
153
+ subject { last_request.env['omniauth.auth'] }
154
+
155
+ it { should be_kind_of Hash }
156
+
157
+ it 'identifes the provider' do
158
+ expect(subject.provider).to eq :cas
159
+ end
160
+
161
+ it 'returns the UID of the user' do
162
+ expect(subject.uid).to eq '54'
163
+ end
164
+
165
+ context 'the info hash' do
166
+ subject { last_request.env['omniauth.auth']['info'] }
167
+
168
+ it 'includes user info attributes' do
169
+ expect(subject.name).to eq 'Peter Segel'
170
+ expect(subject.first_name).to eq 'Peter'
171
+ expect(subject.last_name).to eq 'Segel'
172
+ expect(subject.nickname).to eq 'psegel'
173
+ expect(subject.email).to eq 'psegel@intridea.com'
174
+ expect(subject.location).to eq 'Washington, D.C.'
175
+ expect(subject.image).to eq '/images/user.jpg'
176
+ expect(subject.phone).to eq '555-555-5555'
177
+ end
178
+ end
179
+
180
+ context 'the extra hash' do
181
+ subject { last_request.env['omniauth.auth']['extra'] }
182
+
183
+ it 'includes additional user attributes' do
184
+ expect(subject.user).to eq 'psegel'
185
+ expect(subject.employeeid).to eq '54'
186
+ expect(subject.hire_date).to eq '2004-07-13'
187
+ end
188
+ end
189
+
190
+ context 'the credentials hash' do
191
+ subject { last_request.env['omniauth.auth']['credentials'] }
192
+
193
+ it 'has a ticket value' do
194
+ expect(subject.ticket).to eq '593af'
195
+ end
196
+ end
197
+ end
198
+
199
+ it 'calls through to the master app' do
200
+ expect(last_response.body).to eq 'true'
201
+ end
202
+ end
110
203
 
111
- its(:provider) { should == :cas }
204
+ let(:return_url) { 'http://127.0.0.10/?some=parameter' }
112
205
 
113
- its(:uid) { should == '54'}
206
+ context 'with JASIG flavored XML' do
207
+ let(:xml_file_name) { 'cas_success_jasig.xml' }
114
208
 
115
- context 'the info hash' do
116
- subject { last_request.env['omniauth.auth']['info'] }
209
+ it_behaves_like :successful_validation
210
+ end
117
211
 
118
- it { should have(6).items }
212
+ context 'with classic XML' do
213
+ let(:xml_file_name) { 'cas_success.xml' }
119
214
 
120
- its(:name) { should == 'Peter Segel' }
121
- its(:first_name) { should == 'Peter' }
122
- its(:last_name) { should == 'Segel' }
123
- its(:email) { should == 'psegel@intridea.com' }
124
- its(:location) { should == 'Washington, D.C.' }
125
- its(:image) { should == '/images/user.jpg' }
126
- its(:phone) { should == '555-555-5555' }
215
+ it_behaves_like :successful_validation
127
216
  end
217
+ end
218
+ end
128
219
 
129
- context 'the extra hash' do
130
- subject { last_request.env['omniauth.auth']['extra'] }
220
+ describe 'POST /auth/cas/callback' do
221
+ describe 'with a Single Sign-Out logoutRequest' do
222
+ let(:logoutRequest) do
223
+ %Q[
224
+ <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion\" ID="123abc-1234-ab12-cd34-1234abcd" Version="2.0" IssueInstant="#{Time.now.to_s}">
225
+ <saml:NameID>@NOT_USED@</saml:NameID>
226
+ <samlp:SessionIndex>ST-123456-123abc456def</samlp:SessionIndex>
227
+ </samlp:LogoutRequest>
228
+ ]
229
+ end
131
230
 
132
- it { should have(3).items }
231
+ let(:logout_request) { double('logout_request', call:[200,{},'OK']) }
133
232
 
134
- its(:user) { should == 'psegel' }
135
- its(:employeeid) { should == '54' }
136
- its(:hire_date) { should == '2004-07-13' }
233
+ subject do
234
+ post 'auth/cas/callback', logoutRequest:logoutRequest
137
235
  end
138
236
 
139
- context 'the credentials hash' do
140
- subject { last_request.env['omniauth.auth']['credentials'] }
141
-
142
- it { should have(1).items }
237
+ before do
238
+ allow_any_instance_of(MyCasProvider)
239
+ .to receive(:logout_request_service)
240
+ .and_return double('LogoutRequest', new:logout_request)
143
241
 
144
- its(:ticket) { should == '593af' }
242
+ subject
145
243
  end
146
- end
147
244
 
148
- it 'should call through to the master app' do
149
- last_response.body.should == 'true'
245
+ it 'initializes a LogoutRequest' do
246
+ expect(logout_request).to have_received :call
247
+ end
150
248
  end
151
249
  end
152
-
153
250
  end