saml_idp 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -24,7 +24,7 @@ module SamlIdp
24
24
  end
25
25
 
26
26
  def encode
27
- Base64.encode64(raw)
27
+ Base64.strict_encode64(raw)
28
28
  end
29
29
  private :encode
30
30
 
@@ -15,6 +15,7 @@ module SamlIdp
15
15
  attr_accessor :x509_certificate
16
16
  attr_accessor :authn_context_classref
17
17
  attr_accessor :expiry
18
+ attr_accessor :encryption_opts
18
19
 
19
20
  def initialize(reference_id,
20
21
  response_id,
@@ -25,7 +26,8 @@ module SamlIdp
25
26
  saml_acs_url,
26
27
  algorithm,
27
28
  authn_context_classref,
28
- expiry=60*60
29
+ expiry=60*60,
30
+ encryption_opts=nil
29
31
  )
30
32
  self.reference_id = reference_id
31
33
  self.response_id = response_id
@@ -39,6 +41,7 @@ module SamlIdp
39
41
  self.x509_certificate = x509_certificate
40
42
  self.authn_context_classref = authn_context_classref
41
43
  self.expiry = expiry
44
+ self.encryption_opts = encryption_opts
42
45
  end
43
46
 
44
47
  def build
@@ -46,9 +49,13 @@ module SamlIdp
46
49
  end
47
50
 
48
51
  def signed_assertion
49
- assertion_builder.signed
52
+ if encryption_opts
53
+ assertion_builder.encrypt(sign: true)
54
+ else
55
+ assertion_builder.signed
56
+ end
50
57
  end
51
- private
58
+ private :signed_assertion
52
59
 
53
60
  def response_builder
54
61
  ResponseBuilder.new(response_id, issuer_uri, saml_acs_url, saml_request_id, signed_assertion)
@@ -64,7 +71,8 @@ module SamlIdp
64
71
  saml_acs_url,
65
72
  algorithm,
66
73
  authn_context_classref,
67
- expiry
74
+ expiry,
75
+ encryption_opts
68
76
  end
69
77
  private :assertion_builder
70
78
  end
@@ -6,10 +6,12 @@ module SamlIdp
6
6
  class ServiceProvider
7
7
  include Attributeable
8
8
  attribute :identifier
9
+ attribute :cert
9
10
  attribute :fingerprint
10
11
  attribute :metadata_url
11
12
  attribute :validate_signature
12
13
  attribute :acs_url
14
+ attribute :assertion_consumer_logout_service_url
13
15
 
14
16
  delegate :config, to: :SamlIdp
15
17
 
@@ -17,9 +19,12 @@ module SamlIdp
17
19
  attributes.present?
18
20
  end
19
21
 
20
- def valid_signature?(doc)
21
- !should_validate_signature? ||
22
+ def valid_signature?(doc, require_signature = false)
23
+ if require_signature || should_validate_signature?
22
24
  doc.valid_signature?(fingerprint)
25
+ else
26
+ true
27
+ end
23
28
  end
24
29
 
25
30
  def should_validate_signature?
@@ -110,7 +110,7 @@ module SamlIdp
110
110
  digest_algorithm = get_algorithm
111
111
 
112
112
  hash = digest_algorithm.digest(canon_hashed_element)
113
- Base64.encode64(hash).gsub(/\n/, '')
113
+ Base64.strict_encode64(hash).gsub(/\n/, '')
114
114
  end
115
115
  private :digest
116
116
 
@@ -76,7 +76,7 @@ module SamlIdp
76
76
 
77
77
  def encoded
78
78
  key = OpenSSL::PKey::RSA.new(secret_key, password)
79
- Base64.encode64(key.sign(algorithm.new, raw))
79
+ Base64.strict_encode64(key.sign(algorithm.new, raw))
80
80
  end
81
81
  private :encoded
82
82
 
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module SamlIdp
3
- VERSION = '0.2.1'
3
+ VERSION = '0.3.0'
4
4
  end
@@ -52,15 +52,28 @@ module SamlIdp
52
52
  cert = OpenSSL::X509::Certificate.new(cert_text)
53
53
 
54
54
  # check cert matches registered idp cert
55
- fingerprint = Digest::SHA1.hexdigest(cert.to_der)
55
+ fingerprint = fingerprint_cert(cert)
56
+ sha1_fingerprint = fingerprint_cert_sha1(cert)
57
+ plain_idp_cert_fingerprint = idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
56
58
 
57
- if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
59
+ if fingerprint != plain_idp_cert_fingerprint && sha1_fingerprint != plain_idp_cert_fingerprint
58
60
  return soft ? false : (raise ValidationError.new("Fingerprint mismatch"))
59
61
  end
60
62
 
61
63
  validate_doc(base64_cert, soft)
62
64
  end
63
65
 
66
+ def fingerprint_cert(cert)
67
+ # pick algorithm based on the doc's digest algorithm
68
+ ref_elem = REXML::XPath.first(self, "//ds:Reference", {"ds"=>DSIG})
69
+ digest_algorithm = algorithm(REXML::XPath.first(ref_elem, "//ds:DigestMethod"))
70
+ digest_algorithm.hexdigest(cert.to_der)
71
+ end
72
+
73
+ def fingerprint_cert_sha1(cert)
74
+ OpenSSL::Digest::SHA1.hexdigest(cert.to_der)
75
+ end
76
+
64
77
  def validate_doc(base64_cert, soft = true)
65
78
  # validate references
66
79
 
data/saml_idp.gemspec CHANGED
@@ -34,6 +34,11 @@ If you just need to see the certificate `bundle open saml_idp` and go to
34
34
 
35
35
  Similarly, please see the README about certificates - you should avoid using the
36
36
  defaults in a Production environment. Post any issues you to github.
37
+
38
+ ** New in Version 0.3.0 **
39
+
40
+ Encrypted Assertions require the xmlenc gem. See the example in the Controller
41
+ section of the README.
37
42
  INST
38
43
 
39
44
  s.add_dependency('activesupport')
@@ -45,9 +50,10 @@ defaults in a Production environment. Post any issues you to github.
45
50
  s.add_development_dependency "rake"
46
51
  s.add_development_dependency "simplecov"
47
52
  s.add_development_dependency "rspec", "~> 2.5"
48
- s.add_development_dependency "ruby-saml", "~> 0.8"
53
+ s.add_development_dependency "ruby-saml", "~> 1.2"
49
54
  s.add_development_dependency("rails", "~> 3.2")
50
55
  s.add_development_dependency("capybara")
51
56
  s.add_development_dependency("timecop")
57
+ s.add_development_dependency("xmlenc", ">= 0.6.4")
52
58
  end
53
59
 
@@ -12,6 +12,13 @@ module SamlIdp
12
12
  Saml::XML::Namespaces::AuthnContext::ClassRef::PASSWORD
13
13
  }
14
14
  let(:expiry) { 3*60*60 }
15
+ let (:encryption_opts) do
16
+ {
17
+ cert: Default::X509_CERTIFICATE,
18
+ block_encryption: 'aes256-cbc',
19
+ key_transport: 'rsa-oaep-mgf1p',
20
+ }
21
+ end
15
22
  subject { described_class.new(
16
23
  reference_id,
17
24
  issuer_uri,
@@ -29,5 +36,22 @@ module SamlIdp
29
36
  subject.raw.should == "<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">foo@example.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"email-address\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion>"
30
37
  end
31
38
  end
39
+
40
+ it "builds encrypted XML" do
41
+ builder = described_class.new(
42
+ reference_id,
43
+ issuer_uri,
44
+ name_id,
45
+ audience_uri,
46
+ saml_request_id,
47
+ saml_acs_url,
48
+ algorithm,
49
+ authn_context_classref,
50
+ expiry,
51
+ encryption_opts
52
+ )
53
+ encrypted_xml = builder.encrypt
54
+ encrypted_xml.should_not match(audience_uri)
55
+ end
32
56
  end
33
57
  end
@@ -10,6 +10,7 @@ module SamlIdp
10
10
  it { should respond_to :reference_id_generator }
11
11
  it { should respond_to :attribute_service_location }
12
12
  it { should respond_to :single_service_post_location }
13
+ it { should respond_to :single_logout_service_post_location }
13
14
  it { should respond_to :name_id }
14
15
  it { should respond_to :attributes }
15
16
  it { should respond_to :service_provider }
@@ -25,26 +25,56 @@ describe SamlIdp::Controller do
25
25
  end
26
26
 
27
27
  let(:principal) { double email_address: "foo@example.com" }
28
+ let (:encryption_opts) do
29
+ {
30
+ cert: SamlIdp::Default::X509_CERTIFICATE,
31
+ block_encryption: 'aes256-cbc',
32
+ key_transport: 'rsa-oaep-mgf1p',
33
+ }
34
+ end
28
35
 
29
36
  it "should create a SAML Response" do
30
37
  saml_response = encode_response(principal)
31
38
  response = OneLogin::RubySaml::Response.new(saml_response)
32
39
  response.name_id.should == "foo@example.com"
33
- response.issuer.should == "http://example.com"
40
+ response.issuers.first.should == "http://example.com"
34
41
  response.settings = saml_settings
35
42
  response.is_valid?.should be_truthy
36
43
  end
37
44
 
45
+ it "should create a SAML Logout Response" do
46
+ params[:SAMLRequest] = make_saml_logout_request
47
+ validate_saml_request
48
+ expect(saml_request.logout_request?).to eq true
49
+ saml_response = encode_response(principal)
50
+ response = OneLogin::RubySaml::Logoutresponse.new(saml_response, saml_settings)
51
+ response.validate.should == true
52
+ response.issuer.should == "http://example.com"
53
+ end
54
+
38
55
  [:sha1, :sha256, :sha384, :sha512].each do |algorithm_name|
39
56
  it "should create a SAML Response using the #{algorithm_name} algorithm" do
40
57
  self.algorithm = algorithm_name
41
58
  saml_response = encode_response(principal)
42
59
  response = OneLogin::RubySaml::Response.new(saml_response)
43
60
  response.name_id.should == "foo@example.com"
44
- response.issuer.should == "http://example.com"
61
+ response.issuers.first.should == "http://example.com"
45
62
  response.settings = saml_settings
46
63
  response.is_valid?.should be_truthy
47
64
  end
65
+
66
+ it "should encrypt SAML Response assertion" do
67
+ self.algorithm = algorithm_name
68
+ saml_response = encode_response(principal, encryption: encryption_opts)
69
+ resp_settings = saml_settings
70
+ resp_settings.private_key = SamlIdp::Default::SECRET_KEY
71
+ response = OneLogin::RubySaml::Response.new(saml_response, settings: resp_settings)
72
+ response.document.to_s.should_not match("foo@example.com")
73
+ response.decrypted_document.to_s.should match("foo@example.com")
74
+ response.name_id.should == "foo@example.com"
75
+ response.issuers.first.should == "http://example.com"
76
+ response.is_valid?.should be_truthy
77
+ end
48
78
  end
49
79
  end
50
80
 
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ require 'saml_idp/encryptor'
4
+
5
+ module SamlIdp
6
+ describe Encryptor do
7
+ let (:encryption_opts) do
8
+ {
9
+ cert: Default::X509_CERTIFICATE,
10
+ block_encryption: 'aes256-cbc',
11
+ key_transport: 'rsa-oaep-mgf1p',
12
+ }
13
+ end
14
+
15
+ subject { described_class.new encryption_opts }
16
+
17
+ it "encrypts XML" do
18
+ raw_xml = '<foo>bar</foo>'
19
+ encrypted_xml = subject.encrypt(raw_xml)
20
+ encrypted_xml.should_not match 'bar'
21
+ encrypted_doc = Nokogiri::XML::Document.parse(encrypted_xml)
22
+ encrypted_data = Xmlenc::EncryptedData.new(encrypted_doc.at_xpath('//xenc:EncryptedData', Xmlenc::NAMESPACES))
23
+ decrypted_xml = encrypted_data.decrypt(subject.encryption_key)
24
+ decrypted_xml.should == raw_xml
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'saml_idp/logout_request_builder'
3
+
4
+ module SamlIdp
5
+ describe LogoutRequestBuilder do
6
+ before do
7
+ Timecop.freeze(Time.local(1990))
8
+ end
9
+
10
+ after do
11
+ Timecop.return
12
+ end
13
+
14
+ let(:response_id) { 'some_response_id' }
15
+ let(:issuer_uri) { 'http://example.com' }
16
+ let(:saml_slo_url) { 'http://localhost:3000/saml/logout' }
17
+ let(:name_id) { 'some_name_id' }
18
+ let(:session_index) { 'abc123index' }
19
+ let(:algorithm) { OpenSSL::Digest::SHA256 }
20
+
21
+ subject do
22
+ described_class.new(
23
+ response_id,
24
+ issuer_uri,
25
+ saml_slo_url,
26
+ name_id,
27
+ session_index,
28
+ algorithm
29
+ )
30
+ end
31
+
32
+ it "is a valid SloLogoutrequest" do
33
+ Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
34
+ slo_request = OneLogin::RubySaml::SloLogoutrequest.new(
35
+ subject.encoded,
36
+ settings: saml_settings('localhost:3000')
37
+ )
38
+ slo_request.soft = false
39
+ expect(slo_request.is_valid?).to eq true
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'saml_idp/logout_response_builder'
3
+
4
+ module SamlIdp
5
+ describe LogoutResponseBuilder do
6
+ before do
7
+ Timecop.freeze(Time.local(1990))
8
+ end
9
+
10
+ after do
11
+ Timecop.return
12
+ end
13
+
14
+ let(:response_id) { 'some_response_id' }
15
+ let(:issuer_uri) { 'http://example.com' }
16
+ let(:saml_slo_url) { 'http://localhost:3000/saml/logout' }
17
+ let(:request_id) { 'some_request_id' }
18
+ let(:algorithm) { OpenSSL::Digest::SHA256 }
19
+
20
+ subject do
21
+ described_class.new(
22
+ response_id,
23
+ issuer_uri,
24
+ saml_slo_url,
25
+ request_id,
26
+ algorithm
27
+ )
28
+ end
29
+
30
+ it "is a valid LogoutResponse" do
31
+ Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
32
+ logout_response = OneLogin::RubySaml::Logoutresponse.new(
33
+ subject.encoded,
34
+ saml_settings('localhost:3000')
35
+ )
36
+ logout_response.soft = false
37
+ expect(logout_response.validate).to eq true
38
+ end
39
+ end
40
+ end
41
+ end
@@ -8,5 +8,12 @@ module SamlIdp
8
8
  it "signs valid xml" do
9
9
  Saml::XML::Document.parse(subject.signed).valid_signature?(Default::FINGERPRINT).should be_truthy
10
10
  end
11
+
12
+ it "includes logout element" do
13
+ subject.configurator.single_logout_service_post_location = 'https://example.com/saml/logout'
14
+ subject.fresh.should match(
15
+ '<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/saml/logout"/>'
16
+ )
17
+ end
11
18
  end
12
19
  end
@@ -1,32 +1,90 @@
1
1
  require 'spec_helper'
2
2
  module SamlIdp
3
3
  describe Request do
4
- let(:raw_request) { "<samlp:AuthnRequest AssertionConsumerServiceURL='http://localhost:3000/saml/consume' Destination='http://localhost:1337/saml/auth' ID='_af43d1a0-e111-0130-661a-3c0754403fdb' IssueInstant='2013-08-06T22:01:35Z' Version='2.0' xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'><saml:Issuer xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'>localhost:3000</saml:Issuer><samlp:NameIDPolicy AllowCreate='true' Format='urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'/></samlp:AuthnRequest>" }
5
- subject { described_class.new raw_request }
4
+ describe "authn request" do
5
+ let(:raw_authn_request) { "<samlp:AuthnRequest AssertionConsumerServiceURL='http://localhost:3000/saml/consume' Destination='http://localhost:1337/saml/auth' ID='_af43d1a0-e111-0130-661a-3c0754403fdb' IssueInstant='2013-08-06T22:01:35Z' Version='2.0' xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'><saml:Issuer xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'>localhost:3000</saml:Issuer><samlp:NameIDPolicy AllowCreate='true' Format='urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'/><samlp:RequestedAuthnContext Comparison='exact'><saml:AuthnContextClassRef xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp:AuthnRequest>" }
6
+ subject { described_class.new raw_authn_request }
6
7
 
7
- it "has a valid request_id" do
8
- subject.request_id.should == "_af43d1a0-e111-0130-661a-3c0754403fdb"
9
- end
8
+ it "has a valid request_id" do
9
+ subject.request_id.should == "_af43d1a0-e111-0130-661a-3c0754403fdb"
10
+ end
10
11
 
11
- it "has a valid acs_url" do
12
- subject.acs_url.should == "http://localhost:3000/saml/consume"
13
- end
12
+ it "has a valid acs_url" do
13
+ subject.acs_url.should == "http://localhost:3000/saml/consume"
14
+ end
14
15
 
15
- it "has a valid service_provider" do
16
- subject.service_provider.should be_a ServiceProvider
17
- end
16
+ it "has a valid service_provider" do
17
+ subject.service_provider.should be_a ServiceProvider
18
+ end
18
19
 
19
- it "has a valid service_provider" do
20
- subject.service_provider.should be_truthy
21
- end
20
+ it "has a valid service_provider" do
21
+ subject.service_provider.should be_truthy
22
+ end
22
23
 
23
- it "has a valid issuer" do
24
- subject.issuer.should == "localhost:3000"
25
- end
24
+ it "has a valid issuer" do
25
+ subject.issuer.should == "localhost:3000"
26
+ end
27
+
28
+ it "has a valid valid_signature" do
29
+ subject.valid_signature?.should be_truthy
30
+ end
31
+
32
+ it "should return acs_url for response_url" do
33
+ subject.response_url.should == subject.acs_url
34
+ end
35
+
36
+ it "is a authn request" do
37
+ subject.authn_request?.should == true
38
+ end
39
+
40
+ it "fetches internal request" do
41
+ subject.request['ID'].should == subject.request_id
42
+ end
43
+
44
+ it "has a valid authn context" do
45
+ subject.requested_authn_context.should == "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
46
+ end
26
47
 
27
- it "has a valid valid_signature" do
28
- subject.valid_signature?.should be_truthy
48
+ it "does not permit empty issuer" do
49
+ raw_req = raw_authn_request.gsub('localhost:3000', '')
50
+ authn_request = described_class.new raw_req
51
+ authn_request.issuer.should_not == ''
52
+ authn_request.issuer.should == nil
53
+ end
29
54
  end
30
55
 
56
+ describe "logout request" do
57
+ let(:raw_logout_request) { "<LogoutRequest ID='_some_response_id' Version='2.0' IssueInstant='2010-06-01T13:00:00Z' Destination='http://localhost:3000/saml/logout' xmlns='urn:oasis:names:tc:SAML:2.0:protocol'><Issuer xmlns='urn:oasis:names:tc:SAML:2.0:assertion'>http://example.com</Issuer><NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' Format='urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'>some_name_id</NameID><SessionIndex>abc123index</SessionIndex></LogoutRequest>" }
58
+
59
+ subject { described_class.new raw_logout_request }
60
+
61
+ it "has a valid request_id" do
62
+ subject.request_id.should == '_some_response_id'
63
+ end
64
+
65
+ it "should be flagged as a logout_request" do
66
+ subject.logout_request?.should == true
67
+ end
68
+
69
+ it "should have a valid name_id" do
70
+ subject.name_id.should == 'some_name_id'
71
+ end
72
+
73
+ it "should have a session index" do
74
+ subject.session_index.should == 'abc123index'
75
+ end
76
+
77
+ it "should have a valid issuer" do
78
+ subject.issuer.should == 'http://example.com'
79
+ end
80
+
81
+ it "fetches internal request" do
82
+ subject.request['ID'].should == subject.request_id
83
+ end
84
+
85
+ it "should return logout_url for response_url" do
86
+ subject.response_url.should == subject.logout_url
87
+ end
88
+ end
31
89
  end
32
90
  end