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
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,
|