saml2 1.0.10 → 1.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/Rakefile +2 -7
- data/lib/saml2.rb +2 -0
- data/lib/saml2/attribute.rb +2 -0
- data/lib/saml2/attribute_consuming_service.rb +1 -0
- data/lib/saml2/authn_request.rb +19 -47
- data/lib/saml2/base.rb +5 -2
- data/lib/saml2/bindings.rb +7 -0
- data/lib/saml2/bindings/http_redirect.rb +141 -0
- data/lib/saml2/contact.rb +14 -16
- data/lib/saml2/endpoint.rb +5 -6
- data/lib/saml2/entity.rb +23 -18
- data/lib/saml2/identity_provider.rb +4 -4
- data/lib/saml2/indexed_object.rb +7 -3
- data/lib/saml2/key.rb +19 -1
- data/lib/saml2/logout_request.rb +43 -0
- data/lib/saml2/logout_response.rb +23 -0
- data/lib/saml2/message.rb +109 -0
- data/lib/saml2/name_id.rb +16 -8
- data/lib/saml2/organization_and_contacts.rb +2 -2
- data/lib/saml2/request.rb +8 -0
- data/lib/saml2/response.rb +7 -23
- data/lib/saml2/role.rb +2 -3
- data/lib/saml2/service_provider.rb +24 -2
- data/lib/saml2/sso.rb +2 -2
- data/lib/saml2/status.rb +28 -0
- data/lib/saml2/status_response.rb +33 -0
- data/lib/saml2/version.rb +1 -1
- data/spec/fixtures/identity_provider.xml +1 -0
- data/spec/fixtures/response_signed.xml +1 -1
- data/spec/fixtures/response_with_attribute_signed.xml +1 -1
- data/spec/lib/attribute_consuming_service_spec.rb +37 -37
- data/spec/lib/attribute_spec.rb +17 -17
- data/spec/lib/authn_request_spec.rb +15 -71
- data/spec/lib/bindings/http_redirect_spec.rb +151 -0
- data/spec/lib/conditions_spec.rb +10 -10
- data/spec/lib/entity_spec.rb +12 -12
- data/spec/lib/identity_provider_spec.rb +4 -4
- data/spec/lib/indexed_object_spec.rb +38 -7
- data/spec/lib/logout_request_spec.rb +31 -0
- data/spec/lib/logout_response_spec.rb +31 -0
- data/spec/lib/message_spec.rb +21 -0
- data/spec/lib/response_spec.rb +8 -9
- data/spec/lib/service_provider_spec.rb +29 -8
- data/spec/spec_helper.rb +0 -1
- metadata +41 -11
|
@@ -23,21 +23,21 @@ module SAML2
|
|
|
23
23
|
|
|
24
24
|
def want_authn_requests_signed?
|
|
25
25
|
unless instance_variable_defined?(:@want_authn_requests_signed)
|
|
26
|
-
@want_authn_requests_signed =
|
|
26
|
+
@want_authn_requests_signed = xml['WantAuthnRequestsSigned'] && xml['WantAuthnRequestsSigned'] == 'true'
|
|
27
27
|
end
|
|
28
28
|
@want_authn_requests_signed
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def single_sign_on_services
|
|
32
|
-
@single_sign_on_services ||= load_object_array(
|
|
32
|
+
@single_sign_on_services ||= load_object_array(xml, 'md:SingleSignOnService', Endpoint)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def attribute_profiles
|
|
36
|
-
@attribute_profiles ||= load_string_array(
|
|
36
|
+
@attribute_profiles ||= load_string_array(xml, 'md:AttributeProfile')
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def attributes
|
|
40
|
-
@attributes ||= load_object_array(
|
|
40
|
+
@attributes ||= load_object_array(xml, 'saml:Attribute', Attribute)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def build(builder)
|
data/lib/saml2/indexed_object.rb
CHANGED
|
@@ -19,6 +19,10 @@ module SAML2
|
|
|
19
19
|
!!@is_default
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def default_defined?
|
|
23
|
+
!@is_default.nil?
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
def from_xml(node)
|
|
23
27
|
@index = node['index'] && node['index'].to_i
|
|
24
28
|
@is_default = node['isDefault'] && node['isDefault'] == 'true'
|
|
@@ -50,10 +54,10 @@ module SAML2
|
|
|
50
54
|
end
|
|
51
55
|
end
|
|
52
56
|
|
|
53
|
-
def build(builder)
|
|
57
|
+
def build(builder, *)
|
|
54
58
|
super
|
|
55
|
-
builder.parent.last['index'] = index
|
|
56
|
-
builder.parent.last['isDefault'] = default?
|
|
59
|
+
builder.parent.children.last['index'] = index
|
|
60
|
+
builder.parent.children.last['isDefault'] = default? if default_defined?
|
|
57
61
|
end
|
|
58
62
|
|
|
59
63
|
private
|
data/lib/saml2/key.rb
CHANGED
|
@@ -7,6 +7,24 @@ module SAML2
|
|
|
7
7
|
SIGNING = 'signing'.freeze
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
class EncryptionMethod
|
|
11
|
+
module Algorithm
|
|
12
|
+
AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_accessor :algorithm, :key_size
|
|
16
|
+
|
|
17
|
+
def initialize(algorithm = Algorithm::AES128_CBC, key_size = 128)
|
|
18
|
+
@algorithm, @key_size = algorithm, key_size
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build(builder)
|
|
22
|
+
builder['md'].EncryptionMethod('Algorithm' => algorithm) do |encryption_method|
|
|
23
|
+
encryption_method['xenc'].KeySize(key_size) if key_size
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
10
28
|
attr_accessor :use, :x509, :encryption_methods
|
|
11
29
|
|
|
12
30
|
def self.from_xml(node)
|
|
@@ -46,7 +64,7 @@ module SAML2
|
|
|
46
64
|
end
|
|
47
65
|
end
|
|
48
66
|
encryption_methods.each do |method|
|
|
49
|
-
|
|
67
|
+
method.build(key_descriptor)
|
|
50
68
|
end
|
|
51
69
|
end
|
|
52
70
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'saml2/name_id'
|
|
2
|
+
require 'saml2/request'
|
|
3
|
+
|
|
4
|
+
module SAML2
|
|
5
|
+
class LogoutRequest < Request
|
|
6
|
+
attr_accessor :name_id, :session_index
|
|
7
|
+
|
|
8
|
+
def self.initiate(sso, issuer, name_id, session_index = nil)
|
|
9
|
+
logout_request = new
|
|
10
|
+
logout_request.issuer = issuer
|
|
11
|
+
logout_request.destination = sso.single_logout_services.first.location
|
|
12
|
+
logout_request.name_id = name_id
|
|
13
|
+
logout_request.session_index = session_index
|
|
14
|
+
|
|
15
|
+
logout_request
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name_id
|
|
19
|
+
@name_id ||= (NameID.from_xml(xml.at_xpath('saml:NameID', Namespaces::ALL)) if xml)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def session_index
|
|
23
|
+
@session_index ||= (load_string_array(xml,'samlp:SessionIndex') if xml)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build(builder)
|
|
29
|
+
builder['samlp'].LogoutRequest(
|
|
30
|
+
'xmlns:samlp' => Namespaces::SAMLP,
|
|
31
|
+
'xmlns:saml' => Namespaces::SAML
|
|
32
|
+
) do |logout_request|
|
|
33
|
+
super(logout_request)
|
|
34
|
+
|
|
35
|
+
name_id.build(logout_request)
|
|
36
|
+
|
|
37
|
+
Array(session_index).each do |session_index_instance|
|
|
38
|
+
logout_request['samlp'].SessionIndex(session_index_instance)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'saml2/status_response'
|
|
2
|
+
|
|
3
|
+
module SAML2
|
|
4
|
+
class LogoutResponse < StatusResponse
|
|
5
|
+
def self.respond_to(logout_request, sso, issuer, status_code = Status::SUCCESS)
|
|
6
|
+
logout_response = new
|
|
7
|
+
logout_response.issuer = issuer
|
|
8
|
+
logout_response.destination = sso.single_logout_services.first.location
|
|
9
|
+
logout_response.in_response_to = logout_request.id
|
|
10
|
+
logout_response.status.code = status_code
|
|
11
|
+
logout_response
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build(builder)
|
|
15
|
+
builder['samlp'].LogoutResponse(
|
|
16
|
+
'xmlns:samlp' => Namespaces::SAMLP,
|
|
17
|
+
'xmlns:saml' => Namespaces::SAML
|
|
18
|
+
) do |logout_response|
|
|
19
|
+
super(logout_response)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
require 'time'
|
|
3
|
+
|
|
4
|
+
require 'saml2/base'
|
|
5
|
+
|
|
6
|
+
module SAML2
|
|
7
|
+
class InvalidMessage < RuntimeError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class MissingMessage < InvalidMessage
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class CorruptMessage < InvalidMessage
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class MessageTooLarge < InvalidMessage
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class UnknownMessage < InvalidMessage
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class UnexpectedMessage < InvalidMessage
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class UnsupportedEncoding < InvalidMessage
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class UnsupportedSignatureAlgorithm < InvalidMessage
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class InvalidSignature < InvalidMessage
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class UnsignedMessage < InvalidMessage
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# In the SAML Schema, Request and Response don't technically share a common
|
|
38
|
+
# ancestor, but they have several things in common so it's useful to represent
|
|
39
|
+
# that here
|
|
40
|
+
class Message < Base
|
|
41
|
+
attr_reader :id, :issue_instant
|
|
42
|
+
attr_accessor :issuer, :destination
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
def inherited(klass)
|
|
46
|
+
# explicitly keep track of all messages in this base class
|
|
47
|
+
Message.known_messages[klass.name.sub(/^SAML2::/, '')] = klass
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def from_xml(node)
|
|
51
|
+
return super unless self == Message
|
|
52
|
+
klass = Message.known_messages[node.name]
|
|
53
|
+
raise UnknownMessage.new("Unknown message #{node.name}") unless klass
|
|
54
|
+
klass.from_xml(node)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse(xml)
|
|
58
|
+
result = Message.from_xml(Nokogiri::XML(xml) { |config| config.strict }.root)
|
|
59
|
+
raise UnexpectedMessage.new("Expected a #{self.name}, but got a #{result.class.name}") unless self == Message || result.class == self
|
|
60
|
+
result
|
|
61
|
+
rescue Nokogiri::XML::SyntaxError
|
|
62
|
+
raise CorruptMessage
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
protected
|
|
66
|
+
|
|
67
|
+
def known_messages
|
|
68
|
+
@known_messages ||= {}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def initialize
|
|
73
|
+
@id = "_#{SecureRandom.uuid}"
|
|
74
|
+
@issue_instant = Time.now.utc
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def from_xml(node)
|
|
78
|
+
super
|
|
79
|
+
@id = nil
|
|
80
|
+
@issue_instant = nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def valid_schema?
|
|
84
|
+
return false unless Schemas.protocol.valid?(xml.document)
|
|
85
|
+
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def issuer
|
|
90
|
+
@issuer ||= NameID.from_xml(xml.at_xpath('saml:Issuer', Namespaces::ALL))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def id
|
|
94
|
+
@id ||= xml['ID']
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
protected
|
|
98
|
+
|
|
99
|
+
# should be called from inside the specific request element
|
|
100
|
+
def build(message)
|
|
101
|
+
message.parent['ID'] = id
|
|
102
|
+
message.parent['Version'] = '2.0'
|
|
103
|
+
message.parent['IssueInstant'] = issue_instant.iso8601
|
|
104
|
+
message.parent['Destination'] = destination if destination
|
|
105
|
+
|
|
106
|
+
issuer.build(message, element: 'Issuer') if issuer
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/saml2/name_id.rb
CHANGED
|
@@ -36,25 +36,33 @@ module SAML2
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
attr_accessor :id, :format, :name_qualifier, :sp_name_qualifier
|
|
40
40
|
|
|
41
41
|
def self.from_xml(node)
|
|
42
|
-
node && new(node.content.strip,
|
|
42
|
+
node && new(node.content.strip,
|
|
43
|
+
node['Format'],
|
|
44
|
+
name_qualifier: node['NameQualifier'],
|
|
45
|
+
sp_name_qualifier: node['SPNameQualifier'])
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
def initialize(id = nil, format = nil)
|
|
46
|
-
@id, @format
|
|
48
|
+
def initialize(id = nil, format = nil, name_qualifier: nil, sp_name_qualifier: nil)
|
|
49
|
+
@id, @format, @name_qualifier, @sp_name_qualifier =
|
|
50
|
+
id, format, name_qualifier, sp_name_qualifier
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def ==(rhs)
|
|
50
|
-
id == rhs.id &&
|
|
54
|
+
id == rhs.id &&
|
|
55
|
+
format == rhs.format &&
|
|
56
|
+
name_qualifier == rhs.name_qualifier &&
|
|
57
|
+
sp_name_qualifier == rhs.sp_name_qualifier
|
|
51
58
|
end
|
|
52
59
|
|
|
53
|
-
def build(builder,
|
|
60
|
+
def build(builder, element: nil)
|
|
54
61
|
args = {}
|
|
55
62
|
args['Format'] = format if format
|
|
56
|
-
args['
|
|
57
|
-
|
|
63
|
+
args['NameQualifier'] = name_qualifier if name_qualifier
|
|
64
|
+
args['SPNameQualifier'] = sp_name_qualifier if sp_name_qualifier
|
|
65
|
+
builder['saml'].__send__(element || 'NameID', id, args)
|
|
58
66
|
end
|
|
59
67
|
end
|
|
60
68
|
end
|
|
@@ -18,13 +18,13 @@ module SAML2
|
|
|
18
18
|
|
|
19
19
|
def organization
|
|
20
20
|
unless instance_variable_defined?(:@organization)
|
|
21
|
-
@organization = Organization.from_xml(
|
|
21
|
+
@organization = Organization.from_xml(xml.at_xpath('md:Organization', Namespaces::ALL))
|
|
22
22
|
end
|
|
23
23
|
@organization
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def contacts
|
|
27
|
-
@contacts ||= load_object_array(
|
|
27
|
+
@contacts ||= load_object_array(xml, 'md:ContactPerson', Contact)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
protected
|
data/lib/saml2/response.rb
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
require 'nokogiri-xmlsec'
|
|
2
|
-
require 'securerandom'
|
|
3
2
|
require 'time'
|
|
4
3
|
|
|
5
4
|
require 'saml2/assertion'
|
|
6
5
|
require 'saml2/authn_statement'
|
|
7
|
-
require 'saml2/
|
|
6
|
+
require 'saml2/status_response'
|
|
8
7
|
require 'saml2/subject'
|
|
9
8
|
|
|
10
9
|
module SAML2
|
|
11
|
-
class Response <
|
|
12
|
-
|
|
13
|
-
SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success".freeze
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
attr_reader :id, :issue_instant, :assertions
|
|
17
|
-
attr_accessor :issuer, :in_response_to, :destination, :status_code
|
|
10
|
+
class Response < StatusResponse
|
|
11
|
+
attr_reader :assertions
|
|
18
12
|
|
|
19
13
|
def self.respond_to(authn_request, issuer, name_id, attributes = nil)
|
|
20
14
|
response = initiate(nil, issuer, name_id)
|
|
@@ -59,9 +53,7 @@ module SAML2
|
|
|
59
53
|
end
|
|
60
54
|
|
|
61
55
|
def initialize
|
|
62
|
-
|
|
63
|
-
@status_code = Status::SUCCESS
|
|
64
|
-
@issue_instant = Time.now.utc
|
|
56
|
+
super
|
|
65
57
|
@assertions = []
|
|
66
58
|
end
|
|
67
59
|
|
|
@@ -70,21 +62,13 @@ module SAML2
|
|
|
70
62
|
end
|
|
71
63
|
|
|
72
64
|
private
|
|
65
|
+
|
|
73
66
|
def build(builder)
|
|
74
67
|
builder['samlp'].Response(
|
|
75
68
|
'xmlns:samlp' => Namespaces::SAMLP,
|
|
76
|
-
|
|
77
|
-
Version: '2.0',
|
|
78
|
-
IssueInstant: issue_instant.iso8601,
|
|
79
|
-
Destination: destination
|
|
69
|
+
'xmlns:saml' => Namespaces::SAML
|
|
80
70
|
) do |response|
|
|
81
|
-
response
|
|
82
|
-
|
|
83
|
-
issuer.build(response, element: 'Issuer', include_namespace: true) if issuer
|
|
84
|
-
|
|
85
|
-
response['samlp'].Status do |status|
|
|
86
|
-
status['samlp'].StatusCode(Value: status_code)
|
|
87
|
-
end
|
|
71
|
+
super(response)
|
|
88
72
|
|
|
89
73
|
assertions.each do |assertion|
|
|
90
74
|
response.parent << assertion.to_xml
|
data/lib/saml2/role.rb
CHANGED
|
@@ -23,17 +23,16 @@ module SAML2
|
|
|
23
23
|
|
|
24
24
|
def from_xml(node)
|
|
25
25
|
super
|
|
26
|
-
@root = node
|
|
27
26
|
@supported_protocols = nil
|
|
28
27
|
@keys = nil
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
def supported_protocols
|
|
32
|
-
@supported_protocols ||=
|
|
31
|
+
@supported_protocols ||= xml['protocolSupportEnumeration'].split
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
def keys
|
|
36
|
-
@keys ||= load_object_array(
|
|
35
|
+
@keys ||= load_object_array(xml, 'md:KeyDescriptor', Key)
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
def signing_keys
|
|
@@ -5,18 +5,40 @@ require 'saml2/sso'
|
|
|
5
5
|
|
|
6
6
|
module SAML2
|
|
7
7
|
class ServiceProvider < SSO
|
|
8
|
+
def initialize
|
|
9
|
+
super
|
|
10
|
+
@assertion_consumer_services = []
|
|
11
|
+
@attribute_consuming_services = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def from_xml(node)
|
|
15
|
+
super
|
|
16
|
+
@assertion_consumer_services = nil
|
|
17
|
+
@attribute_consuming_services = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
8
20
|
def assertion_consumer_services
|
|
9
21
|
@assertion_consumer_services ||= begin
|
|
10
|
-
nodes =
|
|
22
|
+
nodes = xml.xpath('md:AssertionConsumerService', Namespaces::ALL)
|
|
11
23
|
Endpoint::Indexed::Array.from_xml(nodes)
|
|
12
24
|
end
|
|
13
25
|
end
|
|
14
26
|
|
|
15
27
|
def attribute_consuming_services
|
|
16
28
|
@attribute_consuming_services ||= begin
|
|
17
|
-
nodes =
|
|
29
|
+
nodes = xml.xpath('md:AttributeConsumingService', Namespaces::ALL)
|
|
18
30
|
AttributeConsumingService::Array.from_xml(nodes)
|
|
19
31
|
end
|
|
20
32
|
end
|
|
33
|
+
|
|
34
|
+
def build(builder)
|
|
35
|
+
builder['md'].SPSSODescriptor do |sp_sso_descriptor|
|
|
36
|
+
super(sp_sso_descriptor)
|
|
37
|
+
|
|
38
|
+
assertion_consumer_services.each do |acs|
|
|
39
|
+
acs.build(sp_sso_descriptor, 'AssertionConsumerService')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
21
43
|
end
|
|
22
44
|
end
|