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