lyrebird 1.0.0.alpha2 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b18c9229d638025f9882d18957a043c890e128ae5cbf9541d97f0cce078ce8e1
4
- data.tar.gz: 28ce7ba82169a7fa5b1ac90299eb09af37781adefc74522a6ee43e16f822f740
3
+ metadata.gz: 61005075359475ff6b492db62c80f7fea1724bb059c03f19a3f9d04c1f37871f
4
+ data.tar.gz: 0e44c46fe69c8701c7b5441f7a23b6bbf752ad957ae044e5f19d398c01a8db91
5
5
  SHA512:
6
- metadata.gz: 7edb0e358a6f295f80731bcf2d6544d55843b69c69bd25fcfbffc9a750240af558dfad598ba2939e6b9fa96033b17a7e827715908e08f3005ece03284c75f9a2
7
- data.tar.gz: c7ba17cac0ffe23dcc264c52c6b8c6ce8fc44900bc0a302866e9c9aee2f874fd6f12ae5feda573c2d864047aac56a6bb5d67a000f570b018be54062b1e0acaab
6
+ metadata.gz: 1ccd8ff5e0c7a4f441dc275a6231924f8969993c7b670b83a60d01f16e4ebf8fff33ccedffaad21a45dc6bca056d1450c5b6566699f9fd733f71fc9342818bec
7
+ data.tar.gz: c0ebff2c3cc4134b84fb52327ee41c7d34bd9b2ab3d89ac7a50a3485bc912ca6936ad64388b04253001106a961f7f2caad313b88a2915e86c7bbd4c516eae673
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.
@@ -84,20 +88,20 @@ end
84
88
  ### Getting the encoded response
85
89
  ```ruby
86
90
  response.mimic # Base64-encoded SAML response (for POST binding)
87
- response.document # REXML::Document for inspection
91
+ response.document # Nokogiri::XML::Document for inspection
88
92
  ```
89
93
 
90
94
  ### Signing
91
95
  Sign both the assertion and response with an IdP certificate:
92
96
  ```ruby
93
- idp_cert = Lyrebird::Certificate.generate
97
+ idp_cert = Lyrebird::Certificate.build
94
98
  response = Lyrebird::Response.build(sign_with: idp_cert)
95
99
  ```
96
100
 
97
101
  ### Encryption
98
102
  Encrypt assertions using the SP's certificate so only the SP can decrypt them:
99
103
  ```ruby
100
- sp_cert = Lyrebird::Certificate.generate # In practice, provided by the SP
104
+ sp_cert = Lyrebird::Certificate.build
101
105
  response = Lyrebird::Response.build(encrypt_with: sp_cert)
102
106
  ```
103
107
 
@@ -121,46 +125,50 @@ Lyrebird::NAMEID_UNSPECIFIED # unspecified
121
125
  Override defaults globally for all responses/assertions:
122
126
  ```ruby
123
127
  # 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" }
128
+ Lyrebird.configure do |d|
129
+ d.issuer = "https://custom.example.com"
130
+ d.recipient = "https://custom.example.com/acs"
131
+ d.audience = "https://custom.example.com"
132
+ d.name_id = "default@example.com"
133
+ d.valid_for = 600 # 10 minutes
134
+ d.attributes = { role: "user" }
135
+ end
130
136
  ```
137
+ Defaults are frozen after configuration for thread safety.
131
138
 
132
139
  ## Certificate
133
140
  Generates and manages X.509 certificates for signing SAML responses.
134
141
 
135
- ### Generating a new certificate
142
+ ### Building a certificate
136
143
  ```ruby
137
144
  # With defaults
138
- cert = Lyrebird::Certificate.generate
145
+ cert = Lyrebird::Certificate.build
139
146
 
140
147
  # 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
- )
148
+ cert = Lyrebird::Certificate.build do |c|
149
+ c.bits = 4096 # RSA key size (default: 2048)
150
+ c.cn = "example.com" # Common Name
151
+ c.o = "Acme" # Organization
152
+ c.valid_for = 30 # Days (default: 365)
153
+ c.valid_until = Time.new(2999, 12, 31) # Overrides valid_for
154
+ end
148
155
  ```
149
156
 
150
157
  ### Loading an existing certificate
151
158
  ```ruby
152
159
  cert = Lyrebird::Certificate.load(
153
- private_key_pem: File.read("private_key.pem"),
154
- certificate_pem: File.read("certificate.pem")
160
+ key_pem: File.read("private_key.pem"),
161
+ x509_pem: File.read("certificate.pem")
155
162
  )
156
163
  ```
157
164
 
158
- ### Exporting
165
+ ### Using a certificate
159
166
  ```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
167
+ cert.key # OpenSSL::PKey::RSA private key
168
+ cert.x509 # OpenSSL::X509::Certificate object
169
+ cert.key_pem # PEM-encoded private key
170
+ cert.x509_pem # PEM-encoded certificate
171
+ cert.sign(data) # Sign data with RSA-SHA256
172
+ cert.base64 # Base64-encoded certificate (for SAML metadata)
173
+ cert.fingerprint # SHA256 fingerprint
166
174
  ```
@@ -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
+ 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,71 @@
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)
50
+ end
51
+
52
+ def sign(data)
53
+ @key.sign("SHA256", data)
47
54
  end
48
55
 
49
56
  private
50
57
 
51
- def build_certificate
52
- now = Time.now
58
+ def build_x509
59
+ now = Time.now.utc
53
60
 
54
61
  OpenSSL::X509::Certificate.new.tap do |c|
55
- c.public_key = @private_key.public_key
62
+ c.version = 2
63
+ c.serial = OpenSSL::BN.rand(64)
64
+ c.public_key = @key.public_key
56
65
  c.subject = build_subject
57
66
  c.issuer = c.subject
58
67
  c.not_before = now
59
68
  c.not_after = @valid_until || now + (@valid_for * 86_400)
60
- c.sign(@private_key, OpenSSL::Digest::SHA256.new)
69
+ c.sign(@key, OpenSSL::Digest::SHA256.new)
61
70
  end
62
71
  end
63
72
 
@@ -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
+ 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