lyrebird 1.0.0.alpha2 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b18c9229d638025f9882d18957a043c890e128ae5cbf9541d97f0cce078ce8e1
4
- data.tar.gz: 28ce7ba82169a7fa5b1ac90299eb09af37781adefc74522a6ee43e16f822f740
3
+ metadata.gz: a2c22dc89d09728d25d5563a9db0c136b144865ddea516ea71bd1806a4bf0ce2
4
+ data.tar.gz: 002da134ef786dd04b2f61a3b2f01a2fe43ec4e772081b9d76dd4b4f5788e839
5
5
  SHA512:
6
- metadata.gz: 7edb0e358a6f295f80731bcf2d6544d55843b69c69bd25fcfbffc9a750240af558dfad598ba2939e6b9fa96033b17a7e827715908e08f3005ece03284c75f9a2
7
- data.tar.gz: c7ba17cac0ffe23dcc264c52c6b8c6ce8fc44900bc0a302866e9c9aee2f874fd6f12ae5feda573c2d864047aac56a6bb5d67a000f570b018be54062b1e0acaab
6
+ metadata.gz: dc069741583e5940e825df140b405fe9c5710632c38927d2fb0baa80c52a4dafcb06170ac72a82259b9708f651e83e2ca11ee1d758d0031c1502f2d068faba57
7
+ data.tar.gz: 78a80065561c35f30b81a588a9584c173b044064a9b73266385a6708109f0b02952e0acbcbc1d6768d1750ea47ebdcbf346f42dbc7b9e151e0731a43316c995a
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present Simple SAML
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ [![Gem Version](
2
+ https://badge.fury.io/rb/lyrebird.svg
3
+ )](https://badge.fury.io/rb/lyrebird)
4
+
1
5
  # Lyrebird
2
6
  A Ruby gem for mimicking SAML Identity Provider (IdP) responses in test
3
7
  environments.
@@ -11,6 +15,10 @@ gem "lyrebird", group: :test
11
15
  ```ruby
12
16
  # test/integration/saml_test.rb
13
17
  class SAMLTest < ActionDispatch::IntegrationTest
18
+ setup do
19
+ @idp_cert = Lyrebird::Certificate.build
20
+ end
21
+
14
22
  test "consume creates a session" do
15
23
  user = users(:alice)
16
24
 
@@ -20,6 +28,7 @@ class SAMLTest < ActionDispatch::IntegrationTest
20
28
  r.recipient = saml_consume_url
21
29
  r.audience = root_url
22
30
  r.name_id = user.email
31
+ r.sign_with = @idp_cert
23
32
 
24
33
  r.attributes do |a|
25
34
  a.email = user.email
@@ -84,20 +93,20 @@ end
84
93
  ### Getting the encoded response
85
94
  ```ruby
86
95
  response.mimic # Base64-encoded SAML response (for POST binding)
87
- response.document # REXML::Document for inspection
96
+ response.document # Nokogiri::XML::Document for inspection
88
97
  ```
89
98
 
90
99
  ### Signing
91
100
  Sign both the assertion and response with an IdP certificate:
92
101
  ```ruby
93
- idp_cert = Lyrebird::Certificate.generate
102
+ idp_cert = Lyrebird::Certificate.build
94
103
  response = Lyrebird::Response.build(sign_with: idp_cert)
95
104
  ```
96
105
 
97
106
  ### Encryption
98
107
  Encrypt assertions using the SP's certificate so only the SP can decrypt them:
99
108
  ```ruby
100
- sp_cert = Lyrebird::Certificate.generate # In practice, provided by the SP
109
+ sp_cert = Lyrebird::Certificate.build
101
110
  response = Lyrebird::Response.build(encrypt_with: sp_cert)
102
111
  ```
103
112
 
@@ -121,46 +130,49 @@ Lyrebird::NAMEID_UNSPECIFIED # unspecified
121
130
  Override defaults globally for all responses/assertions:
122
131
  ```ruby
123
132
  # test/test_helper.rb
124
- Lyrebird::DEFAULTS.issuer = "https://custom.example.com"
125
- Lyrebird::DEFAULTS.recipient = "https://custom.example.com/acs"
126
- Lyrebird::DEFAULTS.audience = "https://custom.example.com"
127
- Lyrebird::DEFAULTS.name_id = "default@example.com"
128
- Lyrebird::DEFAULTS.valid_for = 600 # 10 minutes
129
- Lyrebird::DEFAULTS.attributes = { role: "user" }
133
+ Lyrebird.configure do |d|
134
+ d.issuer = "https://custom.example.com"
135
+ d.recipient = "https://custom.example.com/acs"
136
+ d.audience = "https://custom.example.com"
137
+ d.name_id = "default@example.com"
138
+ d.valid_for = 600 # 10 minutes
139
+ d.attributes = { role: "user" }
140
+ end
130
141
  ```
142
+ Defaults are frozen after configuration for thread safety.
131
143
 
132
144
  ## Certificate
133
145
  Generates and manages X.509 certificates for signing SAML responses.
134
146
 
135
- ### Generating a new certificate
147
+ ### Building a certificate
136
148
  ```ruby
137
149
  # With defaults
138
- cert = Lyrebird::Certificate.generate
150
+ cert = Lyrebird::Certificate.build
139
151
 
140
152
  # With options
141
- cert = Lyrebird::Certificate.generate(
142
- bits: 4096, # RSA key size (default: 2048)
143
- cn: "example.com", # Common Name
144
- o: "Acme", # Organization
145
- valid_for: 30, # Validity in days (default: 365)
146
- valid_until: Time.new(2999, 12, 31) # Specific expiration (overrides valid_for)
147
- )
153
+ cert = Lyrebird::Certificate.build do |c|
154
+ c.bits = 4096 # RSA key size (default: 2048)
155
+ c.cn = "example.com" # Common Name
156
+ c.o = "Acme" # Organization
157
+ c.valid_for = 30 # Days (default: 365)
158
+ c.valid_until = Time.new(2999, 12, 31) # Overrides valid_for
159
+ end
148
160
  ```
149
161
 
150
162
  ### Loading an existing certificate
151
163
  ```ruby
152
164
  cert = Lyrebird::Certificate.load(
153
- private_key_pem: File.read("private_key.pem"),
154
- certificate_pem: File.read("certificate.pem")
165
+ key_pem: File.read("private_key.pem"),
166
+ x509_pem: File.read("certificate.pem")
155
167
  )
156
168
  ```
157
169
 
158
- ### Exporting
170
+ ### Using a certificate
159
171
  ```ruby
160
- cert.private_key # OpenSSL::PKey::RSA object
161
- cert.certificate # OpenSSL::X509::Certificate object
162
- cert.private_key_pem # PEM-encoded private key
163
- cert.certificate_pem # PEM-encoded certificate
164
- cert.base64 # Base64-encoded certificate (for SAML metadata)
165
- cert.fingerprint # SHA256 fingerprint
172
+ cert.key # OpenSSL::PKey::RSA private key
173
+ cert.x509 # OpenSSL::X509::Certificate object
174
+ cert.key_pem # PEM-encoded private key
175
+ cert.x509_pem # PEM-encoded certificate
176
+ cert.base64 # Base64-encoded certificate (for SAML metadata)
177
+ cert.fingerprint # SHA256 fingerprint
166
178
  ```
@@ -28,74 +28,110 @@ module Lyrebird
28
28
  end
29
29
 
30
30
  def document
31
- REXML::Document.new.tap do |d|
32
- d.add_element(root)
31
+ @document ||= Nokogiri::XML::Document.new.tap do |doc|
32
+ doc.root = root(doc)
33
33
  end
34
34
  end
35
35
 
36
36
  private
37
37
 
38
- def root
39
- REXML::Element.new("saml:Assertion").tap do |r|
40
- r.add_namespace("saml", SAML_ASSERTION_NS)
41
- r.add_attribute("ID", ID.generate)
42
- r.add_attribute("Version", "2.0")
43
- r.add_attribute("IssueInstant", @issue_instant.iso8601)
44
- r.add_element("saml:Issuer").text = @issuer
45
- r.add_element(subject)
46
- r.add_element(conditions)
47
- r.add_element(authn_statement)
48
- r.add_element(attribute_statement) if @attributes.any?
38
+ def root(doc)
39
+ doc.create_element("Assertion").tap do |a|
40
+ ns = a.add_namespace_definition("saml", SAML_ASSERTION_NS)
41
+
42
+ a.namespace = ns
43
+ a["ID"] = ID.generate
44
+ a["Version"] = "2.0"
45
+ a["IssueInstant"] = @issue_instant.iso8601
46
+
47
+ a.add_child(doc.create_element("Issuer")).tap do |i|
48
+ i.namespace = ns
49
+ i.content = @issuer
50
+ end
51
+
52
+ a.add_child(subject(doc, ns))
53
+ a.add_child(conditions(doc, ns))
54
+ a.add_child(authn_statement(doc, ns))
55
+ a.add_child(attribute_statement(doc, ns)) if @attributes.any?
49
56
  end
50
57
  end
51
58
 
52
- def subject
53
- REXML::Element.new("saml:Subject").tap do |s|
54
- name_id = s.add_element("saml:NameID")
55
- name_id.add_attribute("Format", @name_id_format)
56
- name_id.text = @name_id
57
- s.add_element(subject_confirmation)
59
+ def subject(doc, ns)
60
+ doc.create_element("Subject").tap do |s|
61
+ s.namespace = ns
62
+
63
+ s.add_child(doc.create_element("NameID")).tap do |nid|
64
+ nid.namespace = ns
65
+ nid["Format"] = @name_id_format
66
+ nid.content = @name_id
67
+ end
68
+
69
+ s.add_child(subject_confirmation(doc, ns))
58
70
  end
59
71
  end
60
72
 
61
- def subject_confirmation
62
- REXML::Element.new("saml:SubjectConfirmation").tap do |sc|
63
- sc.add_attribute("Method", CM_BEARER)
64
- data = sc.add_element("saml:SubjectConfirmationData")
65
- data.add_attribute("NotOnOrAfter", @not_on_or_after.iso8601)
66
- data.add_attribute("Recipient", @recipient)
67
- data.add_attribute("InResponseTo", @in_response_to) if @in_response_to
73
+ def subject_confirmation(doc, ns)
74
+ doc.create_element("SubjectConfirmation").tap do |sc|
75
+ sc.namespace = ns
76
+ sc["Method"] = CM_BEARER
77
+
78
+ sc.add_child(doc.create_element("SubjectConfirmationData")).tap do |d|
79
+ d.namespace = ns
80
+ d["NotOnOrAfter"] = @not_on_or_after.iso8601
81
+ d["Recipient"] = @recipient
82
+ d["InResponseTo"] = @in_response_to if @in_response_to
83
+ end
68
84
  end
69
85
  end
70
86
 
71
- def conditions
72
- REXML::Element.new("saml:Conditions").tap do |c|
73
- c.add_attribute("NotBefore", @not_before.iso8601)
74
- c.add_attribute("NotOnOrAfter", @not_on_or_after.iso8601)
75
- ar = c.add_element("saml:AudienceRestriction")
76
- ar.add_element("saml:Audience").text = @audience
87
+ def conditions(doc, ns)
88
+ doc.create_element("Conditions").tap do |c|
89
+ c.namespace = ns
90
+ c["NotBefore"] = @not_before.iso8601
91
+ c["NotOnOrAfter"] = @not_on_or_after.iso8601
92
+
93
+ c.add_child(doc.create_element("AudienceRestriction")).tap do |ar|
94
+ ar.namespace = ns
95
+ ar.add_child(doc.create_element("Audience")).tap do |a|
96
+ a.namespace = ns
97
+ a.content = @audience
98
+ end
99
+ end
77
100
  end
78
101
  end
79
102
 
80
- def authn_statement
81
- REXML::Element.new("saml:AuthnStatement").tap do |as|
82
- as.add_attribute("AuthnInstant", @issue_instant.iso8601)
83
- as.add_attribute("SessionIndex", ID.generate)
84
- ac = as.add_element("saml:AuthnContext")
85
- cr = ac.add_element("saml:AuthnContextClassRef")
86
- cr.text = @authn_context
103
+ def authn_statement(doc, ns)
104
+ doc.create_element("AuthnStatement").tap do |as|
105
+ as.namespace = ns
106
+ as["AuthnInstant"] = @issue_instant.iso8601
107
+ as["SessionIndex"] = ID.generate
108
+
109
+ as.add_child(doc.create_element("AuthnContext")).tap do |ac|
110
+ ac.namespace = ns
111
+ ac.add_child(doc.create_element("AuthnContextClassRef")).tap do |cr|
112
+ cr.namespace = ns
113
+ cr.content = @authn_context
114
+ end
115
+ end
87
116
  end
88
117
  end
89
118
 
90
- def attribute_statement
91
- REXML::Element.new("saml:AttributeStatement").tap do |as|
119
+ def attribute_statement(doc, ns)
120
+ doc.create_element("AttributeStatement").tap do |as|
121
+ as.namespace = ns
122
+
92
123
  @attributes.each do |name, values|
93
- a = as.add_element("saml:Attribute")
94
- a.add_attribute("Name", name)
95
- a.add_attribute("NameFormat", ATTR_NAME_FORMAT)
124
+ as.add_child(doc.create_element("Attribute")).tap do |a|
125
+ a.namespace = ns
126
+ a["Name"] = name
127
+ a["NameFormat"] = ATTR_NAME_FORMAT
96
128
 
97
- Array(values).each do |value|
98
- a.add_element("saml:AttributeValue").text = value
129
+ Array(values).each do |value|
130
+ a.add_child(doc.create_element("AttributeValue")).tap do |av|
131
+ av.namespace = ns
132
+ av.content = value
133
+ end
134
+ end
99
135
  end
100
136
  end
101
137
  end
@@ -2,62 +2,67 @@
2
2
 
3
3
  module Lyrebird
4
4
  class Certificate
5
- attr_reader :private_key, :certificate
5
+ attr_reader :key, :x509
6
6
 
7
- def self.generate(bits: 2048, **options)
8
- new(OpenSSL::PKey::RSA.new(bits), **options)
7
+ def self.build(**kwargs)
8
+ config = OpenStruct.new(kwargs)
9
+ yield config if block_given?
10
+ new(**config.to_h)
9
11
  end
10
12
 
11
- def self.load(private_key_pem:, certificate_pem:)
12
- private_key = OpenSSL::PKey::RSA.new(private_key_pem)
13
- certificate = OpenSSL::X509::Certificate.new(certificate_pem)
14
- new(private_key, certificate: certificate)
13
+ def self.load(key_pem:, x509_pem:)
14
+ key = OpenSSL::PKey::RSA.new(key_pem)
15
+ x509 = OpenSSL::X509::Certificate.new(x509_pem)
16
+ new(key: key, x509: x509)
15
17
  end
16
18
 
17
19
  def initialize(
18
- private_key,
20
+ bits: 2048,
19
21
  cn: nil,
20
22
  o: nil,
21
23
  valid_for: 365,
22
24
  valid_until: nil,
23
- certificate: nil
25
+ key: nil,
26
+ x509: nil
24
27
  )
25
- @private_key = private_key
26
28
  @common_name = cn
27
29
  @organization = o
28
30
  @valid_for = valid_for
29
31
  @valid_until = valid_until
30
- @certificate = certificate || build_certificate
32
+ @key = key || OpenSSL::PKey::RSA.new(bits)
33
+ @x509 = x509 || build_x509
31
34
  end
32
35
 
33
- def private_key_pem
34
- @private_key.to_pem
36
+ def key_pem
37
+ @key.to_pem
35
38
  end
36
39
 
37
- def certificate_pem
38
- @certificate.to_pem
40
+ def x509_pem
41
+ @x509.to_pem
39
42
  end
40
43
 
41
44
  def fingerprint
42
- OpenSSL::Digest::SHA256.hexdigest(@certificate.to_der)
45
+ OpenSSL::Digest::SHA256.hexdigest(@x509.to_der)
43
46
  end
44
47
 
45
48
  def base64
46
- Base64.strict_encode64(@certificate.to_der)
49
+ Base64.strict_encode64(@x509.to_der)
47
50
  end
48
51
 
49
52
  private
50
53
 
51
- def build_certificate
52
- now = Time.now
54
+ def build_x509
55
+ now = Time.now.utc
53
56
 
54
57
  OpenSSL::X509::Certificate.new.tap do |c|
55
- c.public_key = @private_key.public_key
58
+ c.version = 2
59
+ c.serial = OpenSSL::BN.rand(64)
60
+ c.public_key = @key.public_key
56
61
  c.subject = build_subject
57
62
  c.issuer = c.subject
58
63
  c.not_before = now
59
64
  c.not_after = @valid_until || now + (@valid_for * 86_400)
60
- c.sign(@private_key, OpenSSL::Digest::SHA256.new)
65
+ c.sign(@key, OpenSSL::Digest::SHA256.new)
61
66
  end
62
67
  end
63
68
 
@@ -33,7 +33,7 @@ module Lyrebird
33
33
  @attributes = {
34
34
  first_name: "Test",
35
35
  last_name: "User",
36
- }
36
+ }.freeze
37
37
  end
38
38
  end
39
39
 
@@ -5,70 +5,91 @@ module Lyrebird
5
5
  def initialize(element, certificate)
6
6
  @element = element
7
7
  @certificate = certificate
8
+ @doc = element.document
8
9
  @aes_key = SecureRandom.random_bytes(32)
9
10
  end
10
11
 
11
12
  def encrypt
12
- encrypted_assertion
13
+ build_encrypted_assertion
13
14
  end
14
15
 
15
16
  private
16
17
 
17
- def encrypted_assertion
18
- REXML::Element.new("saml:EncryptedAssertion").tap do |ea|
19
- ea.add_namespace("saml", SAML_ASSERTION_NS)
20
- ea.add_element(encrypted_data)
18
+ def build_encrypted_assertion
19
+ @doc.create_element("EncryptedAssertion").tap do |ea|
20
+ @saml = ea.add_namespace_definition("saml", SAML_ASSERTION_NS)
21
+ ea.namespace = @saml
22
+ ea.add_child(build_encrypted_data)
21
23
  end
22
24
  end
23
25
 
24
- def encrypted_data
25
- REXML::Element.new("xenc:EncryptedData").tap do |ed|
26
- ed.add_namespace("xenc", XMLENC_NS)
27
- ed.add_attribute("Type", "#{XMLENC_NS}Element")
28
- em = ed.add_element("xenc:EncryptionMethod")
29
- em.add_attribute("Algorithm", AES256_CBC)
30
- ed.add_element(key_info)
31
- ed.add_element(cipher_data)
26
+ def build_encrypted_data
27
+ @doc.create_element("EncryptedData").tap do |ed|
28
+ @xenc = ed.add_namespace_definition("xenc", XMLENC_NS)
29
+ ed.namespace = @xenc
30
+ ed["Type"] = "#{XMLENC_NS}Element"
31
+
32
+ ed.add_child(@doc.create_element("EncryptionMethod")).tap do |em|
33
+ em.namespace = @xenc
34
+ em["Algorithm"] = AES256_CBC
35
+ end
36
+
37
+ ed.add_child(build_key_info)
38
+ ed.add_child(build_cipher_data)
32
39
  end
33
40
  end
34
41
 
35
- def key_info
36
- REXML::Element.new("ds:KeyInfo").tap do |ki|
37
- ki.add_namespace("ds", XMLDSIG_NS)
38
- ki.add_element(encrypted_key)
42
+ def build_key_info
43
+ @doc.create_element("KeyInfo").tap do |ki|
44
+ @ds = ki.add_namespace_definition("ds", XMLDSIG_NS)
45
+ ki.namespace = @ds
46
+ ki.add_child(build_encrypted_key)
39
47
  end
40
48
  end
41
49
 
42
- def encrypted_key
43
- REXML::Element.new("xenc:EncryptedKey").tap do |ek|
44
- ek.add_namespace("xenc", XMLENC_NS)
45
- em = ek.add_element("xenc:EncryptionMethod")
46
- em.add_attribute("Algorithm", RSA_OAEP)
47
- ek.add_element(encrypted_key_cipher_data)
50
+ def build_encrypted_key
51
+ @doc.create_element("EncryptedKey").tap do |ek|
52
+ ek.add_namespace_definition("xenc", XMLENC_NS)
53
+ ek.namespace = @xenc
54
+
55
+ ek.add_child(@doc.create_element("EncryptionMethod")).tap do |em|
56
+ em.namespace = @xenc
57
+ em["Algorithm"] = RSA_OAEP
58
+ end
59
+
60
+ ek.add_child(build_encrypted_key_cipher_data)
48
61
  end
49
62
  end
50
63
 
51
- def encrypted_key_cipher_data
52
- public_key = @certificate.certificate.public_key
64
+ def build_encrypted_key_cipher_data
65
+ public_key = @certificate.x509.public_key
53
66
  padding = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
54
67
  encrypted_aes_key = public_key.public_encrypt(@aes_key, padding)
55
68
 
56
- REXML::Element.new("xenc:CipherData").tap do |cd|
57
- cv = Base64.strict_encode64(encrypted_aes_key)
58
- cd.add_element("xenc:CipherValue").text = cv
69
+ @doc.create_element("CipherData").tap do |cd|
70
+ cd.namespace = @xenc
71
+
72
+ cd.add_child(@doc.create_element("CipherValue")).tap do |cv|
73
+ cv.namespace = @xenc
74
+ cv.content = Base64.strict_encode64(encrypted_aes_key)
75
+ end
59
76
  end
60
77
  end
61
78
 
62
- def cipher_data
79
+ def build_cipher_data
63
80
  cipher = OpenSSL::Cipher.new("AES-256-CBC")
64
81
  cipher.encrypt
65
82
  cipher.key = @aes_key
66
83
  iv = cipher.random_iv
67
- ciphertext = cipher.update(@element.to_s) + cipher.final
84
+ ciphertext = cipher.update(@element.to_xml) + cipher.final
85
+
86
+ @doc.create_element("CipherData").tap do |cd|
87
+ cd.namespace = @xenc
68
88
 
69
- REXML::Element.new("xenc:CipherData").tap do |cd|
70
- cv = Base64.strict_encode64(iv + ciphertext)
71
- cd.add_element("xenc:CipherValue").text = cv
89
+ cd.add_child(@doc.create_element("CipherValue")).tap do |cv|
90
+ cv.namespace = @xenc
91
+ cv.content = Base64.strict_encode64(iv + ciphertext)
92
+ end
72
93
  end
73
94
  end
74
95
  end
@@ -35,44 +35,71 @@ module Lyrebird
35
35
  end
36
36
 
37
37
  def mimic
38
- Base64.strict_encode64(document.to_s)
38
+ Base64.strict_encode64(document.to_xml(save_with: 0))
39
39
  end
40
40
 
41
41
  def document
42
- REXML::Document.new.tap do |d|
43
- d.add_element(root)
42
+ @document ||= Nokogiri::XML::Document.new.tap do |doc|
43
+ doc.root = build_response(doc)
44
+ sign_assertion(doc) if @sign_with && !@encrypt_with
45
+ sign_response(doc) if @sign_with
44
46
  end
45
47
  end
46
48
 
47
49
  private
48
50
 
49
- def root
50
- REXML::Element.new("samlp:Response").tap do |r|
51
- r.add_namespace("samlp", SAML_PROTOCOL_NS)
52
- r.add_namespace("saml", SAML_ASSERTION_NS)
53
- r.add_attribute("ID", ID.generate)
54
- r.add_attribute("Version", "2.0")
55
- r.add_attribute("IssueInstant", Time.now.utc.iso8601)
56
- r.add_attribute("Destination", @destination) if @destination
57
- r.add_attribute("InResponseTo", @in_response_to) if @in_response_to
58
- r.add_element("saml:Issuer").text = @issuer
59
- r.add_element(status)
60
- r.add_element(assertion_element)
61
- Signature.new(r, @sign_with).sign! if @sign_with
51
+ def build_response(doc)
52
+ doc.create_element("Response").tap do |r|
53
+ @samlp = r.add_namespace_definition("samlp", SAML_PROTOCOL_NS)
54
+ @saml = r.add_namespace_definition("saml", SAML_ASSERTION_NS)
55
+
56
+ r.namespace = @samlp
57
+ r["ID"] = ID.generate
58
+ r["Version"] = "2.0"
59
+ r["IssueInstant"] = Time.now.utc.iso8601
60
+ r["Destination"] = @destination if @destination
61
+ r["InResponseTo"] = @in_response_to if @in_response_to
62
+
63
+ r.add_child(doc.create_element("Issuer")).tap do |i|
64
+ i.namespace = @saml
65
+ i.content = @issuer
66
+ end
67
+
68
+ r.add_child(build_status(doc))
69
+ r.add_child(build_assertion_element(doc))
70
+ end
71
+ end
72
+
73
+ def build_assertion_element(doc)
74
+ assertion_doc = @assertion.document
75
+ element = assertion_doc.root
76
+
77
+ if @encrypt_with
78
+ Signature.new(element, @sign_with).sign! if @sign_with
79
+ Encryption.new(element, @encrypt_with).encrypt
80
+ else
81
+ element
62
82
  end
63
83
  end
64
84
 
65
- def assertion_element
66
- element = @assertion.document.root
67
- Signature.new(element, @sign_with).sign! if @sign_with
68
- return element unless @encrypt_with
69
- Encryption.new(element, @encrypt_with).encrypt
85
+ def sign_assertion(doc)
86
+ ns = { "saml" => SAML_ASSERTION_NS }
87
+ assertion = doc.at_xpath("//saml:Assertion", ns)
88
+ Signature.new(assertion, @sign_with).sign!
70
89
  end
71
90
 
72
- def status
73
- REXML::Element.new("samlp:Status").tap do |s|
74
- sc = s.add_element("samlp:StatusCode")
75
- sc.add_attribute("Value", STATUS_SUCCESS)
91
+ def sign_response(doc)
92
+ Signature.new(doc.root, @sign_with).sign!
93
+ end
94
+
95
+ def build_status(doc)
96
+ doc.create_element("Status").tap do |s|
97
+ s.namespace = @samlp
98
+
99
+ s.add_child(doc.create_element("StatusCode")).tap do |sc|
100
+ sc.namespace = @samlp
101
+ sc["Value"] = STATUS_SUCCESS
102
+ end
76
103
  end
77
104
  end
78
105
  end