Module: Samlr::Tools

Defined in:
lib/samlr/tools.rb,
lib/samlr/tools/timestamp.rb,
lib/samlr/tools/request_builder.rb,
lib/samlr/tools/metadata_builder.rb,
lib/samlr/tools/response_builder.rb,
lib/samlr/tools/certificate_builder.rb,
lib/samlr/tools/logout_request_builder.rb,
lib/samlr/tools/logout_response_builder.rb

Defined Under Namespace

Modules: LogoutRequestBuilder, LogoutResponseBuilder, RequestBuilder, ResponseBuilder, Timestamp Classes: CertificateBuilder, MetadataBuilder

Constant Summary collapse

SHA_MAP =
{
  1    => OpenSSL::Digest::SHA1,
  256  => OpenSSL::Digest::SHA256,
  384  => OpenSSL::Digest::SHA384,
  512  => OpenSSL::Digest::SHA512
}

Class Method Summary collapse

Class Method Details

.algorithm(value) ⇒ Object

Convert algorithm attribute value to Ruby implementation


25
26
27
28
29
30
31
# File 'lib/samlr/tools.rb', line 25

def self.algorithm(value)
  if value =~ /sha(\d+)$/
    implementation = SHA_MAP[$1.to_i]
  end

  implementation || OpenSSL::Digest::SHA1
end

.canonicalize(xml, options = {}) ⇒ Object

Accepts a document and optionally :path => xpath, :c14n_mode => c14n_mode


34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/samlr/tools.rb', line 34

def self.canonicalize(xml, options = {})
  options  = { :c14n_mode => C14N }.merge(options)
  document = Nokogiri::XML(xml) { |c| c.strict.noblanks }

  if path = options[:path]
    node = document.at(path, NS_MAP)
  else
    node = document
  end

  node.canonicalize(options[:c14n_mode], options[:namespaces])
end

.decode(string) ⇒ Object

CGI unescapes, Base64 decodes and inflates a string


61
62
63
64
65
66
67
68
69
70
71
# File 'lib/samlr/tools.rb', line 61

def self.decode(string)
  unescaped = CGI.unescape(string)
  decoded   = Base64.decode64(unescaped)
  inflater  = Zlib::Inflate.new(-Zlib::MAX_WBITS)
  inflated  = inflater.inflate(decoded)

  inflater.finish
  inflater.close

  inflated
end

.encode(string) ⇒ Object

Deflates, Base64 encodes and CGI escapes a string


53
54
55
56
57
58
# File 'lib/samlr/tools.rb', line 53

def self.encode(string)
  deflated = Zlib::Deflate.deflate(string, 9)[2..-5]
  encoded  = Base64.encode64(deflated)
  escaped  = CGI.escape(encoded)
  escaped
end

.inflate(data) ⇒ Object


136
137
138
139
140
141
142
143
# File 'lib/samlr/tools.rb', line 136

def self.inflate(data)
  inflater  = Zlib::Inflate.new(-Zlib::MAX_WBITS)
  decoded = inflater.inflate(data)

  inflater.finish
  inflater.close
  decoded
end

.parse(data, compressed: false) ⇒ Object

Tries to parse the SAML request, returns nil if no data passed. First, it assumes it to be Base64 encoded. If this fails, it subsequently attempts to parse the raw input as select IdP's send that rather than a Base64 encoded value


113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/samlr/tools.rb', line 113

def self.parse(data, compressed: false)
  return unless data
  decoded = Base64.decode64(data)
  decoded = self.inflate(decoded) if compressed
  begin
    doc = Nokogiri::XML(decoded) { |config| config.strict }
  rescue Nokogiri::XML::SyntaxError => e
    begin
      doc = Nokogiri::XML(data) { |config| config.strict }
    rescue
      raise Samlr::FormatError.new(e.message)
    end
  end

  begin
    Samlr::Tools.validate!(:document => doc)
  rescue Samlr::SamlrError => e
    Samlr.logger.warn("Accepting non schema conforming response: #{e.message}, #{e.details}")
    raise e unless Samlr.validation_mode == :log
  end
  doc
end

.uuidObject

Generate an xs:NCName conforming UUID


48
49
50
# File 'lib/samlr/tools.rb', line 48

def self.uuid
  "samlr-#{UUIDTools::UUID.timestamp_create}"
end

.validate(options = {}) ⇒ Object

Validate a SAML request or response against an XSD. Supply either :path or :document in the options and a :schema (defaults to SAML validation)


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
# File 'lib/samlr/tools.rb', line 79

def self.validate(options = {})
  document = options[:document] || File.read(options[:path])
  schema   = options.fetch(:schema, SAML_SCHEMA)
  bang     = options.fetch(:bang, false)

  if document.is_a?(Nokogiri::XML::Document)
    xml = document
  else
    xml = Nokogiri::XML(document) { |c| c.strict }
  end

  # All bundled schemas are using relative schemaLocation. This means we'll have to
  # change working directory to find them during validation.
  Dir.chdir(Samlr.schema_location) do
    if schema.is_a?(Nokogiri::XML::Schema)
      xsd = schema
    else
      xsd = Nokogiri::XML::Schema(File.read(schema))
    end

    result = xsd.validate(xml)

    if bang && result.length != 0
      raise Samlr::FormatError.new("Schema validation failed", "XSD validation errors: #{result.join(", ")}")
    else
      result.length == 0
    end
  end
end

.validate!(options = {}) ⇒ Object


73
74
75
# File 'lib/samlr/tools.rb', line 73

def self.validate!(options = {})
  validate(options.merge(:bang => true))
end