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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7882f9e6593924c18faed398409e973ca227162a990557156d7992a51c4f7cdb
|
4
|
+
data.tar.gz: c862c3fd1c560068e59340ba4f19a3d810807b6c22a4c36c07fb0ef9a6711132
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89aa07641f0625f8f9fd781cb41a169349567531984c1fe2adc278ae0568687c4e4822be1a9c00e0c561116ffbc39d140abde34a1be439bd36b1a3869d34dcf5
|
7
|
+
data.tar.gz: f07d6fbf9dc816d3223ae9cd1728b3232b469b61057ae7e6e14518efd939e3e8e4aff5c1e3b4a3c56584d7e5dbc9abc33dab180452a8b5d9b5ff7e34012c15e3
|
data/lib/saml2.rb
CHANGED
data/lib/saml2/assertion.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'saml2/conditions'
|
2
4
|
|
3
5
|
module SAML2
|
@@ -16,6 +18,7 @@ module SAML2
|
|
16
18
|
@statements = nil
|
17
19
|
end
|
18
20
|
|
21
|
+
# @return [Subject, nil]
|
19
22
|
def subject
|
20
23
|
if xml && !instance_variable_defined?(:@subject)
|
21
24
|
@subject = Subject.from_xml(xml.at_xpath('saml:Subject', Namespaces::ALL))
|
@@ -23,14 +26,17 @@ module SAML2
|
|
23
26
|
@subject
|
24
27
|
end
|
25
28
|
|
29
|
+
# @return [Conditions]
|
26
30
|
def conditions
|
27
31
|
@conditions ||= Conditions.from_xml(xml.at_xpath('saml:Conditions', Namespaces::ALL))
|
28
32
|
end
|
29
33
|
|
34
|
+
# @return [Array<AuthnStatement, AttributeStatement>]
|
30
35
|
def statements
|
31
36
|
@statements ||= load_object_array(xml, 'saml:AuthnStatement|saml:AttributeStatement')
|
32
37
|
end
|
33
38
|
|
39
|
+
# (see Base#build)
|
34
40
|
def build(builder)
|
35
41
|
builder['saml'].Assertion(
|
36
42
|
'xmlns:saml' => Namespaces::SAML
|
data/lib/saml2/attribute.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'date'
|
2
4
|
|
3
5
|
require 'active_support/core_ext/array/wrap'
|
@@ -8,20 +10,13 @@ require 'saml2/namespaces'
|
|
8
10
|
module SAML2
|
9
11
|
class Attribute < Base
|
10
12
|
module NameFormats
|
11
|
-
BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
|
12
|
-
UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
|
13
|
-
URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
13
|
+
BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
|
14
|
+
UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
|
15
|
+
URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
14
16
|
end
|
15
17
|
|
16
18
|
class << self
|
17
|
-
|
18
|
-
@subclasses ||= []
|
19
|
-
end
|
20
|
-
|
21
|
-
def inherited(klass)
|
22
|
-
subclasses << klass
|
23
|
-
end
|
24
|
-
|
19
|
+
# (see Base.from_xml)
|
25
20
|
def from_xml(node)
|
26
21
|
# pass through for subclasses
|
27
22
|
return super unless self == Attribute
|
@@ -31,20 +26,43 @@ module SAML2
|
|
31
26
|
klass ? klass.from_xml(node) : super
|
32
27
|
end
|
33
28
|
|
29
|
+
# Create an appropriate object to represent an attribute.
|
30
|
+
#
|
31
|
+
# This will create the most appropriate object (i.e. an
|
32
|
+
# {Attribute::X500} if possible) to represent this attribute,
|
33
|
+
# based on its name.
|
34
|
+
# @param name [String]
|
35
|
+
# The attribute name. This can be a friendly name, or a URI.
|
36
|
+
# @param value optional
|
37
|
+
# The attribute value.
|
38
|
+
# @return [Attribute]
|
34
39
|
def create(name, value = nil)
|
35
40
|
(class_for(name) || self).new(name, value)
|
36
41
|
end
|
37
42
|
|
43
|
+
# The XML namespace that this attribute class serializes as.
|
44
|
+
# @return ['saml']
|
38
45
|
def namespace
|
39
46
|
'saml'
|
40
47
|
end
|
41
48
|
|
49
|
+
# The XML element that this attribute class serializes as.
|
50
|
+
# @return ['Attribute']
|
42
51
|
def element
|
43
52
|
'Attribute'
|
44
53
|
end
|
45
54
|
|
46
55
|
protected
|
47
56
|
|
57
|
+
def subclasses
|
58
|
+
@subclasses ||= []
|
59
|
+
end
|
60
|
+
|
61
|
+
def inherited(klass)
|
62
|
+
subclasses << klass
|
63
|
+
end
|
64
|
+
|
65
|
+
|
48
66
|
def class_for(name_or_node)
|
49
67
|
subclasses.find do |klass|
|
50
68
|
klass.respond_to?(:recognizes?) && klass.recognizes?(name_or_node)
|
@@ -52,12 +70,24 @@ module SAML2
|
|
52
70
|
end
|
53
71
|
end
|
54
72
|
|
55
|
-
|
56
|
-
|
73
|
+
# @return [String]
|
74
|
+
attr_accessor :name
|
75
|
+
# @return [String, nil]
|
76
|
+
attr_accessor :friendly_name, :name_format
|
77
|
+
# @return [Object, nil]
|
78
|
+
attr_accessor :value
|
79
|
+
|
80
|
+
# Create a new generic Attribute
|
81
|
+
#
|
82
|
+
# @param name [String]
|
83
|
+
# @param value optional [Object, nil]
|
84
|
+
# @param friendly_name optional [String, nil]
|
85
|
+
# @param name_format optional [String, nil]
|
57
86
|
def initialize(name = nil, value = nil, friendly_name = nil, name_format = nil)
|
58
87
|
@name, @value, @friendly_name, @name_format = name, value, friendly_name, name_format
|
59
88
|
end
|
60
89
|
|
90
|
+
# (see Base#build)
|
61
91
|
def build(builder)
|
62
92
|
builder[self.class.namespace].__send__(self.class.element, 'Name' => name) do |attribute|
|
63
93
|
attribute.parent['FriendlyName'] = friendly_name if friendly_name
|
@@ -71,6 +101,7 @@ module SAML2
|
|
71
101
|
end
|
72
102
|
end
|
73
103
|
|
104
|
+
# (see Base#from_xml)
|
74
105
|
def from_xml(node)
|
75
106
|
super
|
76
107
|
@name = node['Name']
|
@@ -87,6 +118,7 @@ module SAML2
|
|
87
118
|
end
|
88
119
|
|
89
120
|
private
|
121
|
+
|
90
122
|
XS_TYPES = {
|
91
123
|
lookup_qname('xs:boolean', Namespaces::ALL) =>
|
92
124
|
[[TrueClass, FalseClass], nil, ->(v) { %w{true 1}.include?(v) ? true : false }],
|
data/lib/saml2/attribute/x500.rb
CHANGED
@@ -1,34 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SAML2
|
2
4
|
class Attribute
|
3
5
|
class X500 < Attribute
|
4
|
-
GIVEN_NAME = 'urn:oid:2.5.4.42'
|
5
|
-
SN = SURNAME = 'urn:oid:2.5.4.4'
|
6
|
+
GIVEN_NAME = 'urn:oid:2.5.4.42'
|
7
|
+
SN = SURNAME = 'urn:oid:2.5.4.4'
|
6
8
|
# https://www.ietf.org/rfc/rfc2798.txt
|
7
9
|
module InetOrgPerson
|
8
|
-
DISPLAY_NAME = 'urn:oid:2.16.840.1.113730.3.1.241'
|
9
|
-
EMPLOYEE_NUMBER = 'urn:oid:2.16.840.1.113730.3.1.3'
|
10
|
-
EMPLOYEE_TYPE = 'urn:oid:2.16.840.1.113730.3.1.4'
|
11
|
-
PREFERRED_LANGUAGE = 'urn:oid:2.16.840.1.113730.3.1.39'
|
10
|
+
DISPLAY_NAME = 'urn:oid:2.16.840.1.113730.3.1.241'
|
11
|
+
EMPLOYEE_NUMBER = 'urn:oid:2.16.840.1.113730.3.1.3'
|
12
|
+
EMPLOYEE_TYPE = 'urn:oid:2.16.840.1.113730.3.1.4'
|
13
|
+
PREFERRED_LANGUAGE = 'urn:oid:2.16.840.1.113730.3.1.39'
|
12
14
|
end
|
13
15
|
# https://www.internet2.edu/media/medialibrary/2013/09/04/internet2-mace-dir-eduperson-201203.html
|
14
16
|
module EduPerson
|
15
|
-
AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1'
|
16
|
-
ASSURANCE = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.11'
|
17
|
-
ENTITLEMENT = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'
|
18
|
-
NICKNAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.2'
|
19
|
-
ORG_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.3'
|
20
|
-
PRIMARY_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.5'
|
21
|
-
PRIMARY_ORG_UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.8'
|
22
|
-
PRINCIPAL_NAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6'
|
23
|
-
SCOPED_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9'
|
24
|
-
TARGETED_I_D = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10'
|
25
|
-
UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.4'
|
17
|
+
AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1'
|
18
|
+
ASSURANCE = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.11'
|
19
|
+
ENTITLEMENT = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'
|
20
|
+
NICKNAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.2'
|
21
|
+
ORG_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.3'
|
22
|
+
PRIMARY_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.5'
|
23
|
+
PRIMARY_ORG_UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.8'
|
24
|
+
PRINCIPAL_NAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6'
|
25
|
+
SCOPED_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9'
|
26
|
+
TARGETED_I_D = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10'
|
27
|
+
UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.4'
|
26
28
|
end
|
27
29
|
# http://www.ietf.org/rfc/rfc4519.txt
|
28
|
-
UID = USERID = 'urn:oid:0.9.2342.19200300.100.1.1'
|
30
|
+
UID = USERID = 'urn:oid:0.9.2342.19200300.100.1.1'
|
29
31
|
# http://www.ietf.org/rfc/rfc4524.txt
|
30
|
-
MAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
|
32
|
+
MAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
|
31
33
|
|
34
|
+
# Returns true if the param should be an {X500} Attribute.
|
35
|
+
# @param name_or_node [String, Nokogiri::XML::Element]
|
32
36
|
def self.recognizes?(name_or_node)
|
33
37
|
if name_or_node.is_a?(Nokogiri::XML::Element)
|
34
38
|
!!name_or_node.at_xpath("@x500:Encoding", Namespaces::ALL)
|
@@ -37,6 +41,14 @@ module SAML2
|
|
37
41
|
end
|
38
42
|
end
|
39
43
|
|
44
|
+
# Create a new X.500 attribute.
|
45
|
+
#
|
46
|
+
# The name format will always be set to URI.
|
47
|
+
#
|
48
|
+
# @param name [String]
|
49
|
+
# Either an OID or a known friendly name. The opposite value will be
|
50
|
+
# inferred automatically.
|
51
|
+
# @param value optional [Object, nil]
|
40
52
|
def initialize(name = nil, value = nil)
|
41
53
|
# if they pass an OID, infer the friendly name
|
42
54
|
friendly_name = OIDS[name]
|
@@ -51,6 +63,7 @@ module SAML2
|
|
51
63
|
super(name, value, friendly_name, NameFormats::URI)
|
52
64
|
end
|
53
65
|
|
66
|
+
# (see Base#build)
|
54
67
|
def build(builder)
|
55
68
|
super
|
56
69
|
attr = builder.parent.last_element_child
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/core_ext/array/wrap'
|
2
4
|
|
3
5
|
require 'saml2/attribute'
|
@@ -8,40 +10,61 @@ require 'saml2/namespaces'
|
|
8
10
|
module SAML2
|
9
11
|
class RequestedAttribute < Attribute
|
10
12
|
class << self
|
13
|
+
# The XML namespace that this attribute class serializes as.
|
14
|
+
# @return ['md']
|
11
15
|
def namespace
|
12
16
|
'md'
|
13
17
|
end
|
14
18
|
|
19
|
+
# The XML element that this attribute class serializes as.
|
20
|
+
# @return ['RequestedAttribute']
|
15
21
|
def element
|
16
22
|
'RequestedAttribute'
|
17
23
|
end
|
18
24
|
|
25
|
+
# Create a RequestAttribute object to represent an attribute.
|
26
|
+
#
|
27
|
+
# {Attribute.create} will be used to create a temporary object, so that
|
28
|
+
# attribute-class specific inferences (i.e. {Attribute::X500} friendly
|
29
|
+
# names) will be done, but always returns a {RequestedAttribute}.
|
30
|
+
# @param name [String]
|
31
|
+
# The attribute name. This can be a friendly name, or a URI.
|
32
|
+
# @param is_required optional [true, false, nil]
|
33
|
+
# @return [RequestedAttribute]
|
19
34
|
def create(name, is_required = nil)
|
20
|
-
# use Attribute.create to get other subclasses to automatically fill out friendly_name
|
21
|
-
# and name_format, but still return a RequestedAttribute object
|
22
35
|
attribute = Attribute.create(name)
|
23
36
|
new(attribute.name, is_required, attribute.friendly_name, attribute.name_format)
|
24
37
|
end
|
25
38
|
end
|
26
39
|
|
40
|
+
# Create a new {RequestedAttribute}.
|
41
|
+
#
|
42
|
+
# @param name [String]
|
43
|
+
# @param is_required optional [true, false, nil]
|
44
|
+
# @param friendly_name optional [String, nil]
|
45
|
+
# @param name_format optional [String, nil]
|
27
46
|
def initialize(name = nil, is_required = nil, friendly_name = nil, name_format = nil)
|
28
47
|
super(name, nil, friendly_name, name_format)
|
29
48
|
@is_required = is_required
|
30
49
|
end
|
31
50
|
|
51
|
+
# (see Base#from_xml)
|
32
52
|
def from_xml(node)
|
33
53
|
super
|
34
54
|
@is_required = node['isRequired'] && node['isRequired'] == 'true'
|
35
55
|
end
|
36
56
|
|
57
|
+
# @return [true, false, nil]
|
37
58
|
def required?
|
38
59
|
@is_required
|
39
60
|
end
|
40
61
|
end
|
41
62
|
|
42
63
|
class RequiredAttributeMissing < RuntimeError
|
64
|
+
# @return [RequestedAttribute]
|
43
65
|
attr_reader :requested_attribute
|
44
66
|
|
67
|
+
# @param requested_attribute [RequestedAttribute]
|
45
68
|
def initialize(requested_attribute)
|
46
69
|
super("Required attribute #{requested_attribute.name} not provided")
|
47
70
|
@requested_attribute = requested_attribute
|
@@ -49,8 +72,11 @@ module SAML2
|
|
49
72
|
end
|
50
73
|
|
51
74
|
class InvalidAttributeValue < RuntimeError
|
52
|
-
|
75
|
+
# @return [RequestedAttribute]
|
76
|
+
attr_reader :requested_attribute
|
77
|
+
attr_reader :provided_value
|
53
78
|
|
79
|
+
# @param requested_attribute [RequestedAttribute]
|
54
80
|
def initialize(requested_attribute, provided_value)
|
55
81
|
super("Attribute #{requested_attribute.name} is provided value " \
|
56
82
|
"#{provided_value.inspect}, but only allows " \
|
@@ -62,8 +88,13 @@ module SAML2
|
|
62
88
|
class AttributeConsumingService < Base
|
63
89
|
include IndexedObject
|
64
90
|
|
65
|
-
|
91
|
+
# @return [LocalizedName]
|
92
|
+
attr_reader :name, :description
|
93
|
+
# @return [Array<RequestedAttribute>]
|
94
|
+
attr_reader :requested_attributes
|
66
95
|
|
96
|
+
# @param name [String]
|
97
|
+
# @param requested_attributes [::Array<RequestedAttributes>]
|
67
98
|
def initialize(name = nil, requested_attributes = [])
|
68
99
|
super()
|
69
100
|
@name = LocalizedName.new('ServiceName', name)
|
@@ -71,6 +102,7 @@ module SAML2
|
|
71
102
|
@requested_attributes = requested_attributes
|
72
103
|
end
|
73
104
|
|
105
|
+
# (see Base#from_xml)
|
74
106
|
def from_xml(node)
|
75
107
|
super
|
76
108
|
name.from_xml(node.xpath('md:ServiceName', Namespaces::ALL))
|
@@ -78,6 +110,21 @@ module SAML2
|
|
78
110
|
@requested_attributes = load_object_array(node, "md:RequestedAttribute", RequestedAttribute)
|
79
111
|
end
|
80
112
|
|
113
|
+
# Create an {AttributeStatement} from the given attributes hash.
|
114
|
+
#
|
115
|
+
# Given a set of attributes, create and return an {AttributeStatement}
|
116
|
+
# with only the attributes that this {AttributeConsumingService} requests.
|
117
|
+
#
|
118
|
+
# @param attributes [Hash<String => Object>, Array<Attribute>]
|
119
|
+
# If it's a hash, the elements are run through {Attribute.create} first
|
120
|
+
# in order to create proper {Attribute} objects.
|
121
|
+
# @return [AttributeStatement]
|
122
|
+
# @raise [InvalidAttributeValue]
|
123
|
+
# If a {RequestedAttribute} specifies that only specific values are
|
124
|
+
# permissible, and the provided attribute does not match that value.
|
125
|
+
# @raise [RequiredAttributeMissing]
|
126
|
+
# If a {RequestedAttribute} is tagged as required, but it has not been
|
127
|
+
# supplied.
|
81
128
|
def create_statement(attributes)
|
82
129
|
if attributes.is_a?(Hash)
|
83
130
|
attributes = attributes.map { |k, v| Attribute.create(k, v) }
|
@@ -119,6 +166,7 @@ module SAML2
|
|
119
166
|
AttributeStatement.new(attributes)
|
120
167
|
end
|
121
168
|
|
169
|
+
# (see Base#build)
|
122
170
|
def build(builder)
|
123
171
|
builder['md'].AttributeConsumingService do |attribute_consuming_service|
|
124
172
|
name.build(attribute_consuming_service)
|
data/lib/saml2/authn_request.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
2
4
|
require 'zlib'
|
3
5
|
|
@@ -16,12 +18,22 @@ module SAML2
|
|
16
18
|
attr_writer :assertion_consumer_service_index,
|
17
19
|
:assertion_consumer_service_url,
|
18
20
|
:attribute_consuming_service_index,
|
19
|
-
:force_authn,
|
20
21
|
:name_id_policy,
|
21
|
-
:passive,
|
22
22
|
:protocol_binding
|
23
|
+
# @return [Boolean, nil]
|
24
|
+
attr_writer :force_authn, :passive
|
25
|
+
# @return [RequestedAuthnContext, nil]
|
23
26
|
attr_accessor :requested_authn_context
|
24
27
|
|
28
|
+
# Initiate a SAML SSO flow, from a service provider to an identity
|
29
|
+
# provider.
|
30
|
+
# @todo go over these params, and use kwargs. Maybe pass Entity instead
|
31
|
+
# of ServiceProvider.
|
32
|
+
# @param issuer [NameID]
|
33
|
+
# @param identity_provider [IdentityProvider]
|
34
|
+
# @param assertion_consumer_service [Endpoint::Indexed]
|
35
|
+
# @param service_provider [ServiceProvider]
|
36
|
+
# @return [AuthnRequest]
|
25
37
|
def self.initiate(issuer, identity_provider = nil,
|
26
38
|
assertion_consumer_service: nil,
|
27
39
|
service_provider: nil)
|
@@ -37,6 +49,7 @@ module SAML2
|
|
37
49
|
authn_request
|
38
50
|
end
|
39
51
|
|
52
|
+
# @see https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf section 4.1
|
40
53
|
def valid_web_browser_sso_profile?
|
41
54
|
return false unless issuer
|
42
55
|
return false if issuer.format && issuer.format != NameID::Format::ENTITY
|
@@ -44,6 +57,7 @@ module SAML2
|
|
44
57
|
true
|
45
58
|
end
|
46
59
|
|
60
|
+
# @see https://saml2int.org/profile/current/#section82
|
47
61
|
def valid_interoperable_profile?
|
48
62
|
# It's a subset of Web Browser SSO profile
|
49
63
|
return false unless valid_web_browser_sso_profile?
|
@@ -55,6 +69,14 @@ module SAML2
|
|
55
69
|
true
|
56
70
|
end
|
57
71
|
|
72
|
+
# Populate {#assertion_consumer_service} and {#attribute_consuming_service}
|
73
|
+
# attributes.
|
74
|
+
#
|
75
|
+
# Given {ServiceProvider} metadata, resolve the index/urls in this object to actual
|
76
|
+
# objects.
|
77
|
+
#
|
78
|
+
# @param service_provider [ServiceProvider]
|
79
|
+
# @return [Boolean]
|
58
80
|
def resolve(service_provider)
|
59
81
|
# TODO: check signature if present
|
60
82
|
|
@@ -71,6 +93,7 @@ module SAML2
|
|
71
93
|
true
|
72
94
|
end
|
73
95
|
|
96
|
+
# @return [NameID::Policy, nil]
|
74
97
|
def name_id_policy
|
75
98
|
if xml && !instance_variable_defined?(:@name_id_policy)
|
76
99
|
@name_id_policy = NameID::Policy.from_xml(xml.at_xpath('samlp:NameIDPolicy', Namespaces::ALL))
|
@@ -78,8 +101,14 @@ module SAML2
|
|
78
101
|
@name_id_policy
|
79
102
|
end
|
80
103
|
|
81
|
-
|
104
|
+
# Must call {#resolve} before accessing.
|
105
|
+
# @return [AssertionConsumerService, nil]
|
106
|
+
attr_reader :assertion_consumer_service
|
107
|
+
# Must call {#resolve} before accessing.
|
108
|
+
# @return [AttributeConsumingService, nil]
|
109
|
+
attr_reader :attribute_consuming_service
|
82
110
|
|
111
|
+
# @return [Integer, nil]
|
83
112
|
def assertion_consumer_service_index
|
84
113
|
if xml && !instance_variable_defined?(:@assertion_consumer_service_index)
|
85
114
|
@assertion_consumer_service_index = xml['AssertionConsumerServiceIndex']&.to_i
|
@@ -87,6 +116,7 @@ module SAML2
|
|
87
116
|
@assertion_consumer_service_index
|
88
117
|
end
|
89
118
|
|
119
|
+
# @return [String, nil]
|
90
120
|
def assertion_consumer_service_url
|
91
121
|
if xml && !instance_variable_defined?(:@assertion_consumer_service_url)
|
92
122
|
@assertion_consumer_service_url = xml['AssertionConsumerServiceURL']
|
@@ -94,6 +124,7 @@ module SAML2
|
|
94
124
|
@assertion_consumer_service_url
|
95
125
|
end
|
96
126
|
|
127
|
+
# @return [Integer, nil]
|
97
128
|
def attribute_consuming_service_index
|
98
129
|
if xml && !instance_variable_defined?(:@attribute_consuming_service_index)
|
99
130
|
@attribute_consuming_service_index = xml['AttributeConsumingServiceIndex']&.to_i
|
@@ -101,6 +132,7 @@ module SAML2
|
|
101
132
|
@attribute_consuming_service_index
|
102
133
|
end
|
103
134
|
|
135
|
+
# @return [true, false, nil]
|
104
136
|
def force_authn?
|
105
137
|
if xml && !instance_variable_defined?(:@force_authn)
|
106
138
|
@force_authn = xml['ForceAuthn']&.== 'true'
|
@@ -108,6 +140,7 @@ module SAML2
|
|
108
140
|
@force_authn
|
109
141
|
end
|
110
142
|
|
143
|
+
# @return [true, false, nil]
|
111
144
|
def passive?
|
112
145
|
if xml && !instance_variable_defined?(:@passive)
|
113
146
|
@passive = xml['IsPassive']&.== 'true'
|
@@ -115,6 +148,7 @@ module SAML2
|
|
115
148
|
@passive
|
116
149
|
end
|
117
150
|
|
151
|
+
# @return [String, nil]
|
118
152
|
def protocol_binding
|
119
153
|
if xml && !instance_variable_defined?(:@protocol_binding)
|
120
154
|
@protocol_binding = xml['ProtocolBinding']
|
@@ -122,6 +156,7 @@ module SAML2
|
|
122
156
|
@protocol_binding
|
123
157
|
end
|
124
158
|
|
159
|
+
# @return [Subject, nil]
|
125
160
|
def subject
|
126
161
|
if xml && !instance_variable_defined?(:@subject)
|
127
162
|
@subject = Subject.from_xml(xml.at_xpath('saml:Subject', Namespaces::ALL))
|
@@ -129,6 +164,7 @@ module SAML2
|
|
129
164
|
@subject
|
130
165
|
end
|
131
166
|
|
167
|
+
# (see Base#build)
|
132
168
|
def build(builder)
|
133
169
|
builder['samlp'].AuthnRequest(
|
134
170
|
'xmlns:samlp' => Namespaces::SAMLP,
|