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.
- 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
|