saml-kit 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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