saml_idp 0.2.1 → 0.3.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.
@@ -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