summaryrefslogtreecommitdiff
path: root/lib/saml/kit/assertion.rb
blob: 6099fb02dc1af6da662c5bb9c20eb3e43d31930a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
# frozen_string_literal: true

module Saml
  module Kit
    # This class validates the Assertion
    # element nested in a Response element
    # of a SAML document.
    class Assertion < Document
      extend Forwardable
      XPATH = [
        '/samlp:Response/saml:Assertion',
        '/samlp:Response/saml:EncryptedAssertion'
      ].join('|')
      def_delegators :conditions, :started_at, :expired_at, :audiences
      def_delegators :attribute_statement, :attributes

      validate :must_be_decryptable
      validate :must_match_issuer, if: :decryptable?
      validate :must_be_active_session, if: :decryptable?
      validate :must_have_valid_signature, if: :decryptable?
      attr_reader :name, :configuration
      attr_accessor :occurred_at

      def initialize(
        node, configuration: Saml::Kit.configuration, private_keys: []
      )
        @name = 'Assertion'
        @to_nokogiri = node.is_a?(String) ? Nokogiri::XML(node).root : node
        @configuration = configuration
        @occurred_at = Time.current
        @cannot_decrypt = false
        @encrypted = false
        keys = configuration.private_keys(use: :encryption) + private_keys
        decrypt(::Xml::Kit::Decryption.new(private_keys: keys.uniq))
        super(to_s, name: 'Assertion', configuration: configuration)
      end

      def id
        at_xpath('./@ID').try(:value)
      end

      def issuer
        at_xpath('./saml:Issuer').try(:text)
      end

      def version
        at_xpath('./@Version').try(:value)
      end

      def name_id
        at_xpath('./saml:Subject/saml:NameID').try(:text)
      end

      def name_id_format
        at_xpath('./saml:Subject/saml:NameID').attribute('Format').try(:value)
      end

      def signed?
        signature.present?
      end

      def signature
        @signature ||= Signature.new(at_xpath('./ds:Signature'))
      end

      def expired?(now = occurred_at)
        now > expired_at
      end

      def active?(now = occurred_at)
        drifted_started_at = started_at - configuration.clock_drift.to_i.seconds
        now > drifted_started_at && !expired?(now)
      end

      def expected_type?
        at_xpath('../saml:Assertion|../saml:EncryptedAssertion').present?
      end

      def attribute_statement(xpath = './saml:AttributeStatement')
        @attribute_statement ||= AttributeStatement.new(search(xpath))
      end

      def conditions
        @conditions ||= Conditions.new(search('./saml:Conditions'))
      end

      def encrypted?
        @encrypted
      end

      def decryptable?
        return true unless encrypted?

        !@cannot_decrypt
      end

      def to_s
        @to_nokogiri.to_s
      end

      private

      def decrypt(decryptor)
        encrypted_assertion = at_xpath('./xmlenc:EncryptedData')
        @encrypted = encrypted_assertion.present?
        return unless @encrypted

        @to_nokogiri = decryptor.decrypt_node(encrypted_assertion)
      rescue StandardError => error
        @cannot_decrypt = true
        Saml::Kit.logger.error(error)
      end

      def must_match_issuer
        return if audiences.empty? || audiences.include?(configuration.entity_id)

        errors.add(:audience, error_message(:must_match_issuer))
      end

      def must_be_active_session
        return if active?

        errors.add(:base, error_message(:expired))
      end

      def must_have_valid_signature
        return if !signed? || signature.valid?

        signature.errors.each do |attribute, message|
          errors.add(attribute, message)
        end
      end

      def must_be_decryptable
        errors.add(:base, error_message(:cannot_decrypt)) unless decryptable?
      end
    end
  end
end