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 !encrypted? || !@cannot_decrypt
93 end
94
95 def to_s
96 @to_nokogiri.to_s
97 end
98
99 private
100
101 # A nested Assertion is covered by the enclosing Response's policy; a
102 # bare one is the whole document and has nothing else vouching for it.
103 def signature_required_by_type?
104 to_nokogiri == to_nokogiri.document.root
105 end
106
107 def decrypt(decryptor)
108 encrypted_assertion = at_xpath('./xmlenc:EncryptedData')
109 @encrypted = encrypted_assertion.present?
110 return unless @encrypted
111
112 @to_nokogiri = decryptor.decrypt_node(encrypted_assertion)
113 rescue StandardError => error
114 @cannot_decrypt = true
115 Saml::Kit.logger.error(error)
116 end
117
118 def must_match_issuer
119 return if audiences.empty? || audiences.include?(configuration.entity_id)
120
121 errors.add(:audience, error_message(:must_match_issuer))
122 end
123
124 def must_be_active_session
125 return if active?
126
127 errors.add(:base, error_message(:expired))
128 end
129
130 def must_have_valid_signature
131 return if !signed? || signature.valid?
132
133 signature.each_error do |attribute, message|
134 errors.add(attribute, message)
135 end
136 end
137
138 def must_be_decryptable
139 errors.add(:base, error_message(:cannot_decrypt)) unless decryptable?
140 end
141 end
142 end
143end