saml2 2.0.2 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|