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.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +5 -5
- data/.rubocop.yml +92 -0
- data/.rubocop_todo.yml +45 -0
- data/.travis.yml +7 -3
- data/Gemfile +2 -2
- data/Rakefile +5 -3
- data/bin/cibuild +23 -0
- data/bin/console +3 -3
- data/bin/lint +13 -0
- data/bin/setup +1 -1
- data/bin/test +19 -0
- data/exe/saml-kit-create-self-signed-certificate +6 -6
- data/exe/saml-kit-decode-http-redirect +6 -2
- data/lib/saml/kit.rb +42 -39
- data/lib/saml/kit/assertion.rb +67 -25
- data/lib/saml/kit/authentication_request.rb +1 -1
- data/lib/saml/kit/bindings.rb +8 -8
- data/lib/saml/kit/bindings/binding.rb +5 -5
- data/lib/saml/kit/bindings/http_redirect.rb +12 -7
- data/lib/saml/kit/bindings/url_builder.rb +2 -2
- data/lib/saml/kit/buildable.rb +3 -3
- data/lib/saml/kit/builders/assertion.rb +4 -0
- data/lib/saml/kit/builders/authentication_request.rb +3 -3
- data/lib/saml/kit/builders/logout_request.rb +1 -1
- data/lib/saml/kit/builders/logout_response.rb +1 -1
- data/lib/saml/kit/builders/response.rb +2 -8
- data/lib/saml/kit/builders/templates/assertion.builder +1 -1
- data/lib/saml/kit/builders/templates/metadata.builder +4 -4
- data/lib/saml/kit/builders/templates/service_provider_metadata.builder +1 -1
- data/lib/saml/kit/composite_metadata.rb +9 -5
- data/lib/saml/kit/configuration.rb +7 -7
- data/lib/saml/kit/default_registry.rb +1 -1
- data/lib/saml/kit/document.rb +39 -23
- data/lib/saml/kit/identity_provider_metadata.rb +6 -6
- data/lib/saml/kit/invalid_document.rb +2 -2
- data/lib/saml/kit/locales/en.yml +12 -3
- data/lib/saml/kit/logout_request.rb +1 -1
- data/lib/saml/kit/logout_response.rb +1 -1
- data/lib/saml/kit/metadata.rb +43 -41
- data/lib/saml/kit/namespaces.rb +25 -25
- data/lib/saml/kit/null_assertion.rb +17 -0
- data/lib/saml/kit/respondable.rb +2 -3
- data/lib/saml/kit/response.rb +23 -4
- data/lib/saml/kit/rspec/have_query_param.rb +1 -1
- data/lib/saml/kit/service_provider_metadata.rb +3 -3
- data/lib/saml/kit/signature.rb +74 -4
- data/lib/saml/kit/translatable.rb +3 -2
- data/lib/saml/kit/trustable.rb +4 -11
- data/lib/saml/kit/version.rb +1 -1
- data/lib/saml/kit/xml_templatable.rb +10 -5
- data/saml-kit.gemspec +25 -22
- 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:
|
28
|
+
super(xml, name: 'AuthnRequest', configuration: configuration)
|
29
29
|
end
|
30
30
|
|
31
31
|
# Extract the AssertionConsumerServiceURL from the AuthnRequest
|
data/lib/saml/kit/bindings.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
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 =
|
11
|
-
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(
|
17
|
+
def serialize(*)
|
18
18
|
[]
|
19
19
|
end
|
20
20
|
|
21
|
-
def deserialize(
|
22
|
-
raise ArgumentError
|
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
|
-
|
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
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
44
|
+
raise ArgumentError, 'Invalid Signature'
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
48
|
def canonicalize(params)
|
49
|
-
[
|
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 &&
|
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
|
|
data/lib/saml/kit/buildable.rb
CHANGED
@@ -4,19 +4,19 @@ module Saml
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
class_methods do
|
7
|
-
def build(*args)
|
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)
|
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)
|
19
|
+
def builder(*args)
|
20
20
|
builder_class.new(*args).tap do |builder|
|
21
21
|
yield builder if block_given?
|
22
22
|
end
|
@@ -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 =
|
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
|
-
|
30
|
-
|
29
|
+
'xmlns:samlp' => Namespaces::PROTOCOL,
|
30
|
+
'xmlns:saml' => Namespaces::ASSERTION,
|
31
31
|
ID: id,
|
32
32
|
Version: version,
|
33
33
|
IssueInstant: now.utc.iso8601,
|
@@ -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 =
|
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
|
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':
|
8
|
-
xml.OrganizationDisplayName organization_name, 'xml:lang':
|
9
|
-
xml.OrganizationURL organization_url, 'xml:lang':
|
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:
|
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
|
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(
|
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(
|
19
|
-
location = item.attribute(
|
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 = [
|
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
|
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:
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
data/lib/saml/kit/document.rb
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
module Saml
|
2
2
|
module Kit
|
3
3
|
class Document
|
4
|
-
|
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 ?
|
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(
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
91
|
-
|
106
|
+
"samlp": ::Saml::Kit::Namespaces::PROTOCOL
|
107
|
+
})
|
92
108
|
constructor = {
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
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
|
159
|
+
return if version == '2.0'
|
144
160
|
errors[:version] << error_message(:invalid_version)
|
145
161
|
end
|
146
162
|
end
|