saml-kit 1.0.6 → 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +5 -5
  3. data/.rubocop.yml +92 -0
  4. data/.rubocop_todo.yml +45 -0
  5. data/.travis.yml +7 -3
  6. data/Gemfile +2 -2
  7. data/Rakefile +5 -3
  8. data/bin/cibuild +23 -0
  9. data/bin/console +3 -3
  10. data/bin/lint +13 -0
  11. data/bin/setup +1 -1
  12. data/bin/test +19 -0
  13. data/exe/saml-kit-create-self-signed-certificate +6 -6
  14. data/exe/saml-kit-decode-http-redirect +6 -2
  15. data/lib/saml/kit.rb +42 -39
  16. data/lib/saml/kit/assertion.rb +67 -25
  17. data/lib/saml/kit/authentication_request.rb +1 -1
  18. data/lib/saml/kit/bindings.rb +8 -8
  19. data/lib/saml/kit/bindings/binding.rb +5 -5
  20. data/lib/saml/kit/bindings/http_redirect.rb +12 -7
  21. data/lib/saml/kit/bindings/url_builder.rb +2 -2
  22. data/lib/saml/kit/buildable.rb +3 -3
  23. data/lib/saml/kit/builders/assertion.rb +4 -0
  24. data/lib/saml/kit/builders/authentication_request.rb +3 -3
  25. data/lib/saml/kit/builders/logout_request.rb +1 -1
  26. data/lib/saml/kit/builders/logout_response.rb +1 -1
  27. data/lib/saml/kit/builders/response.rb +2 -8
  28. data/lib/saml/kit/builders/templates/assertion.builder +1 -1
  29. data/lib/saml/kit/builders/templates/metadata.builder +4 -4
  30. data/lib/saml/kit/builders/templates/service_provider_metadata.builder +1 -1
  31. data/lib/saml/kit/composite_metadata.rb +9 -5
  32. data/lib/saml/kit/configuration.rb +7 -7
  33. data/lib/saml/kit/default_registry.rb +1 -1
  34. data/lib/saml/kit/document.rb +39 -23
  35. data/lib/saml/kit/identity_provider_metadata.rb +6 -6
  36. data/lib/saml/kit/invalid_document.rb +2 -2
  37. data/lib/saml/kit/locales/en.yml +12 -3
  38. data/lib/saml/kit/logout_request.rb +1 -1
  39. data/lib/saml/kit/logout_response.rb +1 -1
  40. data/lib/saml/kit/metadata.rb +43 -41
  41. data/lib/saml/kit/namespaces.rb +25 -25
  42. data/lib/saml/kit/null_assertion.rb +17 -0
  43. data/lib/saml/kit/respondable.rb +2 -3
  44. data/lib/saml/kit/response.rb +23 -4
  45. data/lib/saml/kit/rspec/have_query_param.rb +1 -1
  46. data/lib/saml/kit/service_provider_metadata.rb +3 -3
  47. data/lib/saml/kit/signature.rb +74 -4
  48. data/lib/saml/kit/translatable.rb +3 -2
  49. data/lib/saml/kit/trustable.rb +4 -11
  50. data/lib/saml/kit/version.rb +1 -1
  51. data/lib/saml/kit/xml_templatable.rb +10 -5
  52. data/saml-kit.gemspec +25 -22
  53. metadata +54 -6
@@ -25,7 +25,7 @@ module Saml
25
25
  # @param xml [String] the raw xml.
26
26
  # @param configuration [Saml::Kit::Configuration] defaults to the global configuration.
27
27
  def initialize(xml, configuration: Saml::Kit.configuration)
28
- super(xml, name: "AuthnRequest", configuration: configuration)
28
+ super(xml, name: 'AuthnRequest', configuration: configuration)
29
29
  end
30
30
 
31
31
  # Extract the AssertionConsumerServiceURL from the AuthnRequest
@@ -1,19 +1,19 @@
1
- require "saml/kit/bindings/binding"
2
- require "saml/kit/bindings/http_post"
3
- require "saml/kit/bindings/http_redirect"
4
- require "saml/kit/bindings/url_builder"
1
+ require 'saml/kit/bindings/binding'
2
+ require 'saml/kit/bindings/http_post'
3
+ require 'saml/kit/bindings/http_redirect'
4
+ require 'saml/kit/bindings/url_builder'
5
5
 
6
6
  module Saml
7
7
  module Kit
8
8
  module Bindings
9
- HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
10
- HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
11
- HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
9
+ HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'.freeze
10
+ HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'.freeze
11
+ HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'.freeze
12
12
  ALL = {
13
13
  http_post: HTTP_POST,
14
14
  http_redirect: HTTP_REDIRECT,
15
15
  http_artifact: HTTP_ARTIFACT,
16
- }
16
+ }.freeze
17
17
 
18
18
  def self.binding_for(binding)
19
19
  ALL[binding]
@@ -14,12 +14,12 @@ module Saml
14
14
  binding == other
15
15
  end
16
16
 
17
- def serialize(builder, relay_state: nil)
17
+ def serialize(*)
18
18
  []
19
19
  end
20
20
 
21
- def deserialize(params)
22
- raise ArgumentError.new("Unsupported binding")
21
+ def deserialize(_params)
22
+ raise ArgumentError, 'Unsupported binding'
23
23
  end
24
24
 
25
25
  def to_h
@@ -27,7 +27,7 @@ module Saml
27
27
  end
28
28
 
29
29
  def ==(other)
30
- self.to_s == other.to_s
30
+ to_s == other.to_s
31
31
  end
32
32
 
33
33
  def eql?(other)
@@ -58,7 +58,7 @@ module Saml
58
58
  elsif parameters[:SAMLResponse].present?
59
59
  parameters[:SAMLResponse]
60
60
  else
61
- raise ArgumentError.new("SAMLRequest or SAMLResponse parameter is required.")
61
+ raise ArgumentError, 'SAMLRequest or SAMLResponse parameter is required.'
62
62
  end
63
63
  end
64
64
  end
@@ -17,7 +17,7 @@ module Saml
17
17
  end
18
18
 
19
19
  def deserialize(params, configuration: Saml::Kit.configuration)
20
- parameters = normalize(params)
20
+ parameters = normalize(params_to_hash(params))
21
21
  document = deserialize_document_from!(parameters, configuration)
22
22
  ensure_valid_signature!(parameters, document)
23
23
  document
@@ -35,25 +35,25 @@ module Saml
35
35
  return if document.provider.nil?
36
36
 
37
37
  if document.provider.verify(
38
- algorithm_for(params[:SigAlg]),
39
- decode(params[:Signature]),
40
- canonicalize(params)
38
+ algorithm_for(params[:SigAlg]),
39
+ decode(params[:Signature]),
40
+ canonicalize(params)
41
41
  )
42
42
  document.signature_verified!
43
43
  else
44
- raise ArgumentError.new("Invalid Signature")
44
+ raise ArgumentError, 'Invalid Signature'
45
45
  end
46
46
  end
47
47
 
48
48
  def canonicalize(params)
49
- [:SAMLRequest, :SAMLResponse, :RelayState, :SigAlg].map do |key|
49
+ %i[SAMLRequest SAMLResponse RelayState SigAlg].map do |key|
50
50
  value = params[key]
51
51
  value.present? ? "#{key}=#{value}" : nil
52
52
  end.compact.join('&')
53
53
  end
54
54
 
55
55
  def algorithm_for(algorithm)
56
- case algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
56
+ case algorithm =~ /(rsa-)?sha(.*?)$/i && Regexp.last_match(2).to_i
57
57
  when 256
58
58
  OpenSSL::Digest::SHA256.new
59
59
  when 384
@@ -74,6 +74,11 @@ module Saml
74
74
  SigAlg: params['SigAlg'] || params[:SigAlg],
75
75
  }
76
76
  end
77
+
78
+ def params_to_hash(value)
79
+ return value unless value.is_a?(String)
80
+ Hash[URI.parse(value).query.split('&').map { |x| x.split('=', 2) }]
81
+ end
77
82
  end
78
83
  end
79
84
  end
@@ -17,7 +17,7 @@ module Saml
17
17
  else
18
18
  payload = to_query_string(
19
19
  saml_document.query_string_parameter => serialize(saml_document.to_xml),
20
- 'RelayState' => relay_state,
20
+ 'RelayState' => relay_state
21
21
  )
22
22
  "#{saml_document.destination}?#{payload}"
23
23
  end
@@ -34,7 +34,7 @@ module Saml
34
34
  to_query_string(
35
35
  saml_document.query_string_parameter => serialize(saml_document.to_xml),
36
36
  'RelayState' => relay_state,
37
- 'SigAlg' => ::Xml::Kit::Namespaces::SHA256,
37
+ 'SigAlg' => ::Xml::Kit::Namespaces::SHA256
38
38
  )
39
39
  end
40
40
 
@@ -4,19 +4,19 @@ module Saml
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  class_methods do
7
- def build(*args) # :yields builder
7
+ def build(*args)
8
8
  builder(*args) do |builder|
9
9
  yield builder if block_given?
10
10
  end.build
11
11
  end
12
12
 
13
- def build_xml(*args) # :yields builder
13
+ def build_xml(*args)
14
14
  builder(*args) do |builder|
15
15
  yield builder if block_given?
16
16
  end.to_xml
17
17
  end
18
18
 
19
- def builder(*args) # :yields builder
19
+ def builder(*args)
20
20
  builder_class.new(*args).tap do |builder|
21
21
  yield builder if block_given?
22
22
  end
@@ -25,6 +25,10 @@ module Saml
25
25
  user.assertion_attributes_for(request)
26
26
  end
27
27
 
28
+ def signing_key_pair
29
+ super || @response_builder.signing_key_pair
30
+ end
31
+
28
32
  private
29
33
 
30
34
  def assertion_options
@@ -15,7 +15,7 @@ module Saml
15
15
  @issuer = configuration.entity_id
16
16
  @name_id_format = Namespaces::PERSISTENT
17
17
  @now = Time.now.utc
18
- @version = "2.0"
18
+ @version = '2.0'
19
19
  end
20
20
 
21
21
  def build
@@ -26,8 +26,8 @@ module Saml
26
26
 
27
27
  def request_options
28
28
  options = {
29
- "xmlns:samlp" => Namespaces::PROTOCOL,
30
- "xmlns:saml" => Namespaces::ASSERTION,
29
+ 'xmlns:samlp' => Namespaces::PROTOCOL,
30
+ 'xmlns:saml' => Namespaces::ASSERTION,
31
31
  ID: id,
32
32
  Version: version,
33
33
  IssueInstant: now.utc.iso8601,
@@ -16,7 +16,7 @@ module Saml
16
16
  @issuer = configuration.entity_id
17
17
  @name_id_format = Saml::Kit::Namespaces::PERSISTENT
18
18
  @now = Time.now.utc
19
- @version = "2.0"
19
+ @version = '2.0'
20
20
  end
21
21
 
22
22
  def build
@@ -16,7 +16,7 @@ module Saml
16
16
  @now = Time.now.utc
17
17
  @request = request
18
18
  @status_code = Namespaces::SUCCESS
19
- @version = "2.0"
19
+ @version = '2.0'
20
20
  end
21
21
 
22
22
  def build
@@ -17,9 +17,10 @@ module Saml
17
17
  @id = ::Xml::Kit::Id.generate
18
18
  @reference_id = ::Xml::Kit::Id.generate
19
19
  @now = Time.now.utc
20
- @version = "2.0"
20
+ @version = '2.0'
21
21
  @status_code = Namespaces::SUCCESS
22
22
  @issuer = configuration.entity_id
23
+ @encryption_certificate = request.try(:provider).try(:encryption_certificates).try(:last)
23
24
  @encrypt = encryption_certificate.present?
24
25
  @configuration = configuration
25
26
  end
@@ -28,13 +29,6 @@ module Saml
28
29
  Saml::Kit::Response.new(to_xml, request_id: request.id, configuration: configuration)
29
30
  end
30
31
 
31
- def encryption_certificate
32
- request.provider.encryption_certificates.first
33
- rescue => error
34
- Saml::Kit.logger.error(error)
35
- nil
36
- end
37
-
38
32
  def assertion
39
33
  @assertion ||=
40
34
  begin
@@ -4,7 +4,7 @@ xml.Assertion(assertion_options) do
4
4
  xml.Subject do
5
5
  xml.NameID name_id, Format: name_id_format
6
6
  xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
7
- xml.SubjectConfirmationData "", subject_confirmation_data_options
7
+ xml.SubjectConfirmationData '', subject_confirmation_data_options
8
8
  end
9
9
  end
10
10
  xml.Conditions conditions_options do
@@ -4,11 +4,11 @@ xml.EntityDescriptor entity_descriptor_options do
4
4
  render identity_provider, xml: xml
5
5
  render service_provider, xml: xml
6
6
  xml.Organization do
7
- xml.OrganizationName organization_name, 'xml:lang': "en"
8
- xml.OrganizationDisplayName organization_name, 'xml:lang': "en"
9
- xml.OrganizationURL organization_url, 'xml:lang': "en"
7
+ xml.OrganizationName organization_name, 'xml:lang': 'en'
8
+ xml.OrganizationDisplayName organization_name, 'xml:lang': 'en'
9
+ xml.OrganizationURL organization_url, 'xml:lang': 'en'
10
10
  end
11
- xml.ContactPerson contactType: "technical" do
11
+ xml.ContactPerson contactType: 'technical' do
12
12
  xml.Company "mailto:#{contact_email}"
13
13
  end
14
14
  end
@@ -12,6 +12,6 @@ xml.SPSSODescriptor descriptor_options do
12
12
  xml.NameIDFormat format
13
13
  end
14
14
  acs_urls.each_with_index do |item, index|
15
- xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index == 0 ? true : false
15
+ xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index.zero?
16
16
  end
17
17
  end
@@ -5,7 +5,7 @@ module Saml
5
5
  attr_reader :service_provider, :identity_provider
6
6
 
7
7
  def initialize(xml)
8
- super("IDPSSODescriptor", xml)
8
+ super('IDPSSODescriptor', xml)
9
9
  @metadatum = [
10
10
  Saml::Kit::ServiceProviderMetadata.new(xml),
11
11
  Saml::Kit::IdentityProviderMetadata.new(xml),
@@ -13,10 +13,10 @@ module Saml
13
13
  end
14
14
 
15
15
  def services(type)
16
- xpath = map { |x| "//md:EntityDescriptor/md:#{x.name}/md:#{type}" }.join("|")
16
+ xpath = map { |x| "//md:EntityDescriptor/md:#{x.name}/md:#{type}" }.join('|')
17
17
  document.find_all(xpath).map do |item|
18
- binding = item.attribute("Binding").value
19
- location = item.attribute("Location").value
18
+ binding = item.attribute('Binding').value
19
+ location = item.attribute('Location').value
20
20
  Saml::Kit::Bindings.create_for(binding, location)
21
21
  end
22
22
  end
@@ -30,12 +30,16 @@ module Saml
30
30
  end
31
31
 
32
32
  def method_missing(name, *args)
33
- if target = find { |x| x.respond_to?(name) }
33
+ if (target = find { |x| x.respond_to?(name) })
34
34
  target.public_send(name, *args)
35
35
  else
36
36
  super
37
37
  end
38
38
  end
39
+
40
+ def respond_to_missing?(method, *)
41
+ find { |x| x.respond_to?(method) }
42
+ end
39
43
  end
40
44
  end
41
45
  end
@@ -20,7 +20,7 @@ module Saml
20
20
  # configuration.add_key_pair(ENV["X509_CERTIFICATE"], ENV["PRIVATE_KEY"], passphrase: ENV['PRIVATE_KEY_PASSPHRASE'], use: :encryption)
21
21
  # end
22
22
  class Configuration
23
- USES = [:signing, :encryption]
23
+ USES = %i[signing encryption].freeze
24
24
  # The issuer to use in requests or responses from this entity to use.
25
25
  attr_accessor :entity_id
26
26
  # The signature method to use when generating signatures (See {Saml::Kit::Builders::XmlSignature::SIGNATURE_METHODS})
@@ -36,7 +36,7 @@ module Saml
36
36
  # The total allowable clock drift for session timeout validation.
37
37
  attr_accessor :clock_drift
38
38
 
39
- def initialize # :yields configuration
39
+ def initialize
40
40
  @clock_drift = 30.seconds
41
41
  @digest_method = :SHA256
42
42
  @key_pairs = []
@@ -85,7 +85,7 @@ module Saml
85
85
  # Return each private for a specific use.
86
86
  #
87
87
  # @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
88
- def private_keys(use: :signing)
88
+ def private_keys(use: nil)
89
89
  key_pairs(use: use).flat_map(&:private_key)
90
90
  end
91
91
 
@@ -97,10 +97,10 @@ module Saml
97
97
  private
98
98
 
99
99
  def ensure_proper_use!(use)
100
- unless USES.include?(use)
101
- error_message = "Use must be either :signing or :encryption"
102
- raise ArgumentError.new(error_message)
103
- end
100
+ return if USES.include?(use)
101
+
102
+ error_message = 'Use must be either :signing or :encryption'
103
+ raise ArgumentError, error_message
104
104
  end
105
105
  end
106
106
  end
@@ -62,7 +62,7 @@ module Saml
62
62
 
63
63
  # Yields each registered [Saml::Kit::Metadata] to the block.
64
64
  def each
65
- @items.each do |key, value|
65
+ @items.each_value do |value|
66
66
  yield value
67
67
  end
68
68
  end
@@ -1,19 +1,20 @@
1
1
  module Saml
2
2
  module Kit
3
3
  class Document
4
- PROTOCOL_XSD = File.expand_path("./xsd/saml-schema-protocol-2.0.xsd", File.dirname(__FILE__)).freeze
4
+ include ActiveModel::Validations
5
+ include XsdValidatable
6
+ include Translatable
7
+ include Trustable
8
+ include Buildable
9
+ PROTOCOL_XSD = File.expand_path('./xsd/saml-schema-protocol-2.0.xsd', File.dirname(__FILE__)).freeze
5
10
  NAMESPACES = {
6
11
  "NameFormat": ::Saml::Kit::Namespaces::ATTR_SPLAT,
7
12
  "ds": ::Xml::Kit::Namespaces::XMLDSIG,
8
13
  "md": ::Saml::Kit::Namespaces::METADATA,
9
14
  "saml": ::Saml::Kit::Namespaces::ASSERTION,
10
15
  "samlp": ::Saml::Kit::Namespaces::PROTOCOL,
16
+ 'xmlenc' => ::Xml::Kit::Namespaces::XMLENC,
11
17
  }.freeze
12
- include ActiveModel::Validations
13
- include XsdValidatable
14
- include Translatable
15
- include Trustable
16
- include Buildable
17
18
  validates_presence_of :content
18
19
  validates_presence_of :id
19
20
  validate :must_match_xsd
@@ -60,13 +61,28 @@ module Saml
60
61
  #
61
62
  # @param pretty [Boolean] formats the xml or returns the raw xml.
62
63
  def to_xml(pretty: false)
63
- pretty ? Nokogiri::XML(content).to_xml(indent: 2) : content
64
+ pretty ? to_nokogiri.to_xml(indent: 2) : content
64
65
  end
65
66
 
66
- # Returns the SAML document as an XHTML string.
67
+ # Returns the SAML document as an XHTML string.
67
68
  # This is useful for rendering in a web page.
68
69
  def to_xhtml
69
- Nokogiri::XML(content, &:noblanks).to_xhtml
70
+ Nokogiri::XML(to_xml, &:noblanks).to_xhtml
71
+ end
72
+
73
+ # @!visibility private
74
+ def to_nokogiri
75
+ @nokogiri ||= Nokogiri::XML(content)
76
+ end
77
+
78
+ # @!visibility private
79
+ def at_xpath(xpath)
80
+ to_nokogiri.at_xpath(xpath, NAMESPACES)
81
+ end
82
+
83
+ # @!visibility private
84
+ def search(xpath)
85
+ to_nokogiri.search(xpath, NAMESPACES)
70
86
  end
71
87
 
72
88
  def to_s
@@ -75,11 +91,11 @@ module Saml
75
91
 
76
92
  class << self
77
93
  XPATH = [
78
- "/samlp:AuthnRequest",
79
- "/samlp:LogoutRequest",
80
- "/samlp:LogoutResponse",
81
- "/samlp:Response",
82
- ].join("|")
94
+ '/samlp:AuthnRequest',
95
+ '/samlp:LogoutRequest',
96
+ '/samlp:LogoutResponse',
97
+ '/samlp:Response',
98
+ ].join('|')
83
99
 
84
100
  # Returns the raw xml as a Saml::Kit SAML document.
85
101
  #
@@ -87,16 +103,16 @@ module Saml
87
103
  # @param configuration [Saml::Kit::Configuration] the configuration to use for unpacking the document.
88
104
  def to_saml_document(xml, configuration: Saml::Kit.configuration)
89
105
  xml_document = ::Xml::Kit::Document.new(xml, namespaces: {
90
- "samlp": ::Saml::Kit::Namespaces::PROTOCOL
91
- })
106
+ "samlp": ::Saml::Kit::Namespaces::PROTOCOL
107
+ })
92
108
  constructor = {
93
- "AuthnRequest" => Saml::Kit::AuthenticationRequest,
94
- "LogoutRequest" => Saml::Kit::LogoutRequest,
95
- "LogoutResponse" => Saml::Kit::LogoutResponse,
96
- "Response" => Saml::Kit::Response,
109
+ 'AuthnRequest' => Saml::Kit::AuthenticationRequest,
110
+ 'LogoutRequest' => Saml::Kit::LogoutRequest,
111
+ 'LogoutResponse' => Saml::Kit::LogoutResponse,
112
+ 'Response' => Saml::Kit::Response,
97
113
  }[xml_document.find_by(XPATH).name] || InvalidDocument
98
114
  constructor.new(xml, configuration: configuration)
99
- rescue => error
115
+ rescue StandardError => error
100
116
  Saml::Kit.logger.error(error)
101
117
  InvalidDocument.new(xml, configuration: configuration)
102
118
  end
@@ -113,7 +129,7 @@ module Saml
113
129
  when Saml::Kit::LogoutRequest.to_s
114
130
  Saml::Kit::Builders::LogoutRequest
115
131
  else
116
- raise ArgumentError.new("Unknown SAML Document #{name}")
132
+ raise ArgumentError, "Unknown SAML Document #{name}"
117
133
  end
118
134
  end
119
135
  end
@@ -140,7 +156,7 @@ module Saml
140
156
 
141
157
  def must_be_valid_version
142
158
  return unless expected_type?
143
- return if "2.0" == version
159
+ return if version == '2.0'
144
160
  errors[:version] << error_message(:invalid_version)
145
161
  end
146
162
  end