omniauth-dice 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +41 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +15 -0
- data/.yardopts +1 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +88 -0
- data/Rakefile +21 -0
- data/certs/stevenhaddox.pem +21 -0
- data/config/gem_sources.yml.example +1 -0
- data/lib/omniauth/dice/version.rb +5 -0
- data/lib/omniauth/strategies/dice.rb +422 -0
- data/lib/omniauth-dice.rb +3 -0
- data/lib/string.rb +17 -0
- data/omniauth-dice.gemspec +50 -0
- data/spec/certs/create_spec_cert.rb +41 -0
- data/spec/certs/ruby_user.crt +20 -0
- data/spec/certs/ruby_user.pub +9 -0
- data/spec/fixtures/valid_auth.json +27 -0
- data/spec/fixtures/valid_auth.xml +20 -0
- data/spec/omniauth/strategies/dice_integrations_spec.rb +201 -0
- data/spec/omniauth/strategies/dice_spec.rb +161 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/test_apps/test_rack_app.rb +17 -0
- data.tar.gz.sig +3 -0
- metadata +424 -0
- metadata.gz.sig +0 -0
data/lib/string.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#
|
2
|
+
# Extend the core String class to include `.to_snake`
|
3
|
+
#
|
4
|
+
class String
|
5
|
+
# Attempts to convert a string into a formatted_snake_case_string
|
6
|
+
def to_snake
|
7
|
+
self.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
8
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
9
|
+
.tr('-', '_')
|
10
|
+
.downcase
|
11
|
+
end
|
12
|
+
|
13
|
+
# Alias to .to_snake
|
14
|
+
def underscore
|
15
|
+
self.to_snake
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'omniauth/dice/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'omniauth-dice'
|
8
|
+
spec.version = Omniauth::Dice::VERSION
|
9
|
+
spec.authors = ['Steven Haddox']
|
10
|
+
spec.email = ['steven.haddox@gmail.com']
|
11
|
+
spec.summary = %q{DN Interoperable Conversion Expert Strategy}
|
12
|
+
spec.description = %q{Simple gem to enable rack powered Ruby apps to authenticate via REST with an enterprise CAS authentication server via X509 client certificates.}
|
13
|
+
spec.homepage = "https://github.com/stevenhaddox/omniauth-dice"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = '>= 1.9.3'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'awesome_print'
|
24
|
+
spec.add_development_dependency 'bundler'
|
25
|
+
spec.add_development_dependency 'capybara'
|
26
|
+
spec.add_development_dependency 'coveralls'
|
27
|
+
spec.add_development_dependency 'rack_session_access'
|
28
|
+
spec.add_development_dependency 'redcarpet'
|
29
|
+
spec.add_development_dependency 'rspec'
|
30
|
+
spec.add_development_dependency 'rack'
|
31
|
+
spec.add_development_dependency 'rack-test'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
33
|
+
spec.add_development_dependency 'rubocop'
|
34
|
+
spec.add_development_dependency 'simplecov'
|
35
|
+
spec.add_development_dependency 'simplecov-badge'
|
36
|
+
spec.add_development_dependency 'webmock'
|
37
|
+
spec.add_development_dependency 'yard'
|
38
|
+
|
39
|
+
spec.add_dependency 'cert_munger', '~> 0.1'
|
40
|
+
spec.add_dependency 'dnc', '~> 0.1'
|
41
|
+
spec.add_dependency 'excon'
|
42
|
+
spec.add_dependency 'faraday'
|
43
|
+
spec.add_dependency 'faraday_middleware'
|
44
|
+
spec.add_dependency 'logging', '~> 1.8'
|
45
|
+
spec.add_dependency 'multi_xml'
|
46
|
+
spec.add_dependency 'omniauth', '~> 1.0'
|
47
|
+
|
48
|
+
spec.cert_chain = ['certs/stevenhaddox.pem']
|
49
|
+
spec.signing_key = File.expand_path("~/.gem/certs/gem-private_key.pem") if $0 =~ /gem\z/
|
50
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'awesome_print'
|
5
|
+
|
6
|
+
root_key = OpenSSL::PKey::RSA.new 2048 # the CA's public/private key
|
7
|
+
root_ca = OpenSSL::X509::Certificate.new
|
8
|
+
root_ca.version = 2 # cf. RFC 5280 - to make it a "v3" certificate
|
9
|
+
root_ca.serial = 1
|
10
|
+
root_ca.subject = OpenSSL::X509::Name.parse "/DC=org/DC=ruby-lang/CN=Ruby CA"
|
11
|
+
root_ca.issuer = root_ca.subject # root CA's are "self-signed"
|
12
|
+
root_ca.public_key = root_key.public_key
|
13
|
+
root_ca.not_before = Time.now
|
14
|
+
root_ca.not_after = root_ca.not_before + 2 * 365 * 24 * 60 * 60 # 2 years validity
|
15
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
16
|
+
ef.subject_certificate = root_ca
|
17
|
+
ef.issuer_certificate = root_ca
|
18
|
+
root_ca.add_extension(ef.create_extension("basicConstraints","CA:TRUE",true))
|
19
|
+
root_ca.add_extension(ef.create_extension("keyUsage","keyCertSign, cRLSign", true))
|
20
|
+
root_ca.add_extension(ef.create_extension("subjectKeyIdentifier","hash",false))
|
21
|
+
root_ca.add_extension(ef.create_extension("authorityKeyIdentifier","keyid:always",false))
|
22
|
+
root_ca.sign(root_key, OpenSSL::Digest::SHA256.new)
|
23
|
+
|
24
|
+
key = OpenSSL::PKey::RSA.new 2048
|
25
|
+
cert = OpenSSL::X509::Certificate.new
|
26
|
+
cert.version = 2
|
27
|
+
cert.serial = 2
|
28
|
+
cert.subject = OpenSSL::X509::Name.parse "/DC=org/DC=ruby-lang/CN=Ruby certificate rbcert"
|
29
|
+
cert.issuer = root_ca.subject # root CA is the issuer
|
30
|
+
cert.public_key = key.public_key
|
31
|
+
cert.not_before = Time.now
|
32
|
+
cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60 # 1 years validity
|
33
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
34
|
+
ef.subject_certificate = cert
|
35
|
+
ef.issuer_certificate = root_ca
|
36
|
+
cert.add_extension(ef.create_extension("keyUsage","digitalSignature", true))
|
37
|
+
cert.add_extension(ef.create_extension("subjectKeyIdentifier","hash",false))
|
38
|
+
cert.sign(root_key, OpenSSL::Digest::SHA256.new)
|
39
|
+
|
40
|
+
File.write('ruby_user.crt', cert)
|
41
|
+
File.write('ruby_user.pub', cert.public_key)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDQDCCAiigAwIBAgIBAjANBgkqhkiG9w0BAQsFADBCMRMwEQYKCZImiZPyLGQB
|
3
|
+
GRYDb3JnMRkwFwYKCZImiZPyLGQBGRYJcnVieS1sYW5nMRAwDgYDVQQDDAdSdWJ5
|
4
|
+
IENBMB4XDTE0MTAyMTE0MTU0NFoXDTE1MTAyMTE0MTU0NFowUjETMBEGCgmSJomT
|
5
|
+
8ixkARkWA29yZzEZMBcGCgmSJomT8ixkARkWCXJ1YnktbGFuZzEgMB4GA1UEAwwX
|
6
|
+
UnVieSBjZXJ0aWZpY2F0ZSByYmNlcnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
7
|
+
ggEKAoIBAQDA2Cb1QoNx7ghJv/q7yzAr89zyQbOr7wJN+X9/qfRTZi7o31JRrO21
|
8
|
+
4VFAQeLdf2InfH/yGcME/6dRm0Y9priOShPkRTrerxD8Xf7df/u/R2cUW6WcQUuB
|
9
|
+
fFHp8f1FcYuwxsX2gFNpboukRfWSgIvkXG+BAl/HSUjPyjiEYYTk2nkjOUmFObA8
|
10
|
+
ip7xJ2n+ZyHl4SggvC+C8QmDQt3dBsZ3C2BovtJ6YQRMJjaEXGWl2fxJfgnJ94aq
|
11
|
+
/M3TP5OUvnwDh8MJq4LuPNPc2PBAAvrQ0SFUaZxlxt9cWtnEGhT6jejBWrik8BNk
|
12
|
+
YGrU9HIpYeg4a9i5GRDZ6s/xlZ4X+pjlAgMBAAGjMTAvMA4GA1UdDwEB/wQEAwIH
|
13
|
+
gDAdBgNVHQ4EFgQUPGVKgxbUCl/OiPHnzeN3qR5BMe4wDQYJKoZIhvcNAQELBQAD
|
14
|
+
ggEBAFnSbIvnYubRuEcGux5m1y2UlzSg/GyNDhnWsfPqQQ7FfolFTk6W8+xKuRSg
|
15
|
+
foiuV2Od0n1nBMd9ePzoF5QabMHCK1Al2o/P7PkUPgmuAnWWdTLikNtt6H6v9WLe
|
16
|
+
R9Ng37xv9jOVFOQjYCYvc94cJe1OBXW7ppkRY3kUQQbIrO7Imwi8gB8KT4qwu/Ld
|
17
|
+
Xt53I/aFMQ0JSkqhJ4Bhff/Ol2RcDjEt4U6Hne6lRVB9qCAz0gE8MN4HagWOlS1U
|
18
|
+
Nxd+cde+JIVBHuDz63gExGPCdLBRL/Mi3LORpZT41/ryw3JCqTir0dgPXsVTtJPr
|
19
|
+
iMZ2EcsmShyBUKKj2AIok5gZzgM=
|
20
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,9 @@
|
|
1
|
+
-----BEGIN PUBLIC KEY-----
|
2
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwNgm9UKDce4ISb/6u8sw
|
3
|
+
K/Pc8kGzq+8CTfl/f6n0U2Yu6N9SUaztteFRQEHi3X9iJ3x/8hnDBP+nUZtGPaa4
|
4
|
+
jkoT5EU63q8Q/F3+3X/7v0dnFFulnEFLgXxR6fH9RXGLsMbF9oBTaW6LpEX1koCL
|
5
|
+
5FxvgQJfx0lIz8o4hGGE5Np5IzlJhTmwPIqe8Sdp/mch5eEoILwvgvEJg0Ld3QbG
|
6
|
+
dwtgaL7SemEETCY2hFxlpdn8SX4JyfeGqvzN0z+TlL58A4fDCauC7jzT3NjwQAL6
|
7
|
+
0NEhVGmcZcbfXFrZxBoU+o3owVq4pPATZGBq1PRyKWHoOGvYuRkQ2erP8ZWeF/qY
|
8
|
+
5QIDAQAB
|
9
|
+
-----END PUBLIC KEY-----
|
@@ -0,0 +1,27 @@
|
|
1
|
+
{
|
2
|
+
"citizenshipStatus": "US",
|
3
|
+
"country": "USA",
|
4
|
+
"dn": "cn=pr. twilight sparkle,ou=c001,ou=mlp,ou=pny,o=princesses of celestia,c=us",
|
5
|
+
"email": "twilight@example.org",
|
6
|
+
"firstName": "twilight",
|
7
|
+
"lastName": "sparkle",
|
8
|
+
"fullName": "twilight sparkle",
|
9
|
+
"grantBy": [
|
10
|
+
"princess celestia"
|
11
|
+
],
|
12
|
+
"organizations": [
|
13
|
+
"princesses",
|
14
|
+
"librarians",
|
15
|
+
"unicorns"
|
16
|
+
],
|
17
|
+
"uid": "twilight.sparkle",
|
18
|
+
"dutyorg": "ponyville library",
|
19
|
+
"visas": [
|
20
|
+
"EQUESTRIA",
|
21
|
+
"CLOUDSDALE"
|
22
|
+
],
|
23
|
+
"affiliations": [
|
24
|
+
"WONDERBOLTS"
|
25
|
+
],
|
26
|
+
"telephoneNumber": "555-555-5555"
|
27
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<userinfo>
|
3
|
+
<citizenshipStatus>US</citizenshipStatus>
|
4
|
+
<country>USA</country>
|
5
|
+
<dn>cn=pr. twilight sparkle,ou=c001,ou=mlp,ou=pny,o=princesses of celestia,c=us</dn>
|
6
|
+
<email>twilight@example.org</email>
|
7
|
+
<firstName>twilight</firstName>
|
8
|
+
<lastName>sparkle</lastName>
|
9
|
+
<fullName>twilight sparkle</fullName>
|
10
|
+
<grantBy>princess celestia</grantBy>
|
11
|
+
<organizations>princesses</organizations>
|
12
|
+
<organizations>librarians</organizations>
|
13
|
+
<organizations>unicorns</organizations>
|
14
|
+
<uid>twilight.sparkle</uid>
|
15
|
+
<dutyorg>ponyville library</dutyorg>
|
16
|
+
<visas>EQUASTRIA</visas>
|
17
|
+
<visas>CLOUDSDALE</visas>
|
18
|
+
<affiliations>WONDERBOLTS</affiliations>
|
19
|
+
<telephoneNumber>555-555-5555</telephoneNumber>
|
20
|
+
</userinfo>
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require'dnc'
|
3
|
+
|
4
|
+
class MockDice; end
|
5
|
+
describe OmniAuth::Strategies::Dice, type: :strategy do
|
6
|
+
attr_accessor :app
|
7
|
+
let(:auth_hash) { full_auth_hash }
|
8
|
+
let!(:user_cert) { File.read('spec/certs/ruby_user.crt') }
|
9
|
+
let!(:raw_dn) { '/DC=org/DC=ruby-lang/CN=Ruby certificate rbcert' }
|
10
|
+
let!(:user_dn) { DN.new(dn_string: '/DC=org/DC=ruby-lang/CN=Ruby certificate rbcert') }
|
11
|
+
let(:raw_issuer_dn) { '/DC=org/DC=ruby-lang/CN=Ruby CA' }
|
12
|
+
let(:issuer_dn) { 'CN=RUBY CA,DC=RUBY-LANG,DC=ORG' }
|
13
|
+
let!(:valid_user_json) { File.read('spec/fixtures/valid_auth.json') }
|
14
|
+
let(:valid_user_xml) { File.read('spec/fixtures/valid_auth.xml') }
|
15
|
+
|
16
|
+
def full_auth_hash
|
17
|
+
{
|
18
|
+
"provider"=>"Dice",
|
19
|
+
"uid"=>"cn=ruby certificate rbcert,dc=ruby-lang,dc=org",
|
20
|
+
"extra" => {
|
21
|
+
"raw_info" => valid_user_json
|
22
|
+
},
|
23
|
+
"info" => {
|
24
|
+
"dn" => "cn=pr. twilight sparkle,ou=c001,ou=mlp,ou=pny,o=princesses of celestia,c=us",
|
25
|
+
"email" => "twilight@example.org",
|
26
|
+
"first_name" => "twilight",
|
27
|
+
"last_name" => "sparkle",
|
28
|
+
"full_name" => "twilight sparkle",
|
29
|
+
"common_name" => "pr. twilight sparkle",
|
30
|
+
"name" => "pr. twilight sparkle",
|
31
|
+
"citizenship_status" => "US",
|
32
|
+
"country" => "USA",
|
33
|
+
"grant_by" => [
|
34
|
+
"princess celestia"
|
35
|
+
],
|
36
|
+
"organizations" => [
|
37
|
+
"princesses",
|
38
|
+
"librarians",
|
39
|
+
"unicorns"
|
40
|
+
],
|
41
|
+
"uid" => "twilight.sparkle",
|
42
|
+
"dutyorg" => "ponyville library",
|
43
|
+
"visas" => [
|
44
|
+
"EQUESTRIA",
|
45
|
+
"CLOUDSDALE"
|
46
|
+
],
|
47
|
+
"affiliations" => [
|
48
|
+
"WONDERBOLTS"
|
49
|
+
],
|
50
|
+
"telephone_number" => "555-555-5555",
|
51
|
+
"primary_visa?" => true,
|
52
|
+
"likely_npe?" => false
|
53
|
+
}
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# customize rack app for testing, if block is given, reverts to default
|
58
|
+
# rack app after testing is done
|
59
|
+
def set_app!(dice_options = {})
|
60
|
+
dice_options = {:model => MockDice}.merge(dice_options)
|
61
|
+
old_app = self.app
|
62
|
+
self.app = Rack::Builder.app do
|
63
|
+
use Rack::Session::Cookie, :secret => '1337geeks'
|
64
|
+
use RackSessionAccess::Middleware
|
65
|
+
use OmniAuth::Strategies::Dice, dice_options
|
66
|
+
run lambda{|env| [404, {'env' => env}, ["HELLO!"]]}
|
67
|
+
end
|
68
|
+
if block_given?
|
69
|
+
yield
|
70
|
+
self.app = old_app
|
71
|
+
end
|
72
|
+
self.app
|
73
|
+
end
|
74
|
+
|
75
|
+
before(:all) do
|
76
|
+
defaults={
|
77
|
+
cas_server: 'http://example.org',
|
78
|
+
authentication_path: '/dn'
|
79
|
+
}
|
80
|
+
set_app!(defaults)
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
describe '#request_phase' do
|
85
|
+
it 'should fail without a client DN' do
|
86
|
+
expect { get '/auth/dice' }.to raise_error(OmniAuth::Error, 'You need a valid DN to authenticate.')
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should set the client & issuer's DN (from certificate)" do
|
90
|
+
header 'Ssl-Client-Cert', user_cert
|
91
|
+
get '/auth/dice'
|
92
|
+
expect(last_request.env['HTTP_SSL_CLIENT_CERT']).to eq(user_cert)
|
93
|
+
expect(last_request.url).to eq('http://example.org/auth/dice')
|
94
|
+
expect(last_request.env['rack.session']['omniauth.params']['user_dn']).to eq(user_dn.to_s)
|
95
|
+
expect(last_request.env['rack.session']['omniauth.params']['issuer_dn']).to eq(issuer_dn)
|
96
|
+
expect(last_response.location).to eq('http://example.org/auth/dice/callback')
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should set the client's DN (from header)" do
|
100
|
+
header 'Ssl-Client-S-Dn', raw_dn
|
101
|
+
get '/auth/dice'
|
102
|
+
expect(last_request.env['HTTP_SSL_CLIENT_S_DN']).to eq(raw_dn)
|
103
|
+
expect(last_request.url).to eq('http://example.org/auth/dice')
|
104
|
+
expect(last_request.env['rack.session']['omniauth.params']['user_dn']).to eq(user_dn.to_s)
|
105
|
+
expect(last_request.env['rack.session']['omniauth.params']['issuer_dn']).to be_nil
|
106
|
+
expect(last_response.location).to eq('http://example.org/auth/dice/callback')
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should set the issuer's DN (from header)" do
|
110
|
+
header 'Ssl-Client-S-Dn', raw_dn
|
111
|
+
header 'Ssl-Client-I-Dn', raw_issuer_dn
|
112
|
+
get '/auth/dice'
|
113
|
+
expect(last_request.env['HTTP_SSL_CLIENT_I_DN']).to eq(raw_issuer_dn)
|
114
|
+
expect(last_request.url).to eq('http://example.org/auth/dice')
|
115
|
+
expect(last_request.env['rack.session']['omniauth.params']['issuer_dn']).to eq(issuer_dn)
|
116
|
+
expect(last_response.location).to eq('http://example.org/auth/dice/callback')
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe '#callback_phase' do
|
121
|
+
before(:each) do
|
122
|
+
set_app!({
|
123
|
+
cas_server: 'https://example.org:3000',
|
124
|
+
authentication_path: '/dn',
|
125
|
+
dnc_options: { transformation: 'downcase' },
|
126
|
+
ssl_config: {
|
127
|
+
ca_file: 'spec/certs/CA.pem',
|
128
|
+
client_cert: 'spec/certs/client.pem',
|
129
|
+
client_key: 'spec/certs/key.np.pem'
|
130
|
+
},
|
131
|
+
primary_visa: 'CLOUDSDALE'
|
132
|
+
})
|
133
|
+
|
134
|
+
stub_request(:get, "https://example.org:3000/dn/cn=ruby%20certificate%20rbcert,dc=ruby-lang,dc=org/info.json?issuerDN=cn=ruby%20ca,dc=ruby-lang,dc=org").
|
135
|
+
with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json', 'Host'=>'example.org:3000', 'User-Agent'=>/^Faraday via Ruby.*$/, 'X-Xsrf-Useprotection'=>'false'}).
|
136
|
+
to_return(status: 200, body: valid_user_json, headers: {})
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'success' do
|
140
|
+
it 'should return a 200 with a JSON object of user information on success' do
|
141
|
+
header 'Ssl-Client-Cert', user_cert
|
142
|
+
get '/auth/dice'
|
143
|
+
follow_redirect!
|
144
|
+
expect(last_response.location).to eq('/')
|
145
|
+
raw_info = last_request.env['rack.session']['omniauth.auth']['extra']['raw_info']
|
146
|
+
expect(raw_info).to eq(valid_user_json)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'should return an omniauth auth_hash' do
|
150
|
+
header 'Ssl-Client-Cert', user_cert
|
151
|
+
get '/auth/dice'
|
152
|
+
follow_redirect!
|
153
|
+
expect(last_response.location).to eq('/')
|
154
|
+
raw_info = last_request.env['rack.session']['omniauth.auth']['extra']['raw_info']
|
155
|
+
expect(last_request.env['rack.session']['omniauth.auth']).to be_kind_of(Hash)
|
156
|
+
ap '>'*40
|
157
|
+
ap last_request.env['rack.session']['omniauth.auth'].sort
|
158
|
+
ap '<'*40
|
159
|
+
ap auth_hash.sort
|
160
|
+
expect(last_request.env['rack.session']['omniauth.auth'].sort).to eq(auth_hash.sort)
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'should return a 200 with an XML object of user information on success' do
|
164
|
+
set_app!({
|
165
|
+
cas_server: 'https://example.org:3000',
|
166
|
+
authentication_path: '/dn',
|
167
|
+
format_header: 'application/xml',
|
168
|
+
format: 'xml',
|
169
|
+
dnc_options: { transformation: 'downcase' },
|
170
|
+
ssl_config: {
|
171
|
+
ca_file: 'spec/certs/CA.pem',
|
172
|
+
client_cert: 'spec/certs/client.pem',
|
173
|
+
client_key: 'spec/certs/key.np.pem'
|
174
|
+
}
|
175
|
+
})
|
176
|
+
stub_request(:get, "https://example.org:3000/dn/cn=ruby%20certificate%20rbcert,dc=ruby-lang,dc=org/info.xml?issuerDN=cn=ruby%20ca,dc=ruby-lang,dc=org").
|
177
|
+
with(:headers => {'Accept'=>'application/xml', 'Content-Type'=>'application/xml', 'Host'=>'example.org:3000', 'User-Agent'=>/^Faraday via Ruby.*$/, 'X-Xsrf-Useprotection'=>'false'}).
|
178
|
+
to_return(status: 200, body: valid_user_xml, headers: {})
|
179
|
+
|
180
|
+
header 'Ssl-Client-Cert', user_cert
|
181
|
+
get '/auth/dice'
|
182
|
+
follow_redirect!
|
183
|
+
expect(last_response.location).to eq('/')
|
184
|
+
raw_info = last_request.env['rack.session']['omniauth.auth']['extra']['raw_info']
|
185
|
+
expect(raw_info).to eq(valid_user_xml)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
context 'fail' do
|
190
|
+
it 'should raise a 404 with text for a non-existent user DN' do
|
191
|
+
stub_request(:get, "https://example.org:3000/dn/cn=ruby%20certificate%20rbcert,dc=ruby-lang,dc=org/info.json?issuerDN=cn=ruby%20ca,dc=ruby-lang,dc=org").
|
192
|
+
with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json', 'Host'=>'example.org:3000', 'User-Agent'=>/^Faraday via Ruby.*$/, 'X-Xsrf-Useprotection'=>'false'}).
|
193
|
+
to_return(status: 404, body: "User of dn:cn=ruby certificate rbcert,dc=ruby-lang,dc=org not found", headers: {})
|
194
|
+
|
195
|
+
header 'Ssl-Client-Cert', user_cert
|
196
|
+
get '/auth/dice'
|
197
|
+
expect { get '/auth/dice'; follow_redirect! }.to raise_error(OmniAuth::Error, 'invalid_credentials')
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe OmniAuth::Strategies::Dice do
|
4
|
+
let!(:app) { TestRackApp.new }
|
5
|
+
let(:invalid_subject) { OmniAuth::Strategies::Dice.new(app) }
|
6
|
+
let(:dice_default_opts) { {
|
7
|
+
cas_server: 'https://dice.dev',
|
8
|
+
authentication_path: '/users'
|
9
|
+
} }
|
10
|
+
let(:valid_subject) {
|
11
|
+
OmniAuth::Strategies::Dice.new(app, dice_default_opts )
|
12
|
+
}
|
13
|
+
let!(:client_dn_from_cert) { '/DC=org/DC=ruby-lang/CN=Ruby certificate rbcert' }
|
14
|
+
let(:client_dn_reversed) { client_dn_from_cert.split('/').reverse.join('/') }
|
15
|
+
let(:formatted_client_dn) { 'CN=RUBY CERTIFICATE RBCERT,DC=RUBY-LANG,DC=ORG' }
|
16
|
+
|
17
|
+
# Travis-CI hack?
|
18
|
+
before(:all) do
|
19
|
+
@rack_env = ENV['RACK_ENV']
|
20
|
+
ENV['RACK_ENV'] = 'test'
|
21
|
+
end
|
22
|
+
|
23
|
+
context "invalid params" do
|
24
|
+
subject { invalid_subject }
|
25
|
+
let(:subject_without_authentication_path) { OmniAuth::Strategies::Dice.new(app, cas_server: 'https://dice.dev') }
|
26
|
+
|
27
|
+
it 'should require a cas server url' do
|
28
|
+
expect{ subject }.to raise_error(RequiredCustomParamError, "omniauth-dice error: cas_server is required")
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should require an authentication path' do
|
32
|
+
expect{ subject_without_authentication_path }.to raise_error(RequiredCustomParamError, "omniauth-dice error: authentication_path is required")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "defaults" do
|
37
|
+
subject { valid_subject }
|
38
|
+
it 'should have the correct name' do
|
39
|
+
expect(subject.options.name).to eq('dice')
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should return the default options" do
|
43
|
+
expect(subject.options.format).to eq('json')
|
44
|
+
expect(subject.options.format_header).to eq('application/json')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "configured with options" do
|
49
|
+
subject { valid_subject }
|
50
|
+
|
51
|
+
it 'should have the configured CAS server URL' do
|
52
|
+
expect(subject.options.cas_server).to eq("https://dice.dev")
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should have the configured authorization path' do
|
56
|
+
expect(subject.options.authentication_path).to eq('/users')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context ".format_dn" do
|
61
|
+
subject { valid_subject }
|
62
|
+
|
63
|
+
it 'should ensure the client DN format is in the proper order' do
|
64
|
+
formatted_cert_dn = subject.format_dn(client_dn_from_cert)
|
65
|
+
expect(formatted_cert_dn).to eq(formatted_client_dn)
|
66
|
+
|
67
|
+
formatted_reverse_client_dn = subject.format_dn(client_dn_reversed)
|
68
|
+
expect(formatted_reverse_client_dn).to eq(formatted_client_dn)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context ".set_name" do
|
73
|
+
before do
|
74
|
+
@info_hash = {
|
75
|
+
'common_name' => 'twilight.sparkle',
|
76
|
+
'full_name' => 'Princess Twilight Sparkle',
|
77
|
+
'first_name' => 'twilight',
|
78
|
+
'last_name' => 'sparkle'
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should not set a name field if it is already defined' do
|
83
|
+
dice = OmniAuth::Strategies::Dice.new( app, dice_default_opts )
|
84
|
+
name = dice.send( :set_name, @info_hash.merge({'name' => 'nightmare moon'}) )
|
85
|
+
expect(name).to eq('nightmare moon')
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should default to :cn then :first_name and finally :first_last_name' do
|
89
|
+
dice = OmniAuth::Strategies::Dice.new( app, dice_default_opts )
|
90
|
+
# With only full_name available
|
91
|
+
name = dice.send(:set_name, { 'full_name' => @info_hash['full_name'] })
|
92
|
+
expect(name).to eq(@info_hash['full_name'])
|
93
|
+
# With only first_name and last_name available
|
94
|
+
name = dice.send(:set_name, { 'first_name' => @info_hash['first_name'], 'last_name' => @info_hash['last_name'] })
|
95
|
+
expect(name).to eq("#{@info_hash['first_name']} #{@info_hash['last_name']}")
|
96
|
+
# With only the dn available
|
97
|
+
name = dice.send(:set_name, { 'common_name' => @info_hash['common_name'] })
|
98
|
+
expect(name).to eq(@info_hash['common_name'])
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should support custom name formatting set as :cn' do
|
102
|
+
dice = OmniAuth::Strategies::Dice.new( app, dice_default_opts.merge({name_format: :cn}) )
|
103
|
+
name = dice.send(:set_name, @info_hash)
|
104
|
+
expect(name).to eq(@info_hash['common_name'])
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should support custom name formatting set as :full_name' do
|
108
|
+
dice = OmniAuth::Strategies::Dice.new( app, dice_default_opts.merge({name_format: :full_name}) )
|
109
|
+
name = dice.send(:set_name, @info_hash)
|
110
|
+
expect(name).to eq(@info_hash['full_name'])
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should support custom name formatting set as :first_last_name' do
|
114
|
+
dice = OmniAuth::Strategies::Dice.new( app, dice_default_opts.merge({name_format: :first_last_name}) )
|
115
|
+
name = dice.send(:set_name, @info_hash)
|
116
|
+
expect(name).to eq("#{@info_hash['first_name']} #{@info_hash['last_name']}")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context ".identify_npe" do
|
121
|
+
before do
|
122
|
+
@dice = OmniAuth::Strategies::Dice.new( app, dice_default_opts )
|
123
|
+
@all_info = {
|
124
|
+
'dn' => 'cn=twilight.sparkle,ou=c001,ou=mlp,ou=pny,o=princesses of celestia,c=us',
|
125
|
+
'full_name' => 'Princess Twilight Sparkle',
|
126
|
+
'first_name' => 'twilight',
|
127
|
+
'last_name' => 'sparkle',
|
128
|
+
'email' => 'twilight@example.org'
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should identify a client as a likely npe when the CN contains a *.tld" do
|
133
|
+
npe = @dice.send( :identify_npe, @all_info.merge({'common_name' => 'twilight.mlp.com'}) )
|
134
|
+
expect(npe).to eq(true)
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should identify a client as a likely npe when there is a DN & no email" do
|
138
|
+
['email'].each { |k| @all_info.delete(k) }
|
139
|
+
npe = @dice.send(:identify_npe, @all_info)
|
140
|
+
expect(npe).to eq(true)
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should identify a client as a likely npe when there is a DN, email, and NO name fields" do
|
144
|
+
['full_name', 'first_name', 'last_name'].each { |k| @all_info.delete(k) }
|
145
|
+
npe = @dice.send(:identify_npe, @all_info)
|
146
|
+
expect(npe).to eq(true)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should identify a client as not an npe when there is a DN, email, and ANY name field" do
|
150
|
+
name_keys = ['full_name', 'first_name', 'last_name']
|
151
|
+
names = ['Twilight Sparkle', 'Twilight', 'Sparkle']
|
152
|
+
name_keys.each{ |key| @all_info.delete(key) }
|
153
|
+
name_keys.each_with_index do |key, index|
|
154
|
+
name_hash = @all_info.dup
|
155
|
+
name_hash[key] = names[index]
|
156
|
+
npe = @dice.send(:identify_npe, name_hash)
|
157
|
+
expect(npe).to eq(false)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|