omniauth-cas 1.0.4 → 2.0.0
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 +6 -14
- data/.editorconfig +16 -0
- data/.travis.yml +16 -3
- data/CHANGELOG.md +27 -0
- data/Gemfile +5 -0
- data/README.md +47 -16
- data/lib/omniauth-cas.rb +1 -1
- data/lib/omniauth/cas/version.rb +1 -1
- data/lib/omniauth/strategies/cas.rb +105 -45
- data/lib/omniauth/strategies/cas/logout_request.rb +58 -0
- data/lib/omniauth/strategies/cas/service_ticket_validator.rb +24 -9
- data/omniauth-cas.gemspec +7 -9
- data/spec/fixtures/cas_success.xml +3 -0
- data/spec/fixtures/cas_success_jasig.xml +3 -0
- data/spec/omniauth/strategies/cas/logout_request_spec.rb +105 -0
- data/spec/omniauth/strategies/cas/service_ticket_validator_spec.rb +54 -13
- data/spec/omniauth/strategies/cas_spec.rb +180 -84
- data/spec/spec_helper.rb +0 -4
- metadata +44 -59
- data/.rvmrc +0 -1
- data/History.md +0 -58
- data/lib/omniauth/strategies/cas/configuration.rb +0 -34
- data/spec/omniauth/strategies/cas/configuration_spec.rb +0 -60
@@ -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
|
@@ -6,9 +6,10 @@ module OmniAuth
|
|
6
6
|
module Strategies
|
7
7
|
class CAS
|
8
8
|
class ServiceTicketValidator
|
9
|
-
|
10
9
|
VALIDATION_REQUEST_HEADERS = { 'Accept' => '*/*' }
|
11
10
|
|
11
|
+
attr_reader :success_body
|
12
|
+
|
12
13
|
# Build a validator from a +configuration+, a
|
13
14
|
# +return_to+ URL, and a +ticket+.
|
14
15
|
#
|
@@ -20,6 +21,13 @@ module OmniAuth
|
|
20
21
|
@uri = URI.parse(strategy.service_validate_url(return_to_url, ticket))
|
21
22
|
end
|
22
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
|
+
|
23
31
|
# Request validation of the ticket from the CAS server's
|
24
32
|
# serviceValidate (CAS 2.0) function.
|
25
33
|
#
|
@@ -29,31 +37,39 @@ module OmniAuth
|
|
29
37
|
#
|
30
38
|
# @raise any connection errors encountered.
|
31
39
|
def user_info
|
32
|
-
parse_user_info(
|
40
|
+
parse_user_info(@success_body)
|
33
41
|
end
|
34
42
|
|
35
43
|
private
|
36
44
|
|
45
|
+
# Merges attributes with multiple values into an array if support is
|
46
|
+
# enabled (disabled by default)
|
47
|
+
def attribute_value(user_info, attribute, value)
|
48
|
+
if @options.merge_multivalued_attributes && user_info.key?(attribute)
|
49
|
+
Array(user_info[attribute]).push(value)
|
50
|
+
else
|
51
|
+
value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
37
55
|
# turns an `<cas:authenticationSuccess>` node into a Hash;
|
38
56
|
# returns nil if given nil
|
39
57
|
def parse_user_info(node)
|
40
58
|
return nil if node.nil?
|
41
|
-
|
42
59
|
{}.tap do |hash|
|
43
60
|
node.children.each do |e|
|
44
61
|
node_name = e.name.sub(/^cas:/, '')
|
45
|
-
unless e.kind_of?(Nokogiri::XML::Text) ||
|
46
|
-
node_name == 'proxies'
|
62
|
+
unless e.kind_of?(Nokogiri::XML::Text) || node_name == 'proxies'
|
47
63
|
# There are no child elements
|
48
64
|
if e.element_children.count == 0
|
49
|
-
hash[node_name] = e.content
|
65
|
+
hash[node_name] = attribute_value(hash, node_name, e.content)
|
50
66
|
elsif e.element_children.count
|
51
67
|
# JASIG style extra attributes
|
52
68
|
if node_name == 'attributes'
|
53
|
-
hash.merge!
|
69
|
+
hash.merge!(parse_user_info(e))
|
54
70
|
else
|
55
71
|
hash[node_name] = [] if hash[node_name].nil?
|
56
|
-
hash[node_name].push
|
72
|
+
hash[node_name].push(parse_user_info(e))
|
57
73
|
end
|
58
74
|
end
|
59
75
|
end
|
@@ -93,7 +109,6 @@ module OmniAuth
|
|
93
109
|
end
|
94
110
|
result
|
95
111
|
end
|
96
|
-
|
97
112
|
end
|
98
113
|
end
|
99
114
|
end
|
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.
|
19
|
-
gem.add_dependency 'nokogiri', '~> 1.
|
18
|
+
gem.add_dependency 'omniauth', '~> 1.2'
|
19
|
+
gem.add_dependency 'nokogiri', '~> 1.5'
|
20
20
|
gem.add_dependency 'addressable', '~> 2.3'
|
21
21
|
|
22
|
-
gem.add_development_dependency 'rake'
|
23
|
-
gem.add_development_dependency 'webmock'
|
24
|
-
gem.add_development_dependency '
|
25
|
-
gem.add_development_dependency '
|
26
|
-
gem.add_development_dependency 'rack-test', '~> 0.6'
|
22
|
+
gem.add_development_dependency 'rake'
|
23
|
+
gem.add_development_dependency 'webmock'
|
24
|
+
gem.add_development_dependency 'rspec'
|
25
|
+
gem.add_development_dependency 'rack-test'
|
27
26
|
|
28
27
|
gem.add_development_dependency 'awesome_print'
|
29
|
-
|
30
28
|
end
|
@@ -10,5 +10,8 @@
|
|
10
10
|
<cas:image>/images/user.jpg</cas:image>
|
11
11
|
<cas:phone>555-555-5555</cas:phone>
|
12
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>
|
13
16
|
</cas:authenticationSuccess>
|
14
17
|
</cas:serviceResponse>
|
@@ -11,6 +11,9 @@
|
|
11
11
|
<cas:image>/images/user.jpg</cas:image>
|
12
12
|
<cas:phone>555-555-5555</cas:phone>
|
13
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>
|
14
17
|
</cas:attributes>
|
15
18
|
</cas:authenticationSuccess>
|
16
19
|
</cas:serviceResponse>
|
@@ -0,0 +1,105 @@
|
|
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
|
+
let(:response_body) { subject[2].respond_to?(:body) ? subject[2].body : subject[2] }
|
62
|
+
|
63
|
+
context 'that returns TRUE' do
|
64
|
+
let(:callback) { Proc.new{true} }
|
65
|
+
|
66
|
+
it 'responds with OK' do
|
67
|
+
expect(subject[0]).to eq 200
|
68
|
+
expect(response_body).to eq ['OK']
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'that returns Nil' do
|
73
|
+
let(:callback) { Proc.new{} }
|
74
|
+
|
75
|
+
it 'responds with OK' do
|
76
|
+
expect(subject[0]).to eq 200
|
77
|
+
expect(response_body).to eq ['OK']
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'that returns a tuple' do
|
82
|
+
let(:callback) { Proc.new{ [400,{},'Bad Request'] } }
|
83
|
+
|
84
|
+
it 'responds with OK' do
|
85
|
+
expect(subject[0]).to eq 400
|
86
|
+
expect(response_body).to eq ['Bad Request']
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'that raises an error' do
|
91
|
+
let(:exception) { RuntimeError.new('error' )}
|
92
|
+
let(:callback) { Proc.new{raise exception} }
|
93
|
+
|
94
|
+
before do
|
95
|
+
allow(strategy).to receive(:fail!)
|
96
|
+
subject
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'responds with an error' do
|
100
|
+
expect(strategy).to have_received(:fail!)
|
101
|
+
.with(:logout_request, exception)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -1,33 +1,74 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe OmniAuth::Strategies::CAS::ServiceTicketValidator do
|
4
|
-
let(:
|
5
|
-
|
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
|
-
|
10
|
+
double('provider_options',
|
12
11
|
disable_ssl_verification?: false,
|
12
|
+
merge_multivalued_attributes: false,
|
13
13
|
ca_path: '/etc/ssl/certsZOMG'
|
14
14
|
)
|
15
15
|
end
|
16
|
-
|
17
16
|
let(:validator) do
|
18
|
-
OmniAuth::Strategies::CAS::ServiceTicketValidator.new(
|
17
|
+
OmniAuth::Strategies::CAS::ServiceTicketValidator.new( strategy, provider_options, '/foo', nil )
|
19
18
|
end
|
20
19
|
|
21
|
-
describe '#
|
22
|
-
|
23
|
-
stub_request(:get, 'https://example.org/serviceValidate?')
|
24
|
-
|
20
|
+
describe '#call' do
|
21
|
+
before do
|
22
|
+
stub_request(:get, 'https://example.org/serviceValidate?')
|
23
|
+
.to_return(status: 200, body: '')
|
25
24
|
end
|
26
25
|
|
27
|
-
|
28
|
-
|
26
|
+
subject { validator.call }
|
27
|
+
|
28
|
+
it 'returns itself' do
|
29
|
+
expect(subject).to eq validator
|
30
|
+
end
|
29
31
|
|
32
|
+
it 'uses the configured CA path' do
|
30
33
|
subject
|
34
|
+
expect(provider_options).to have_received :ca_path
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#user_info' do
|
39
|
+
let(:ok_fixture) do
|
40
|
+
File.expand_path(File.join(File.dirname(__FILE__), '../../../fixtures/cas_success.xml'))
|
41
|
+
end
|
42
|
+
let(:service_response) { File.read(ok_fixture) }
|
43
|
+
|
44
|
+
before do
|
45
|
+
stub_request(:get, 'https://example.org/serviceValidate?')
|
46
|
+
.to_return(status: 200, body:service_response)
|
47
|
+
validator.call
|
48
|
+
end
|
49
|
+
|
50
|
+
subject { validator.user_info }
|
51
|
+
|
52
|
+
context 'with default settings' do
|
53
|
+
it 'parses user info from the response' do
|
54
|
+
expect(subject).to include 'user' => 'psegel'
|
55
|
+
expect(subject).to include 'roles' => 'financier'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'when merging multivalued attributes' do
|
60
|
+
let(:provider_options) do
|
61
|
+
double('provider_options',
|
62
|
+
disable_ssl_verification?: false,
|
63
|
+
merge_multivalued_attributes: true,
|
64
|
+
ca_path: '/etc/ssl/certsZOMG'
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'parses multivalued user info from the response' do
|
69
|
+
expect(subject).to include 'user' => 'psegel'
|
70
|
+
expect(subject).to include 'roles' => %w[senator lobbyist financier]
|
71
|
+
end
|
31
72
|
end
|
32
73
|
end
|
33
|
-
end
|
74
|
+
end
|
@@ -3,11 +3,24 @@ require 'spec_helper'
|
|
3
3
|
describe OmniAuth::Strategies::CAS, type: :strategy do
|
4
4
|
include Rack::Test::Methods
|
5
5
|
|
6
|
-
|
7
|
-
|
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,
|
13
|
+
use MyCasProvider,
|
14
|
+
name: :cas,
|
15
|
+
host: 'cas.example.org',
|
16
|
+
ssl: false,
|
17
|
+
port: 8080,
|
18
|
+
uid_field: :employeeid,
|
19
|
+
fetch_raw_info: Proc.new { |v, opts, ticket, info, node|
|
20
|
+
info.empty? ? {} : {
|
21
|
+
"roles" => node.xpath('//cas:roles').map(&:text),
|
22
|
+
}
|
23
|
+
}
|
11
24
|
run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] }
|
12
25
|
}.to_app
|
13
26
|
end
|
@@ -22,13 +35,56 @@ describe OmniAuth::Strategies::CAS, type: :strategy do
|
|
22
35
|
|
23
36
|
it { should be_redirect }
|
24
37
|
|
25
|
-
it '
|
26
|
-
subject.headers
|
38
|
+
it 'redirects to the CAS server' do
|
39
|
+
expect(subject.headers).to include 'Location' => "http://cas.example.org:8080/login?#{redirect_params}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#cas_url' do
|
44
|
+
let(:params) { Hash.new }
|
45
|
+
let(:provider) { MyCasProvider.new(nil, params) }
|
46
|
+
|
47
|
+
subject { provider.cas_url }
|
48
|
+
|
49
|
+
it 'raises an ArgumentError' do
|
50
|
+
expect{subject}.to raise_error ArgumentError, %r{:host and :login_url MUST be provided}
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'with an explicit :url option' do
|
54
|
+
let(:url) { 'https://example.org:8080/my_cas' }
|
55
|
+
let(:params) { super().merge url:url }
|
56
|
+
|
57
|
+
before { subject }
|
58
|
+
|
59
|
+
it { should eq url }
|
60
|
+
|
61
|
+
it 'parses the URL into it the appropriate strategy options' do
|
62
|
+
expect(provider.options).to include ssl:true
|
63
|
+
expect(provider.options).to include host:'example.org'
|
64
|
+
expect(provider.options).to include port:8080
|
65
|
+
expect(provider.options).to include path:'/my_cas'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'with explicit URL component' do
|
70
|
+
let(:params) { super().merge host:'example.org', port:1234, ssl:true, path:'/a/path' }
|
71
|
+
|
72
|
+
before { subject }
|
73
|
+
|
74
|
+
it { should eq 'https://example.org:1234/a/path' }
|
75
|
+
|
76
|
+
it 'parses the URL into it the appropriate strategy options' do
|
77
|
+
expect(provider.options).to include ssl:true
|
78
|
+
expect(provider.options).to include host:'example.org'
|
79
|
+
expect(provider.options).to include port:1234
|
80
|
+
expect(provider.options).to include path:'/a/path'
|
81
|
+
end
|
27
82
|
end
|
28
83
|
end
|
29
84
|
|
30
85
|
describe 'defaults' do
|
31
86
|
subject { MyCasProvider.default_options.to_hash }
|
87
|
+
|
32
88
|
it { should include('ssl' => true) }
|
33
89
|
end
|
34
90
|
|
@@ -52,114 +108,154 @@ describe OmniAuth::Strategies::CAS, type: :strategy do
|
|
52
108
|
end
|
53
109
|
end
|
54
110
|
|
55
|
-
describe 'GET /auth/cas/callback
|
56
|
-
|
57
|
-
|
58
|
-
subject { last_response }
|
59
|
-
|
60
|
-
it { should be_redirect }
|
111
|
+
describe 'GET /auth/cas/callback' do
|
112
|
+
context 'without a ticket' do
|
113
|
+
before { get '/auth/cas/callback' }
|
61
114
|
|
62
|
-
|
63
|
-
subject.headers['Location'].should == '/auth/failure?message=no_ticket&strategy=cas'
|
64
|
-
end
|
65
|
-
end
|
115
|
+
subject { last_response }
|
66
116
|
|
67
|
-
|
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 }
|
117
|
+
it { should be_redirect }
|
75
118
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
subject.headers['Location'].should == '/auth/failure?message=invalid_ticket&strategy=cas'
|
119
|
+
it 'redirects with a failure message' do
|
120
|
+
expect(subject.headers).to include 'Location' => '/auth/failure?message=no_ticket&strategy=cas'
|
121
|
+
end
|
80
122
|
end
|
81
|
-
end
|
82
123
|
|
83
|
-
|
84
|
-
shared_examples :successful_validation do
|
124
|
+
context 'with an invalid ticket' do
|
85
125
|
before do
|
86
|
-
stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?ticket=
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
get "/auth/cas/callback?ticket=593af&url=#{return_url}"
|
126
|
+
stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?ticket=9391d/).
|
127
|
+
to_return( body: File.read('spec/fixtures/cas_failure.xml') )
|
128
|
+
get '/auth/cas/callback?ticket=9391d'
|
91
129
|
end
|
92
130
|
|
93
|
-
|
94
|
-
@request_uri.scan('ticket=').length.should == 1
|
95
|
-
end
|
131
|
+
subject { last_response }
|
96
132
|
|
97
|
-
it
|
98
|
-
WebMock.should have_requested(:get, 'http://cas.example.org:8080/serviceValidate')
|
99
|
-
.with(query: {
|
100
|
-
ticket: '593af',
|
101
|
-
service: 'http://example.org/auth/cas/callback?url=' + Rack::Utils.escape('http://127.0.0.10/?some=parameter')
|
102
|
-
})
|
103
|
-
end
|
133
|
+
it { should be_redirect }
|
104
134
|
|
105
|
-
|
106
|
-
subject
|
135
|
+
it 'redirects with a failure message' do
|
136
|
+
expect(subject.headers).to include 'Location' => '/auth/failure?message=invalid_ticket&strategy=cas'
|
137
|
+
end
|
138
|
+
end
|
107
139
|
|
108
|
-
|
140
|
+
describe 'with a valid ticket' do
|
141
|
+
shared_examples :successful_validation do
|
142
|
+
before do
|
143
|
+
stub_request(:get, /^http:\/\/cas.example.org:8080?\/serviceValidate\?([^&]+&)?ticket=593af/)
|
144
|
+
.with { |request| @request_uri = request.uri.to_s }
|
145
|
+
.to_return( body: File.read("spec/fixtures/#{xml_file_name}") )
|
109
146
|
|
110
|
-
|
147
|
+
get "/auth/cas/callback?ticket=593af&url=#{return_url}"
|
148
|
+
end
|
111
149
|
|
112
|
-
|
150
|
+
it 'strips the ticket parameter from the callback URL' do
|
151
|
+
expect(@request_uri.scan('ticket=').size).to eq 1
|
152
|
+
end
|
113
153
|
|
114
|
-
|
115
|
-
|
154
|
+
it 'properly encodes the service URL' do
|
155
|
+
expect(WebMock).to have_requested(:get, 'http://cas.example.org:8080/serviceValidate')
|
156
|
+
.with(query: {
|
157
|
+
ticket: '593af',
|
158
|
+
service: 'http://example.org/auth/cas/callback?url=' + Rack::Utils.escape('http://127.0.0.10/?some=parameter')
|
159
|
+
})
|
160
|
+
end
|
116
161
|
|
117
|
-
|
162
|
+
context "request.env['omniauth.auth']" do
|
163
|
+
subject { last_request.env['omniauth.auth'] }
|
164
|
+
|
165
|
+
it { should be_kind_of Hash }
|
166
|
+
|
167
|
+
it 'identifes the provider' do
|
168
|
+
expect(subject.provider).to eq :cas
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'returns the UID of the user' do
|
172
|
+
expect(subject.uid).to eq '54'
|
173
|
+
end
|
174
|
+
|
175
|
+
context 'the info hash' do
|
176
|
+
subject { last_request.env['omniauth.auth']['info'] }
|
177
|
+
|
178
|
+
it 'includes user info attributes' do
|
179
|
+
expect(subject.name).to eq 'Peter Segel'
|
180
|
+
expect(subject.first_name).to eq 'Peter'
|
181
|
+
expect(subject.last_name).to eq 'Segel'
|
182
|
+
expect(subject.nickname).to eq 'psegel'
|
183
|
+
expect(subject.email).to eq 'psegel@intridea.com'
|
184
|
+
expect(subject.location).to eq 'Washington, D.C.'
|
185
|
+
expect(subject.image).to eq '/images/user.jpg'
|
186
|
+
expect(subject.phone).to eq '555-555-5555'
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'the extra hash' do
|
191
|
+
subject { last_request.env['omniauth.auth']['extra'] }
|
192
|
+
|
193
|
+
it 'includes additional user attributes' do
|
194
|
+
expect(subject.user).to eq 'psegel'
|
195
|
+
expect(subject.employeeid).to eq '54'
|
196
|
+
expect(subject.hire_date).to eq '2004-07-13'
|
197
|
+
expect(subject.roles).to eq %w(senator lobbyist financier)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
context 'the credentials hash' do
|
202
|
+
subject { last_request.env['omniauth.auth']['credentials'] }
|
203
|
+
|
204
|
+
it 'has a ticket value' do
|
205
|
+
expect(subject.ticket).to eq '593af'
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
118
209
|
|
119
|
-
|
120
|
-
|
121
|
-
its(:last_name) { should == 'Segel' }
|
122
|
-
its(:email) { should == 'psegel@intridea.com' }
|
123
|
-
its(:location) { should == 'Washington, D.C.' }
|
124
|
-
its(:image) { should == '/images/user.jpg' }
|
125
|
-
its(:phone) { should == '555-555-5555' }
|
210
|
+
it 'calls through to the master app' do
|
211
|
+
expect(last_response.body).to eq 'true'
|
126
212
|
end
|
213
|
+
end
|
127
214
|
|
128
|
-
|
129
|
-
subject { last_request.env['omniauth.auth']['extra'] }
|
215
|
+
let(:return_url) { 'http://127.0.0.10/?some=parameter' }
|
130
216
|
|
131
|
-
|
217
|
+
context 'with JASIG flavored XML' do
|
218
|
+
let(:xml_file_name) { 'cas_success_jasig.xml' }
|
132
219
|
|
133
|
-
|
134
|
-
|
135
|
-
its(:hire_date) { should == '2004-07-13' }
|
136
|
-
end
|
220
|
+
it_behaves_like :successful_validation
|
221
|
+
end
|
137
222
|
|
138
|
-
|
139
|
-
|
223
|
+
context 'with classic XML' do
|
224
|
+
let(:xml_file_name) { 'cas_success.xml' }
|
140
225
|
|
141
|
-
|
226
|
+
it_behaves_like :successful_validation
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
142
230
|
|
143
|
-
|
144
|
-
|
231
|
+
describe 'POST /auth/cas/callback' do
|
232
|
+
describe 'with a Single Sign-Out logoutRequest' do
|
233
|
+
let(:logoutRequest) do
|
234
|
+
%Q[
|
235
|
+
<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}">
|
236
|
+
<saml:NameID>@NOT_USED@</saml:NameID>
|
237
|
+
<samlp:SessionIndex>ST-123456-123abc456def</samlp:SessionIndex>
|
238
|
+
</samlp:LogoutRequest>
|
239
|
+
]
|
145
240
|
end
|
146
241
|
|
147
|
-
|
148
|
-
|
242
|
+
let(:logout_request) { double('logout_request', call:[200,{},'OK']) }
|
243
|
+
|
244
|
+
subject do
|
245
|
+
post 'auth/cas/callback', logoutRequest:logoutRequest
|
149
246
|
end
|
150
|
-
end
|
151
247
|
|
152
|
-
|
248
|
+
before do
|
249
|
+
allow_any_instance_of(MyCasProvider)
|
250
|
+
.to receive(:logout_request_service)
|
251
|
+
.and_return double('LogoutRequest', new:logout_request)
|
153
252
|
|
154
|
-
|
155
|
-
|
156
|
-
it_behaves_like :successful_validation
|
157
|
-
end
|
253
|
+
subject
|
254
|
+
end
|
158
255
|
|
159
|
-
|
160
|
-
|
161
|
-
|
256
|
+
it 'initializes a LogoutRequest' do
|
257
|
+
expect(logout_request).to have_received :call
|
258
|
+
end
|
162
259
|
end
|
163
260
|
end
|
164
|
-
|
165
261
|
end
|