saml2 2.0.2 → 2.1.0
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/lib/saml2.rb +2 -0
- data/lib/saml2/assertion.rb +6 -0
- data/lib/saml2/attribute.rb +45 -13
- data/lib/saml2/attribute/x500.rb +32 -19
- data/lib/saml2/attribute_consuming_service.rb +52 -4
- data/lib/saml2/authn_request.rb +39 -3
- data/lib/saml2/authn_statement.rb +23 -11
- data/lib/saml2/base.rb +36 -0
- data/lib/saml2/bindings.rb +3 -1
- data/lib/saml2/bindings/http_post.rb +17 -1
- data/lib/saml2/bindings/http_redirect.rb +54 -9
- data/lib/saml2/conditions.rb +43 -16
- data/lib/saml2/contact.rb +17 -6
- data/lib/saml2/endpoint.rb +13 -0
- data/lib/saml2/engine.rb +2 -0
- data/lib/saml2/entity.rb +20 -0
- data/lib/saml2/identity_provider.rb +11 -1
- data/lib/saml2/indexed_object.rb +13 -3
- data/lib/saml2/key.rb +89 -32
- data/lib/saml2/localized_name.rb +8 -0
- data/lib/saml2/logout_request.rb +12 -3
- data/lib/saml2/logout_response.rb +9 -0
- data/lib/saml2/message.rb +38 -7
- data/lib/saml2/name_id.rb +42 -16
- data/lib/saml2/namespaces.rb +10 -8
- data/lib/saml2/organization.rb +5 -0
- data/lib/saml2/organization_and_contacts.rb +5 -0
- data/lib/saml2/request.rb +3 -0
- data/lib/saml2/requested_authn_context.rb +7 -1
- data/lib/saml2/response.rb +20 -2
- data/lib/saml2/role.rb +12 -2
- data/lib/saml2/schemas.rb +2 -0
- data/lib/saml2/service_provider.rb +6 -0
- data/lib/saml2/signable.rb +32 -2
- data/lib/saml2/sso.rb +7 -0
- data/lib/saml2/status.rb +8 -1
- data/lib/saml2/status_response.rb +7 -1
- data/lib/saml2/subject.rb +22 -5
- data/lib/saml2/version.rb +3 -1
- data/spec/lib/bindings/http_redirect_spec.rb +23 -2
- data/spec/lib/conditions_spec.rb +10 -11
- data/spec/lib/identity_provider_spec.rb +1 -1
- data/spec/lib/service_provider_spec.rb +7 -2
- metadata +5 -5
data/lib/saml2/endpoint.rb
CHANGED
@@ -1,23 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'saml2/bindings/http_post'
|
2
4
|
|
3
5
|
module SAML2
|
4
6
|
class Endpoint < Base
|
7
|
+
# @return [String]
|
5
8
|
attr_reader :location, :binding
|
6
9
|
|
10
|
+
# @param location [String]
|
11
|
+
# @param binding [String]
|
7
12
|
def initialize(location = nil, binding = Bindings::HTTP_POST::URN)
|
8
13
|
@location, @binding = location, binding
|
9
14
|
end
|
10
15
|
|
16
|
+
# @param rhs [Endpoint]
|
17
|
+
# @return [Boolean]
|
11
18
|
def ==(rhs)
|
12
19
|
location == rhs.location && binding == rhs.binding
|
13
20
|
end
|
14
21
|
|
22
|
+
# (see Base#from_xml)
|
15
23
|
def from_xml(node)
|
16
24
|
super
|
17
25
|
@location = node['Location']
|
18
26
|
@binding = node['Binding']
|
19
27
|
end
|
20
28
|
|
29
|
+
# (see Base#build)
|
21
30
|
def build(builder, element)
|
22
31
|
builder['md'].__send__(element, 'Location' => location, 'Binding' => binding)
|
23
32
|
end
|
@@ -25,6 +34,10 @@ module SAML2
|
|
25
34
|
class Indexed < Endpoint
|
26
35
|
include IndexedObject
|
27
36
|
|
37
|
+
# @param location [String]
|
38
|
+
# @param index [Integer]
|
39
|
+
# @param is_default [true, false, nil]
|
40
|
+
# @param binding [String]
|
28
41
|
def initialize(location = nil, index = nil, is_default = nil, binding = Bindings::HTTP_POST::URN)
|
29
42
|
super(location, binding)
|
30
43
|
@index, @is_default = index, is_default
|
data/lib/saml2/engine.rb
CHANGED
data/lib/saml2/entity.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'nokogiri'
|
2
4
|
|
3
5
|
require 'saml2/base'
|
@@ -11,8 +13,13 @@ module SAML2
|
|
11
13
|
include OrganizationAndContacts
|
12
14
|
include Signable
|
13
15
|
|
16
|
+
# @return [String]
|
14
17
|
attr_writer :entity_id
|
15
18
|
|
19
|
+
# Parse a metadata file, and return an appropriate object.
|
20
|
+
#
|
21
|
+
# @param xml [String, IO] Anything that can be passed to +Nokogiri::XML+
|
22
|
+
# @return [Entity, Group, nil]
|
16
23
|
def self.parse(xml)
|
17
24
|
document = Nokogiri::XML(xml)
|
18
25
|
|
@@ -46,6 +53,7 @@ module SAML2
|
|
46
53
|
@valid_until = nil
|
47
54
|
end
|
48
55
|
|
56
|
+
# (see Base#from_xml)
|
49
57
|
def from_xml(node)
|
50
58
|
super
|
51
59
|
@id = nil
|
@@ -55,14 +63,17 @@ module SAML2
|
|
55
63
|
'EntitiesDescriptor' => Group)
|
56
64
|
end
|
57
65
|
|
66
|
+
# (see Message#valid_schema?)
|
58
67
|
def valid_schema?
|
59
68
|
Schemas.federation.valid?(xml.document)
|
60
69
|
end
|
61
70
|
|
71
|
+
# (see Message#id)
|
62
72
|
def id
|
63
73
|
@id ||= xml['ID']
|
64
74
|
end
|
65
75
|
|
76
|
+
# @return [Time, nil]
|
66
77
|
def valid_until
|
67
78
|
unless instance_variable_defined?(:@valid_until)
|
68
79
|
@valid_until = xml['validUntil'] && Time.parse(xml['validUntil'])
|
@@ -79,6 +90,7 @@ module SAML2
|
|
79
90
|
@id = "_#{SecureRandom.uuid}"
|
80
91
|
end
|
81
92
|
|
93
|
+
# (see Base#from_xml)
|
82
94
|
def from_xml(node)
|
83
95
|
super
|
84
96
|
@id = nil
|
@@ -86,18 +98,22 @@ module SAML2
|
|
86
98
|
@roles = nil
|
87
99
|
end
|
88
100
|
|
101
|
+
# (see Message#valid_schema?)
|
89
102
|
def valid_schema?
|
90
103
|
Schemas.federation.valid?(xml.document)
|
91
104
|
end
|
92
105
|
|
106
|
+
# @return [String]
|
93
107
|
def entity_id
|
94
108
|
@entity_id || xml && xml['entityID']
|
95
109
|
end
|
96
110
|
|
111
|
+
# (see Message#id)
|
97
112
|
def id
|
98
113
|
@id ||= xml['ID']
|
99
114
|
end
|
100
115
|
|
116
|
+
# @return [Time, nil]
|
101
117
|
def valid_until
|
102
118
|
unless instance_variable_defined?(:@valid_until)
|
103
119
|
@valid_until = xml['validUntil'] && Time.parse(xml['validUntil'])
|
@@ -105,19 +121,23 @@ module SAML2
|
|
105
121
|
@valid_until
|
106
122
|
end
|
107
123
|
|
124
|
+
# @return [Array<IdentityProvider>]
|
108
125
|
def identity_providers
|
109
126
|
roles.select { |r| r.is_a?(IdentityProvider) }
|
110
127
|
end
|
111
128
|
|
129
|
+
# @return [Array<ServiceProvider>]
|
112
130
|
def service_providers
|
113
131
|
roles.select { |r| r.is_a?(ServiceProvider) }
|
114
132
|
end
|
115
133
|
|
134
|
+
# @return [Array<Role>]
|
116
135
|
def roles
|
117
136
|
@roles ||= load_object_array(xml, 'md:IDPSSODescriptor', IdentityProvider) +
|
118
137
|
load_object_array(xml, 'md:SPSSODescriptor', ServiceProvider)
|
119
138
|
end
|
120
139
|
|
140
|
+
# (see Base#build)
|
121
141
|
def build(builder)
|
122
142
|
builder['md'].EntityDescriptor('entityID' => entity_id,
|
123
143
|
'xmlns:md' => Namespaces::METADATA,
|
@@ -1,9 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'saml2/attribute'
|
2
4
|
require 'saml2/sso'
|
3
5
|
|
4
6
|
module SAML2
|
5
7
|
class IdentityProvider < SSO
|
6
|
-
|
8
|
+
# @return [Boolean, nil]
|
9
|
+
attr_writer :want_authn_requests_signed
|
10
|
+
attr_writer :single_sign_on_services, :attribute_profiles, :attributes
|
7
11
|
|
8
12
|
def initialize
|
9
13
|
super
|
@@ -13,6 +17,7 @@ module SAML2
|
|
13
17
|
@attributes = []
|
14
18
|
end
|
15
19
|
|
20
|
+
# (see Base#from_xml)
|
16
21
|
def from_xml(node)
|
17
22
|
super
|
18
23
|
remove_instance_variable(:@want_authn_requests_signed)
|
@@ -21,6 +26,7 @@ module SAML2
|
|
21
26
|
@attributes = nil
|
22
27
|
end
|
23
28
|
|
29
|
+
# @return [Boolean, nil]
|
24
30
|
def want_authn_requests_signed?
|
25
31
|
unless instance_variable_defined?(:@want_authn_requests_signed)
|
26
32
|
@want_authn_requests_signed = xml['WantAuthnRequestsSigned'] && xml['WantAuthnRequestsSigned'] == 'true'
|
@@ -28,18 +34,22 @@ module SAML2
|
|
28
34
|
@want_authn_requests_signed
|
29
35
|
end
|
30
36
|
|
37
|
+
# @return [Array<Endpoint>]
|
31
38
|
def single_sign_on_services
|
32
39
|
@single_sign_on_services ||= load_object_array(xml, 'md:SingleSignOnService', Endpoint)
|
33
40
|
end
|
34
41
|
|
42
|
+
# @return [Array<String>]
|
35
43
|
def attribute_profiles
|
36
44
|
@attribute_profiles ||= load_string_array(xml, 'md:AttributeProfile')
|
37
45
|
end
|
38
46
|
|
47
|
+
# @return [Array<Attribute>]
|
39
48
|
def attributes
|
40
49
|
@attributes ||= load_object_array(xml, 'saml:Attribute', Attribute)
|
41
50
|
end
|
42
51
|
|
52
|
+
# (see Base#build)
|
43
53
|
def build(builder)
|
44
54
|
builder['md'].IDPSSODescriptor do |idp_sso_descriptor|
|
45
55
|
super(idp_sso_descriptor)
|
data/lib/saml2/indexed_object.rb
CHANGED
@@ -1,18 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'saml2/base'
|
2
4
|
|
3
5
|
module SAML2
|
4
6
|
module IndexedObject
|
7
|
+
# @return [Integer]
|
5
8
|
attr_accessor :index
|
6
9
|
|
7
|
-
def initialize(*
|
10
|
+
def initialize(*)
|
8
11
|
@is_default = nil
|
9
12
|
super
|
10
13
|
end
|
11
14
|
|
12
15
|
def eql?(rhs)
|
13
16
|
index == rhs.index &&
|
14
|
-
|
15
|
-
|
17
|
+
default? == rhs.default? &&
|
18
|
+
super
|
16
19
|
end
|
17
20
|
|
18
21
|
def default?
|
@@ -23,13 +26,18 @@ module SAML2
|
|
23
26
|
!@is_default.nil?
|
24
27
|
end
|
25
28
|
|
29
|
+
# (see Base#from_xml)
|
26
30
|
def from_xml(node)
|
27
31
|
@index = node['index'] && node['index'].to_i
|
28
32
|
@is_default = node['isDefault'] && node['isDefault'] == 'true'
|
29
33
|
super
|
30
34
|
end
|
31
35
|
|
36
|
+
# Keeps an Array of {IndexedObject}s in their +index+ed order.
|
32
37
|
class Array < ::Array
|
38
|
+
# Returns the first object which is set as the default, or the first
|
39
|
+
# object if none are set as the default.
|
40
|
+
# @return [IndexedObject]
|
33
41
|
attr_reader :default
|
34
42
|
|
35
43
|
def self.from_xml(nodes)
|
@@ -68,6 +76,7 @@ module SAML2
|
|
68
76
|
end
|
69
77
|
end
|
70
78
|
|
79
|
+
# (see Base#build)
|
71
80
|
def build(builder, *)
|
72
81
|
super
|
73
82
|
builder.parent.children.last['index'] = index
|
@@ -75,6 +84,7 @@ module SAML2
|
|
75
84
|
end
|
76
85
|
|
77
86
|
private
|
87
|
+
|
78
88
|
def self.included(klass)
|
79
89
|
klass.const_set(:Array, Array.dup)
|
80
90
|
end
|
data/lib/saml2/key.rb
CHANGED
@@ -1,23 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'saml2/base'
|
1
4
|
require 'saml2/namespaces'
|
2
5
|
|
3
6
|
module SAML2
|
4
|
-
|
7
|
+
# This represents the XML Signatures <KeyInfo> element, and actually contains a
|
8
|
+
# reference to an X.509 certificate, not solely a public key.
|
9
|
+
class KeyInfo < Base
|
10
|
+
# @return [String] The PEM encoded certificate.
|
11
|
+
attr_reader :x509
|
12
|
+
|
13
|
+
# @param x509 [String] The PEM encoded certificate.
|
14
|
+
def initialize(x509 = nil)
|
15
|
+
self.x509 = x509
|
16
|
+
end
|
17
|
+
|
18
|
+
# (see Base#from_xml)
|
19
|
+
def from_xml(node)
|
20
|
+
self.x509 = node.at_xpath('dsig:KeyInfo/dsig:X509Data/dsig:X509Certificate', Namespaces::ALL)&.content&.strip
|
21
|
+
end
|
22
|
+
|
23
|
+
def x509=(value)
|
24
|
+
@x509 = value&.gsub(/\w*-+(BEGIN|END) CERTIFICATE-+\w*/, "")&.strip
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [OpenSSL::X509::Certificate]
|
28
|
+
def certificate
|
29
|
+
@certificate ||= OpenSSL::X509::Certificate.new(Base64.decode64(x509))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Formats a fingerprint as all lowercase, with a : every two characters.
|
33
|
+
# @param fingerprint [String]
|
34
|
+
# @return [String]
|
35
|
+
def self.format_fingerprint(fingerprint)
|
36
|
+
fingerprint.downcase.gsub(/(\h{2})(?=\h)/, '\1:')
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String]
|
40
|
+
def fingerprint
|
41
|
+
@fingerprint ||= self.class.format_fingerprint(Digest::SHA1.hexdigest(certificate.to_der))
|
42
|
+
end
|
43
|
+
|
44
|
+
# (see Base#build)
|
45
|
+
def build(builder)
|
46
|
+
builder['dsig'].KeyInfo do |key_info|
|
47
|
+
key_info['dsig'].X509Data do |x509_data|
|
48
|
+
x509_data['dsig'].X509Certificate(x509)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class KeyDescriptor < KeyInfo
|
5
55
|
module Type
|
6
|
-
ENCRYPTION = 'encryption'
|
7
|
-
SIGNING = 'signing'
|
56
|
+
ENCRYPTION = 'encryption'
|
57
|
+
SIGNING = 'signing'
|
8
58
|
end
|
9
59
|
|
10
|
-
class EncryptionMethod
|
60
|
+
class EncryptionMethod < Base
|
11
61
|
module Algorithm
|
12
|
-
AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
|
62
|
+
AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
|
13
63
|
end
|
14
64
|
|
15
|
-
|
65
|
+
# @see Algorithm
|
66
|
+
# @return [String]
|
67
|
+
attr_accessor :algorithm
|
68
|
+
# @return [Integer]
|
69
|
+
attr_accessor :key_size
|
16
70
|
|
71
|
+
# @param algorithm [String]
|
72
|
+
# @param key_size [Integer]
|
17
73
|
def initialize(algorithm = Algorithm::AES128_CBC, key_size = 128)
|
18
74
|
@algorithm, @key_size = algorithm, key_size
|
19
75
|
end
|
20
76
|
|
77
|
+
# (see Base#from_xml)
|
78
|
+
def from_xml(node)
|
79
|
+
self.algorithm = node['Algorithm']
|
80
|
+
self.key_size = node.at_xpath('xenc:KeySize', Namespaces::ALL)&.content&.to_i
|
81
|
+
end
|
82
|
+
|
83
|
+
# (see Base#build)
|
21
84
|
def build(builder)
|
22
85
|
builder['md'].EncryptionMethod('Algorithm' => algorithm) do |encryption_method|
|
23
86
|
encryption_method['xenc'].KeySize(key_size) if key_size
|
@@ -25,18 +88,24 @@ module SAML2
|
|
25
88
|
end
|
26
89
|
end
|
27
90
|
|
28
|
-
|
91
|
+
# @see Type
|
92
|
+
# @return [String]
|
93
|
+
attr_accessor :use
|
94
|
+
# @return [Array<EncryptionMethod>]
|
95
|
+
attr_accessor :encryption_methods
|
29
96
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
new(x509 && x509.content.strip, node['use'], methods.map { |m| m['Algorithm'] })
|
97
|
+
# (see Base#from_xml)
|
98
|
+
def from_xml(node)
|
99
|
+
super
|
100
|
+
self.use = node['use']
|
101
|
+
self.encryption_methods = load_object_array(node, 'md:EncryptionMethod', EncryptionMethod)
|
36
102
|
end
|
37
103
|
|
38
|
-
|
39
|
-
|
104
|
+
# @param x509 [String] The PEM encoded certificate.
|
105
|
+
# @param use optional [String] See {Type}
|
106
|
+
# @param encryption_methods [Array<EncryptionMethod>]
|
107
|
+
def initialize(x509 = nil, use = nil, encryption_methods = [])
|
108
|
+
@use, self.x509, @encryption_methods = use, x509, encryption_methods
|
40
109
|
end
|
41
110
|
|
42
111
|
def encryption?
|
@@ -47,30 +116,18 @@ module SAML2
|
|
47
116
|
use.nil? || use == Type::SIGNING
|
48
117
|
end
|
49
118
|
|
50
|
-
|
51
|
-
@certificate ||= OpenSSL::X509::Certificate.new(Base64.decode64(x509))
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.format_fingerprint(fingerprint)
|
55
|
-
fingerprint.downcase.gsub(/(\h{2})(?=\h)/, '\1:')
|
56
|
-
end
|
57
|
-
|
58
|
-
def fingerprint
|
59
|
-
@fingerprint ||= self.class.format_fingerprint(Digest::SHA1.hexdigest(certificate.to_der))
|
60
|
-
end
|
61
|
-
|
119
|
+
# (see Base#build)
|
62
120
|
def build(builder)
|
63
121
|
builder['md'].KeyDescriptor do |key_descriptor|
|
64
122
|
key_descriptor.parent['use'] = use if use
|
65
|
-
key_descriptor
|
66
|
-
key_info['dsig'].X509Data do |x509_data|
|
67
|
-
x509_data['dsig'].X509Certificate(x509)
|
68
|
-
end
|
69
|
-
end
|
123
|
+
super(key_descriptor)
|
70
124
|
encryption_methods.each do |method|
|
71
125
|
method.build(key_descriptor)
|
72
126
|
end
|
73
127
|
end
|
74
128
|
end
|
75
129
|
end
|
130
|
+
|
131
|
+
# @deprecated Deprecated alias for KeyDescriptor
|
132
|
+
Key = KeyDescriptor
|
76
133
|
end
|
data/lib/saml2/localized_name.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'saml2/base'
|
2
4
|
require 'saml2/namespaces'
|
3
5
|
|
@@ -16,6 +18,11 @@ module SAML2
|
|
16
18
|
end
|
17
19
|
end
|
18
20
|
|
21
|
+
# @param lang [String, Symbol, :all, nil]
|
22
|
+
# The language to retrieve the localized string for.
|
23
|
+
# +:all+ will return the hash itself, and +nil+ will return the first
|
24
|
+
# localized string regardless of language.
|
25
|
+
# @return [String]
|
19
26
|
def [](lang)
|
20
27
|
case lang
|
21
28
|
when :all
|
@@ -27,6 +34,7 @@ module SAML2
|
|
27
34
|
end
|
28
35
|
end
|
29
36
|
|
37
|
+
# @return [String] The first localized string regardless of language
|
30
38
|
def to_s
|
31
39
|
self[nil].to_s
|
32
40
|
end
|
data/lib/saml2/logout_request.rb
CHANGED
@@ -1,11 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'saml2/name_id'
|
2
4
|
require 'saml2/request'
|
3
5
|
|
4
6
|
module SAML2
|
5
7
|
class LogoutRequest < Request
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
attr_writer :name_id, :session_index
|
9
|
+
|
10
|
+
# @param sso [SSO]
|
11
|
+
# @param issuer [NameID]
|
12
|
+
# @param name_id [NameID]
|
13
|
+
# @param session_index optional [String, Array<String>]
|
14
|
+
# @return [LogoutRequest]
|
15
|
+
def self.initiate(sso, issuer, name_id, session_index = [])
|
9
16
|
logout_request = new
|
10
17
|
logout_request.issuer = issuer
|
11
18
|
logout_request.destination = sso.single_logout_services.first.location
|
@@ -15,10 +22,12 @@ module SAML2
|
|
15
22
|
logout_request
|
16
23
|
end
|
17
24
|
|
25
|
+
# @return [NameID]
|
18
26
|
def name_id
|
19
27
|
@name_id ||= (NameID.from_xml(xml.at_xpath('saml:NameID', Namespaces::ALL)) if xml)
|
20
28
|
end
|
21
29
|
|
30
|
+
# @return [String, Array<String>]
|
22
31
|
def session_index
|
23
32
|
@session_index ||= (load_string_array(xml,'samlp:SessionIndex') if xml)
|
24
33
|
end
|