Class: SAML2::Response

Inherits:
StatusResponse show all
Defined in:
lib/saml2/response.rb

Instance Attribute Summary collapse

Attributes inherited from StatusResponse

#in_response_to, #status

Attributes inherited from Message

#destination, #errors, #issuer

Attributes inherited from Base

#xml

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Message

from_xml, #id, #issue_instant, parse, #valid_schema?

Methods included from Signable

#signature, #signed?, #signing_key, #valid_signature?, #validate_signature

Methods inherited from Base

#decrypt, from_xml, #inspect, load_object_array, load_string_array, lookup_qname, #to_s, #to_xml

Constructor Details

#initializeResponse

Returns a new instance of Response.


71
72
73
74
# File 'lib/saml2/response.rb', line 71

def initialize
  super
  @assertions = []
end

Instance Attribute Details

#assertionsArray<Assertion> (readonly)

Returns:


252
253
254
# File 'lib/saml2/response.rb', line 252

def assertions
  @assertions
end

Class Method Details

.initiate(service_provider, issuer, name_id, attributes = nil) ⇒ Response

Begin an IdP Initiated login

Parameters:

  • service_provider (ServiceProvider)
  • issuer (NameID)
  • name_id (NameID)

    The subject

  • attributes (defaults to: nil)

    optional [Hash<String => String>, Array<Attribute>]

Returns:


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/saml2/response.rb', line 45

def self.initiate(service_provider, issuer, name_id, attributes = nil)
  response = new
  response.issuer = issuer
  response.destination = service_provider.assertion_consumer_services.default.location if service_provider
  assertion = Assertion.new
  assertion.subject = Subject.new
  assertion.subject.name_id = name_id
  assertion.subject.confirmation = Subject::Confirmation.new
  assertion.subject.confirmation.method = Subject::Confirmation::Methods::BEARER
  assertion.subject.confirmation.not_on_or_after = Time.now.utc + 30
  assertion.subject.confirmation.recipient = response.destination if response.destination
  assertion.issuer = issuer
  assertion.conditions.not_before = Time.now.utc - 5
  assertion.conditions.not_on_or_after = Time.now.utc + 30
  authn_statement = AuthnStatement.new
  authn_statement.authn_instant = response.issue_instant
  authn_statement.authn_context_class_ref = AuthnStatement::Classes::UNSPECIFIED
  assertion.statements << authn_statement
  if attributes && service_provider.attribute_consuming_services.default
    statement = service_provider.attribute_consuming_services.default.create_statement(attributes)
    assertion.statements << statement if statement
  end
  response.assertions << assertion
  response
end

.respond_to(authn_request, issuer, name_id, attributes = nil) ⇒ Response

Respond to an AuthnRequest

AuthnRequest#resolve needs to have been previously called on the AuthnRequest.

Parameters:

  • authn_request (AuthnRequest)
  • issuer (NameID)
  • name_id (NameID)

    The Subject

  • attributes (defaults to: nil)

    optional [Hash<String => String>, Array<Attribute>]

Returns:


23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/saml2/response.rb', line 23

def self.respond_to(authn_request, issuer, name_id, attributes = nil)
  response = initiate(nil, issuer, name_id)
  response.in_response_to = authn_request.id
  response.destination = authn_request.assertion_consumer_service.location
  confirmation = response.assertions.first.subject.confirmation
  confirmation.in_response_to = authn_request.id
  confirmation.recipient = response.destination
  if attributes && authn_request.attribute_consuming_service
    statement = authn_request.attribute_consuming_service.create_statement(attributes)
    response.assertions.first.statements << statement if statement
  end
  response.assertions.first.conditions << Conditions::AudienceRestriction.new(authn_request.issuer.id)

  response
end

Instance Method Details

#from_xml(node) ⇒ void

This method returns an undefined value.

Parse an XML element into this object.

Parameters:

  • node (Nokogiri::XML::Element)

77
78
79
80
# File 'lib/saml2/response.rb', line 77

def from_xml(node)
  super
  remove_instance_variable(:@assertions)
end

#sign(x509_certificate, private_key, algorithm_name = :sha256) ⇒ self

Sign this object.

Signs each assertion.

Parameters:

  • x509_certificate (String)

    The certificate corresponding to private_key, to be embedded in the signature.

  • private_key (String)

    The key to use to sign.

  • algorithm_name (Symbol) (defaults to: :sha256)

Returns:

  • (self)

261
262
263
264
265
266
267
268
269
270
# File 'lib/saml2/response.rb', line 261

def sign(x509_certificate, private_key, algorithm_name = :sha256)
  # make sure we no longer pretty print this object
  @pretty = false

  # if there are no assertions (encrypted?), just sign the response itself
  return super if assertions.empty?

  assertions.each { |assertion| assertion.sign(x509_certificate, private_key, algorithm_name) }
  self
end

#validate(service_provider:, identity_provider:, verification_time: nil, ignore_audience_condition: false) ⇒ Object

Validates a response is well-formed, signed, and optionally decrypts it

Parameters:

  • service_provider (Entity)

    The metadata object for the ServiceProvider receiving this SAML2::Response. The first ServiceProvider in the Entity is used.

  • identity_provider (Entity)

    The metadata object for the IdentityProvider the SAML2::Response is being received from. The first IdentityProvider in the Entity is used.

  • verification_time (defaults to: nil)

    optional [DateTime] Validate timestamps (signing certificate validity, issued at, etc.) as of this point in time.

  • ignore_audience_condition (defaults to: false)

    optional [true, false] Don't validate any Audience conditions.

Raises:

  • (ArgumentError)

96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/saml2/response.rb', line 96

def validate(service_provider:,
             identity_provider:,
             verification_time: nil,
             ignore_audience_condition: false)
  raise ArgumentError, "service_provider should be an Entity object" unless service_provider.is_a?(Entity)
  raise ArgumentError, "service_provider should have at least one service_provider role" unless (sp = service_provider.service_providers.first)

  # validate the schema
  super()
  return errors unless errors.empty?

  if verification_time.nil?
    verification_time = Time.now.utc    # they issued it in the (near) future according to our clock;
    # use their clock instead

    verification_time = issue_instant if issue_instant > verification_time && issue_instant < verification_time + 5 * 60
  end

  # not finding the issuer is not exceptional
  if identity_provider.nil?
    errors << "could not find issuer of response"
    return errors
  end

  # getting the wrong data type is exceptional, and we should raise an error
  raise ArgumentError, "identity_provider should be an Entity object" unless identity_provider.is_a?(Entity)
  raise ArgumentError, "identity_provider should have at least one identity_provider role" unless (idp = identity_provider.identity_providers.first)

  issuer = self.issuer || assertions.first&.issuer
  unless identity_provider.entity_id == issuer&.id
    errors << "received unexpected message from '#{issuer&.id}'; expected it to be from '#{identity_provider.entity_id}'"
    return errors
  end

  certificates = idp.signing_keys.map(&:certificate)
  if idp.fingerprints.empty? && certificates.empty?
    errors << "could not find certificate to validate message"
    return errors
  end

  if signed?
    unless (signature_errors = validate_signature(fingerprint: idp.fingerprints,
                                                  cert: certificates)).empty?
      return errors.concat(signature_errors)
    end
    response_signed = true
  end

  assertion = assertions.first

  # this might be nil, if the assertion was encrypted
  if assertion&.signed?
    unless (signature_errors = assertion.validate_signature(fingerprint: idp.fingerprints,
                                                            cert: certificates)).empty?
      return errors.concat(signature_errors)
    end
    assertion_signed = true
  end

  find_decryption_key = ->(embedded_certificates) do
    key = nil
    embedded_certificates.each do |cert_info|
      cert = case cert_info
               when OpenSSL::X509::Certificate; cert_info
               when Hash; sp.encryption_keys.map(&:certificate).find { |c| c.serial == cert_info[:serial] }
             end
      next unless cert
      key = sp.private_keys.find { |k| cert.check_private_key(k) }
      break if key
    end
    if !key      # couldn't figure out which key to use; just try them all

      next sp.private_keys
    end
    key
  end

  unless sp.private_keys.empty?
    begin
      decypted_anything = decrypt(&find_decryption_key)
    rescue XMLSec::DecryptionError
      errors << "unable to decrypt response"
      return errors
    end

    if decypted_anything      # have to re-validate the schema, since we just replaced content

      super()      # also clear this cached value so that we can see cached assertions

      remove_instance_variable(:@assertions)
      return errors unless errors.empty?
    end
  end

  unless status.success?
    errors << "response is not successful: #{status}"
    return errors
  end

  assertion ||= assertions.first
  unless assertion
    errors << "no assertion found"
    return errors
  end

  # if we didn't previously check the assertion's signature (because it was encrypted)
  # check it now
  if assertion.signed? && !assertion_signed
    unless (signature_errors = assertion.validate_signature(fingerprint: idp.fingerprints,
                                                            cert: certificates)).empty?
      return errors.concat(signature_errors)
    end
    assertion_signed = true
  end

  # only do our own issue instant validation if the assertion
  # doesn't mandate any
  unless assertion.conditions&.not_on_or_after
    if assertion.issue_instant +  5 * 60 < verification_time ||
        assertion.issue_instant - 5 * 60 > verification_time
      errors << "assertion not recently issued"
      return errors
    end
  end

  if assertion.conditions &&
      !(condition_errors = assertion.conditions.validate(verification_time: verification_time,
                                                         audience: service_provider.entity_id,
                                                         ignore_audience_condition: ignore_audience_condition)).empty?
    return errors.concat(condition_errors)
  end

  if !response_signed && !assertion_signed
    errors << "neither response nor assertion were signed"
    return errors
  end

  unless sp.private_keys.empty?
    begin
      decypted_anything = assertion.decrypt(&find_decryption_key)
    rescue XMLSec::DecryptionError
      errors << "unable to decrypt assertion"
      return errors
    end

    if decypted_anything
      super()
      return errors unless errors.empty?
    end
  end

  # no error
  errors
end