omniauth-dice 0.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.
- 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
|