saml2 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +13 -0
- data/app/views/saml2/http_post.html.erb +14 -0
- data/lib/saml2.rb +9 -0
- data/lib/saml2/assertion.rb +37 -0
- data/lib/saml2/attribute.rb +127 -0
- data/lib/saml2/attribute/x500.rb +79 -0
- data/lib/saml2/attribute_consuming_service.rb +76 -0
- data/lib/saml2/authn_request.rb +116 -0
- data/lib/saml2/authn_statement.rb +26 -0
- data/lib/saml2/base.rb +53 -0
- data/lib/saml2/contact.rb +50 -0
- data/lib/saml2/endpoint.rb +46 -0
- data/lib/saml2/engine.rb +4 -0
- data/lib/saml2/entity.rb +84 -0
- data/lib/saml2/identity_provider.rb +57 -0
- data/lib/saml2/indexed_object.rb +59 -0
- data/lib/saml2/key.rb +46 -0
- data/lib/saml2/name_id.rb +60 -0
- data/lib/saml2/namespaces.rb +21 -0
- data/lib/saml2/organization.rb +74 -0
- data/lib/saml2/organization_and_contacts.rb +35 -0
- data/lib/saml2/profiles.rb +7 -0
- data/lib/saml2/response.rb +92 -0
- data/lib/saml2/role.rb +53 -0
- data/lib/saml2/schemas.rb +18 -0
- data/lib/saml2/service_provider.rb +30 -0
- data/lib/saml2/sso.rb +36 -0
- data/lib/saml2/subject.rb +49 -0
- data/lib/saml2/version.rb +3 -0
- data/schemas/saml-schema-assertion-2.0.xsd +283 -0
- data/schemas/saml-schema-metadata-2.0.xsd +339 -0
- data/schemas/saml-schema-protocol-2.0.xsd +302 -0
- data/schemas/xenc-schema.xsd +136 -0
- data/schemas/xml.xsd +287 -0
- data/schemas/xmldsig-core-schema.xsd +309 -0
- data/spec/fixtures/authnrequest.xml +12 -0
- data/spec/fixtures/calculated.txt +1 -0
- data/spec/fixtures/certificate.pem +25 -0
- data/spec/fixtures/entities.xml +13 -0
- data/spec/fixtures/privatekey.key +27 -0
- data/spec/fixtures/response_signed.xml +47 -0
- data/spec/fixtures/response_with_attribute_signed.xml +47 -0
- data/spec/fixtures/service_provider.xml +79 -0
- data/spec/fixtures/xmlsec.txt +1 -0
- data/spec/lib/attribute_consuming_service_spec.rb +74 -0
- data/spec/lib/attribute_spec.rb +39 -0
- data/spec/lib/authn_request_spec.rb +52 -0
- data/spec/lib/entity_spec.rb +45 -0
- data/spec/lib/identity_provider_spec.rb +23 -0
- data/spec/lib/indexed_object_spec.rb +38 -0
- data/spec/lib/response_spec.rb +60 -0
- data/spec/lib/service_provider_spec.rb +30 -0
- data/spec/spec_helper.rb +6 -0
- metadata +191 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e6cca9457d5ec3880dd5e3972e96c9275973e3b1
|
4
|
+
data.tar.gz: 2c2276cd951f78f8d6fb34eb0322e019a1de152b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cdb33be918597518b11aa3ebfb3e8ddcbb788f517dd53282e24305d71070398bcc9ff0d4045c1ec0e61dafbe42d7866a89ef2ed16cfd97219a13d139beac640d
|
7
|
+
data.tar.gz: 74e237a2935f8b8cef4bf8f32a768d10947f8eee1bcd5f6c84df0fdf61318f1d5d7a6e73a185c66ce6d8c48992d1ba83091d04f4f545ce3350192d0d26907f36
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
6
|
+
</head>
|
7
|
+
<body onload="document.forms[0].submit();" style="visibility:hidden;">
|
8
|
+
<%= form_tag(@saml_acs_url) do %>
|
9
|
+
<%= hidden_field_tag("SAMLResponse", @saml_response) %>
|
10
|
+
<%= hidden_field_tag("RelayState", @relay_state) if @relay_state %>
|
11
|
+
<%= submit_tag "Submit" %>
|
12
|
+
<% end %>
|
13
|
+
</body>
|
14
|
+
</html>
|
data/lib/saml2.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module SAML2
|
2
|
+
class Assertion
|
3
|
+
attr_reader :id, :issue_instant, :statements
|
4
|
+
attr_accessor :issuer, :subject
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@id = "_#{SecureRandom.uuid}"
|
8
|
+
@issue_instant = Time.now.utc
|
9
|
+
@statements = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def sign(x509_certificate, private_key, algorithm_name = :sha256)
|
13
|
+
to_xml
|
14
|
+
|
15
|
+
@xml.set_id_attribute('ID')
|
16
|
+
@xml.sign!(cert: x509_certificate, key: private_key, digest_alg: algorithm_name.to_s, signature_alg: "rsa-#{algorithm_name}", uri: "##{id}")
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_xml
|
21
|
+
@xml ||= Nokogiri::XML::Builder.new do |builder|
|
22
|
+
builder['saml'].Assertion(
|
23
|
+
'xmlns:saml' => Namespaces::SAML,
|
24
|
+
ID: id,
|
25
|
+
Version: '2.0',
|
26
|
+
IssueInstant: issue_instant.iso8601
|
27
|
+
) do |builder|
|
28
|
+
issuer.build(builder, element: 'Issuer')
|
29
|
+
|
30
|
+
subject.build(builder)
|
31
|
+
|
32
|
+
statements.each { |stmt| stmt.build(builder) }
|
33
|
+
end
|
34
|
+
end.doc.root
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'saml2/base'
|
2
|
+
require 'saml2/namespaces'
|
3
|
+
|
4
|
+
module SAML2
|
5
|
+
class AttributeType < Base
|
6
|
+
attr_accessor :name, :friendly_name, :name_format
|
7
|
+
|
8
|
+
def initialize(name = nil, friendly_name = nil, name_format = nil)
|
9
|
+
@name, @friendly_name, @name_format = name, friendly_name, name_format
|
10
|
+
end
|
11
|
+
|
12
|
+
def from_xml(node)
|
13
|
+
@name = node['Name']
|
14
|
+
@friendly_name = node['FriendlyName']
|
15
|
+
@name_format = node['NameFormat']
|
16
|
+
self
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Attribute < AttributeType
|
21
|
+
module NameFormats
|
22
|
+
BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic".freeze
|
23
|
+
UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified".freeze
|
24
|
+
URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri".freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def subclasses
|
29
|
+
@subclasses ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
def inherited(klass)
|
33
|
+
subclasses << klass
|
34
|
+
end
|
35
|
+
|
36
|
+
def from_xml(node)
|
37
|
+
# pass through for subclasses
|
38
|
+
super unless self == Attribute
|
39
|
+
|
40
|
+
# look for an appropriate subclass
|
41
|
+
klass = subclasses.find { |klass| klass.recognizes?(node) }
|
42
|
+
if klass
|
43
|
+
klass.from_xml(node)
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def create(name, value = nil)
|
50
|
+
klass = subclasses.find { |klass| klass.recognizes?(name) } || self
|
51
|
+
klass.new(name, value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_accessor :value
|
56
|
+
|
57
|
+
def initialize(name = nil, value = nil, friendly_name = nil, name_format = nil)
|
58
|
+
super(name, friendly_name, name_format)
|
59
|
+
@value = value
|
60
|
+
end
|
61
|
+
|
62
|
+
def build(builder)
|
63
|
+
builder['saml'].Attribute('Name' => name) do |builder|
|
64
|
+
builder.parent['FriendlyName'] = friendly_name if friendly_name
|
65
|
+
builder.parent['NameFormat'] = name_format if name_format
|
66
|
+
Array(value).each do |val|
|
67
|
+
xsi_type, val = convert_to_xsi(value)
|
68
|
+
builder['saml'].AttributeValue(val) do |builder|
|
69
|
+
builder.parent['xsi:type'] = xsi_type if xsi_type
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def from_xml(node)
|
76
|
+
@value = node.xpath('saml:AttributeValue', Namespaces::ALL).map do |node|
|
77
|
+
convert_from_xsi(node['xsi:type'], node.content && node.content.strip)
|
78
|
+
end
|
79
|
+
@value = @value.first if @value.length == 1
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
XSI_TYPES = {
|
85
|
+
'xsd:string' => [String, nil, nil],
|
86
|
+
nil => [DateTime, ->(v) { v.iso8601 }, ->(v) { DateTime.parse(v) if v }]
|
87
|
+
}.freeze
|
88
|
+
|
89
|
+
def convert_to_xsi(value)
|
90
|
+
xsi_type = nil
|
91
|
+
converter = nil
|
92
|
+
XSI_TYPES.each do |type, (klass, to_xsi, from_xsi)|
|
93
|
+
if klass === value
|
94
|
+
xsi_type = type
|
95
|
+
converter = to_xsi
|
96
|
+
break
|
97
|
+
end
|
98
|
+
end
|
99
|
+
value = converter.call(value) if converter
|
100
|
+
[xsi_type, value]
|
101
|
+
end
|
102
|
+
|
103
|
+
def convert_from_xsi(type, value)
|
104
|
+
info = XSI_TYPES[type]
|
105
|
+
if info && info.last
|
106
|
+
value = info.last.call(value)
|
107
|
+
end
|
108
|
+
value
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class AttributeStatement
|
113
|
+
attr_reader :attributes
|
114
|
+
|
115
|
+
def initialize(attributes)
|
116
|
+
@attributes = attributes
|
117
|
+
end
|
118
|
+
|
119
|
+
def build(builder)
|
120
|
+
builder['saml'].AttributeStatement('xmlns:xsi' => Namespaces::XSI) do |builder|
|
121
|
+
@attributes.each { |attr| attr.build(builder) }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
require 'saml2/attribute/x500'
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module SAML2
|
2
|
+
class Attribute
|
3
|
+
class X500 < Attribute
|
4
|
+
GIVEN_NAME = 'urn:oid:2.5.4.42'.freeze
|
5
|
+
SN = SURNAME = 'urn:oid:2.5.4.4'.freeze
|
6
|
+
# https://www.ietf.org/rfc/rfc2798.txt
|
7
|
+
module InetOrgPerson
|
8
|
+
DISPLAY_NAME = 'urn:oid:2.16.840.1.113730.3.1.241'.freeze
|
9
|
+
EMPLOYEE_NUMBER = 'urn:oid:2.16.840.1.113730.3.1.3'.freeze
|
10
|
+
EMPLOYEE_TYPE = 'urn:oid:2.16.840.1.113730.3.1.4'.freeze
|
11
|
+
PREFERRED_LANGUAGE = 'urn:oid:2.16.840.1.113730.3.1.39'.freeze
|
12
|
+
end
|
13
|
+
# https://www.internet2.edu/media/medialibrary/2013/09/04/internet2-mace-dir-eduperson-201203.html
|
14
|
+
module EduPerson
|
15
|
+
AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1'.freeze
|
16
|
+
ASSURANCE = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.11'.freeze
|
17
|
+
ENTITLEMENT = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'.freeze
|
18
|
+
NICKNAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.2'.freeze
|
19
|
+
ORG_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.3'.freeze
|
20
|
+
PRIMARY_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.5'.freeze
|
21
|
+
PRIMARY_ORG_UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.8'.freeze
|
22
|
+
PRINCIPAL_NAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6'.freeze
|
23
|
+
SCOPED_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9'.freeze
|
24
|
+
TARGETED_I_D = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10'.freeze
|
25
|
+
UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.4'.freeze
|
26
|
+
end
|
27
|
+
# http://www.ietf.org/rfc/rfc4524.txt
|
28
|
+
MAIL = 'urn:oid:0.9.2342.19200300.100.1.3'.freeze
|
29
|
+
|
30
|
+
def self.recognizes?(name_or_node)
|
31
|
+
if name_or_node.is_a?(Nokogiri::XML::Element)
|
32
|
+
!!name_or_node.at_xpath("@x500:Encoding", Namespaces::ALL)
|
33
|
+
else
|
34
|
+
FRIENDLY_NAMES.include?(name_or_node) || OIDS.include?(name_or_node)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(name = nil, value = nil)
|
39
|
+
# if they pass an OID, infer the friendly name
|
40
|
+
friendly_name = OIDS[name]
|
41
|
+
unless friendly_name
|
42
|
+
# if they pass a friendly name, infer the OID
|
43
|
+
proper_name = FRIENDLY_NAMES[name]
|
44
|
+
if proper_name
|
45
|
+
name, friendly_name = proper_name, name
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
super(name, value, friendly_name, NameFormats::URI)
|
50
|
+
end
|
51
|
+
|
52
|
+
def build(builder)
|
53
|
+
super
|
54
|
+
attr = builder.parent.last_element_child
|
55
|
+
attr.add_namespace_definition('x500', Namespaces::X500)
|
56
|
+
attr['x500:Encoding'] = 'LDAP'
|
57
|
+
end
|
58
|
+
|
59
|
+
# build hashes out of our known attribute names for quick lookup
|
60
|
+
FRIENDLY_NAMES = ([self] + constants).inject({}) do |hash, mod|
|
61
|
+
mod = const_get(mod) unless mod.is_a?(Module)
|
62
|
+
next hash unless mod.is_a?(Module)
|
63
|
+
# Don't look in modules inherited from parent classes
|
64
|
+
next hash unless mod.name.start_with?(self.name)
|
65
|
+
mod.constants.each do |key|
|
66
|
+
value = mod.const_get(key)
|
67
|
+
next unless value.is_a?(String)
|
68
|
+
key = key.to_s.downcase.gsub(/_\w/) { |c| c[1].upcase }
|
69
|
+
# eduPerson prefixes all of their names
|
70
|
+
key = "eduPerson#{key.sub(/^\w/) { |c| c.upcase }}" if mod == EduPerson
|
71
|
+
hash[key] = value
|
72
|
+
end
|
73
|
+
hash
|
74
|
+
end.freeze
|
75
|
+
OIDS = FRIENDLY_NAMES.invert.freeze
|
76
|
+
private_constant :FRIENDLY_NAMES, :OIDS
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'saml2/attribute'
|
2
|
+
require 'saml2/indexed_object'
|
3
|
+
require 'saml2/namespaces'
|
4
|
+
|
5
|
+
module SAML2
|
6
|
+
class RequestedAttribute < AttributeType
|
7
|
+
def initialize(name = nil, is_required = nil, name_format = nil)
|
8
|
+
super(name, name_format)
|
9
|
+
@is_required = is_required
|
10
|
+
end
|
11
|
+
|
12
|
+
def from_xml(node)
|
13
|
+
@is_required = node['isRequired'] && node['isRequired'] == 'true'
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def required?
|
18
|
+
@is_required
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class RequiredAttributeMissing < RuntimeError
|
23
|
+
attr_reader :requested_attribute
|
24
|
+
|
25
|
+
def initialize(requested_attribute)
|
26
|
+
super("Required attribute #{requested_attribute.name} not provided")
|
27
|
+
@requested_attribute = requested_attribute
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class AttributeConsumingService < Base
|
32
|
+
include IndexedObject
|
33
|
+
|
34
|
+
attr_reader :name, :requested_attributes
|
35
|
+
|
36
|
+
def initialize(name = nil, requested_attributes = [])
|
37
|
+
@name, @requested_attributes = name, requested_attributes
|
38
|
+
end
|
39
|
+
|
40
|
+
def from_xml(node)
|
41
|
+
@name = node['ServiceName']
|
42
|
+
@requested_attributes = load_object_array(node, "md:RequestedAttribute", RequestedAttribute)
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_statement(attributes)
|
47
|
+
if attributes.is_a?(Hash)
|
48
|
+
attributes = attributes.map { |k, v| Attribute.create(k, v) }
|
49
|
+
end
|
50
|
+
|
51
|
+
attributes_hash = {}
|
52
|
+
attributes.each do |attr|
|
53
|
+
attr.value = attr.value.call if attr.value.respond_to?(:call)
|
54
|
+
attributes_hash[[attr.name, attr.name_format]] = attr
|
55
|
+
if attr.name_format
|
56
|
+
attributes_hash[[attr.name, nil]] = attr
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
attributes = []
|
61
|
+
requested_attributes.each do |requested_attr|
|
62
|
+
attr = attributes_hash[[requested_attr.name, requested_attr.name_format]]
|
63
|
+
if requested_attr.name_format
|
64
|
+
attr ||= attributes_hash[[requested_attr.name, nil]]
|
65
|
+
end
|
66
|
+
if attr
|
67
|
+
attributes << attr
|
68
|
+
elsif requested_attr.required?
|
69
|
+
raise RequiredAttributeMissing.new(requested_attr)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
return nil if attributes.empty?
|
73
|
+
AttributeStatement.new(attributes)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
require 'saml2/attribute_consuming_service'
|
5
|
+
require 'saml2/endpoint'
|
6
|
+
require 'saml2/name_id'
|
7
|
+
require 'saml2/namespaces'
|
8
|
+
require 'saml2/schemas'
|
9
|
+
require 'saml2/subject'
|
10
|
+
|
11
|
+
module SAML2
|
12
|
+
class AuthnRequest
|
13
|
+
def self.decode(authnrequest)
|
14
|
+
begin
|
15
|
+
zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
16
|
+
authnrequest = zstream.inflate(Base64.decode64(authnrequest))
|
17
|
+
zstream.finish
|
18
|
+
zstream.close
|
19
|
+
rescue Zlib::BufError
|
20
|
+
end
|
21
|
+
parse(authnrequest)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.parse(authnrequest)
|
25
|
+
new(Nokogiri::XML(authnrequest))
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(document)
|
29
|
+
@document = document
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid_schema?
|
33
|
+
return false unless Schemas.protocol.valid?(@document)
|
34
|
+
# Check for the correct root element
|
35
|
+
return false unless @document.at_xpath('/samlp:AuthnRequest', Namespaces::ALL)
|
36
|
+
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def valid_web_browser_sso_profile?
|
41
|
+
return false unless issuer
|
42
|
+
return false if issuer.format && issuer.format != NameID::Format::ENTITY
|
43
|
+
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid_interoperable_profile?
|
48
|
+
# It's a subset of Web Browser SSO profile
|
49
|
+
return false unless valid_web_browser_sso_profile?
|
50
|
+
|
51
|
+
return false unless assertion_consumer_service_url
|
52
|
+
return false if protocol_binding && protocol_binding != Endpoint::Bindings::HTTP_POST
|
53
|
+
return false if subject
|
54
|
+
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
def resolve(service_provider)
|
59
|
+
# TODO: check signature if present
|
60
|
+
|
61
|
+
if assertion_consumer_service_url
|
62
|
+
@assertion_consumer_service = service_provider.assertion_consumer_services.find { |acs| acs.location == assertion_consumer_service_url }
|
63
|
+
else
|
64
|
+
@assertion_consumer_service = service_provider.assertion_consumer_services.resolve(assertion_consumer_service_index)
|
65
|
+
end
|
66
|
+
@attribute_consuming_service = service_provider.attribute_consuming_services.resolve(attribute_consuming_service_index)
|
67
|
+
|
68
|
+
return false unless @assertion_consumer_service
|
69
|
+
return false if attribute_consuming_service_index && !@attribute_consuming_service
|
70
|
+
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def issuer
|
75
|
+
@issuer ||= NameID.from_xml(@document.root.at_xpath('saml:Issuer', Namespaces::ALL))
|
76
|
+
end
|
77
|
+
|
78
|
+
def name_id_policy
|
79
|
+
@name_id_policy ||= NameID::Policy.from_xml(@document.root.at_xpath('samlp:NameIDPolicy', Namespaces::ALL))
|
80
|
+
end
|
81
|
+
|
82
|
+
def id
|
83
|
+
@document.root['ID']
|
84
|
+
end
|
85
|
+
|
86
|
+
attr_reader :assertion_consumer_service, :attribute_consuming_service
|
87
|
+
|
88
|
+
def assertion_consumer_service_url
|
89
|
+
@document.root['AssertionConsumerServiceURL']
|
90
|
+
end
|
91
|
+
|
92
|
+
def assertion_consumer_service_index
|
93
|
+
@document.root['AssertionConsumerServiceIndex'] && @document.root['AssertionConsumerServiceIndex'].to_i
|
94
|
+
end
|
95
|
+
|
96
|
+
def attribute_consuming_service_index
|
97
|
+
@document.root['AttributeConsumerServiceIndex'] && @document.root['AttributeConsumerServiceIndex'].to_i
|
98
|
+
end
|
99
|
+
|
100
|
+
def force_authn?
|
101
|
+
@document.root['ForceAuthn']
|
102
|
+
end
|
103
|
+
|
104
|
+
def passive?
|
105
|
+
@document.root['IsPassive']
|
106
|
+
end
|
107
|
+
|
108
|
+
def protocol_binding
|
109
|
+
@document.root['ProtocolBinding']
|
110
|
+
end
|
111
|
+
|
112
|
+
def subject
|
113
|
+
@subject ||= Subject.from_xml(@document.at_xpath('saml:Subject', Namespaces::ALL))
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|