main
1# frozen_string_literal: true
2
3module Saml
4 module Kit
5 # This class validates the Assertion
6 # element nested in a Response element
7 # of a SAML document.
8 class Assertion < Document
9 extend Forwardable
10 XPATH = [
11 '/samlp:Response/saml:Assertion',
12 '/samlp:Response/saml:EncryptedAssertion'
13 ].join('|')
14 def_delegators :conditions, :started_at, :expired_at, :audiences
15 def_delegators :attribute_statement, :attributes
16
17 validate :must_be_decryptable
18 validate :must_match_issuer, if: :decryptable?
19 validate :must_be_active_session, if: :decryptable?
20 validate :must_have_valid_signature, if: :decryptable?
21 attr_reader :name, :configuration
22 attr_accessor :occurred_at
23
24 def initialize(
25 node, configuration: Saml::Kit.configuration, private_keys: []
26 )
27 @name = 'Assertion'
28 @to_nokogiri = node.is_a?(String) ? Nokogiri::XML(node).root : node
29 @configuration = configuration
30 @occurred_at = Time.current
31 @cannot_decrypt = false
32 @encrypted = false
33 keys = configuration.private_keys(use: :encryption) + private_keys
34 decrypt(::Xml::Kit::Decryption.new(private_keys: keys.uniq))
35 super(to_s, name: 'Assertion', configuration: configuration)
36 end
37
38 def id
39 at_xpath('./@ID').try(:value)
40 end
41
42 def issuer
43 at_xpath('./saml:Issuer').try(:text)
44 end
45
46 def version
47 at_xpath('./@Version').try(:value)
48 end
49
50 def name_id
51 at_xpath('./saml:Subject/saml:NameID').try(:text)
52 end
53
54 def name_id_format
55 at_xpath('./saml:Subject/saml:NameID').attribute('Format').try(:value)
56 end
57
58 def signed?
59 signature.present?
60 end
61
62 def signature
63 @signature ||= Signature.new(at_xpath('./ds:Signature'))
64 end
65
66 def expired?(now = occurred_at)
67 now > expired_at
68 end
69
70 def active?(now = occurred_at)
71 drifted_started_at = started_at - configuration.clock_drift.to_i.seconds
72 now > drifted_started_at && !expired?(now)
73 end
74
75 def expected_type?
76 at_xpath('../saml:Assertion|../saml:EncryptedAssertion').present?
77 end
78
79 def attribute_statement(xpath = './saml:AttributeStatement')
80 @attribute_statement ||= AttributeStatement.new(search(xpath))
81 end
82
83 def conditions
84 @conditions ||= Conditions.new(search('./saml:Conditions'))
85 end
86
87 def encrypted?
88 @encrypted
89 end
90
91 def decryptable?
92 return true unless encrypted?
93
94 !@cannot_decrypt
95 end
96
97 def to_s
98 @to_nokogiri.to_s
99 end
100
101 private
102
103 def decrypt(decryptor)
104 encrypted_assertion = at_xpath('./xmlenc:EncryptedData')
105 @encrypted = encrypted_assertion.present?
106 return unless @encrypted
107
108 @to_nokogiri = decryptor.decrypt_node(encrypted_assertion)
109 rescue StandardError => error
110 @cannot_decrypt = true
111 Saml::Kit.logger.error(error)
112 end
113
114 def must_match_issuer
115 return if audiences.empty? || audiences.include?(configuration.entity_id)
116
117 errors.add(:audience, error_message(:must_match_issuer))
118 end
119
120 def must_be_active_session
121 return if active?
122
123 errors.add(:base, error_message(:expired))
124 end
125
126 def must_have_valid_signature
127 return if !signed? || signature.valid?
128
129 signature.each_error do |attribute, message|
130 errors.add(attribute, message)
131 end
132 end
133
134 def must_be_decryptable
135 errors.add(:base, error_message(:cannot_decrypt)) unless decryptable?
136 end
137 end
138 end
139end