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
@@ -0,0 +1,26 @@
|
|
1
|
+
module SAML2
|
2
|
+
class AuthnStatement
|
3
|
+
module Classes
|
4
|
+
INTERNET_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol".freeze # IP address
|
5
|
+
INTERNET_PROTOCOL_PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword".freeze # IP address, as well as username/password
|
6
|
+
KERBEROS = "urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos".freeze
|
7
|
+
PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password".freeze # username/password, NOT over SSL
|
8
|
+
PASSWORD_PROTECTED_TRANSPORT = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport".freeze # username/password over SSL
|
9
|
+
PREVIOUS_SESSION = "urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession".freeze # remember me
|
10
|
+
SMARTCARD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard".freeze
|
11
|
+
SMARTCARD_PKI = "urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI".freeze # smartcard with a private key on it
|
12
|
+
TLS_CLIENT = "urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient".freeze # SSL client certificate
|
13
|
+
UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified".freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :authn_instant, :authn_context_class_ref
|
17
|
+
|
18
|
+
def build(builder)
|
19
|
+
builder['saml'].AuthnStatement('AuthnInstant' => authn_instant.iso8601) do |builder|
|
20
|
+
builder['saml'].AuthnContext do |builder|
|
21
|
+
builder['saml'].AuthnContextClassRef(authn_context_class_ref) if authn_context_class_ref
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/saml2/base.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'saml2/namespaces'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
class Base
|
5
|
+
def self.from_xml(node)
|
6
|
+
return nil unless node
|
7
|
+
new.from_xml(node)
|
8
|
+
end
|
9
|
+
|
10
|
+
def from_xml(node)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
# make sure to not FORMAT it - it breaks signatures!
|
16
|
+
to_xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_xml
|
20
|
+
unless @document
|
21
|
+
builder = Nokogiri::XML::Builder.new
|
22
|
+
build(builder)
|
23
|
+
@document = builder.doc
|
24
|
+
end
|
25
|
+
@document
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.load_string_array(node, element)
|
29
|
+
node.xpath(element, Namespaces::ALL).map do |node|
|
30
|
+
node.content && node.content.strip
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.load_object_array(node, element, klass)
|
35
|
+
node.xpath(element, Namespaces::ALL).map do |node|
|
36
|
+
if klass.is_a?(Hash)
|
37
|
+
klass[node.name].from_xml(node)
|
38
|
+
else
|
39
|
+
klass.from_xml(node)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
def load_string_array(node, element)
|
46
|
+
self.class.load_string_array(node, element)
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_object_array(node, element, klass)
|
50
|
+
self.class.load_object_array(node, element, klass)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'saml2/base'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
class Contact
|
5
|
+
module Type
|
6
|
+
ADMINISTRATIVE = 'administrative'.freeze
|
7
|
+
BILLING = 'billing'.freeze
|
8
|
+
OTHER = 'other'.freeze
|
9
|
+
SUPPORT = 'support'.freeze
|
10
|
+
TECHNICAL = 'technical'.freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :type, :company, :given_name, :surname, :email_addresses, :telephone_numbers
|
14
|
+
|
15
|
+
def self.from_xml(node)
|
16
|
+
return nil unless node
|
17
|
+
|
18
|
+
result = new(node['contactType'])
|
19
|
+
company = node.at_xpath('md:Company', Namespaces::ALL)
|
20
|
+
result.company = company && company.content && company.content.strip
|
21
|
+
given_name = node.at_xpath('md:GivenName', Namespaces::ALL)
|
22
|
+
result.given_name = given_name && given_name.content && given_name.content.strip
|
23
|
+
surname = node.at_xpath('md:SurName', Namespaces::ALL)
|
24
|
+
result.surname = surname && surname.content && surname.content.strip
|
25
|
+
result.email_addresses = Base.load_string_array(node, 'md:EmailAddress')
|
26
|
+
result.telephone_numbers = Base.load_string_array(node, 'md:TelephoneNumber')
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(type = Type::OTHER)
|
31
|
+
@type = type
|
32
|
+
@email_addresses = []
|
33
|
+
@telephone_numbers = []
|
34
|
+
end
|
35
|
+
|
36
|
+
def build(builder)
|
37
|
+
builder['md'].ContactPerson('contactType' => type) do |builder|
|
38
|
+
builder['md'].Company(company) if company
|
39
|
+
builder['md'].GivenName(given_name) if given_name
|
40
|
+
builder['md'].SurName(surname) if surname
|
41
|
+
email_addresses.each do |email|
|
42
|
+
builder['md'].EmailAddress(email)
|
43
|
+
end
|
44
|
+
telephone_numbers.each do |tel|
|
45
|
+
builder['md'].TelephoneNumber(tel)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module SAML2
|
2
|
+
class Endpoint < Base
|
3
|
+
module Bindings
|
4
|
+
HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze
|
5
|
+
HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze
|
6
|
+
end
|
7
|
+
|
8
|
+
module Encodings
|
9
|
+
DEFLATE= "urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE".freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :location, :binding
|
13
|
+
|
14
|
+
def initialize(location, binding = Bindings::HTTP_POST)
|
15
|
+
@location, @binding = location, binding
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(rhs)
|
19
|
+
location == rhs.location && binding == rhs.binding
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_xml(node)
|
23
|
+
@location = node['Location']
|
24
|
+
@binding = node['Binding']
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def build(builder, element)
|
29
|
+
builder['md'].__send__(element, 'Location' => location, 'Binding' => binding)
|
30
|
+
end
|
31
|
+
|
32
|
+
class Indexed < Endpoint
|
33
|
+
include IndexedObject
|
34
|
+
|
35
|
+
def initialize(location = nil, index = nil, is_default = false, binding = Bindings::HTTP_POST)
|
36
|
+
super(location, binding)
|
37
|
+
@index, @is_default = index, is_default
|
38
|
+
end
|
39
|
+
|
40
|
+
def eql?(rhs)
|
41
|
+
location == rhs.location &&
|
42
|
+
binding == rhs.binding && super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/saml2/engine.rb
ADDED
data/lib/saml2/entity.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
require 'saml2/base'
|
4
|
+
require 'saml2/identity_provider'
|
5
|
+
require 'saml2/organization_and_contacts'
|
6
|
+
require 'saml2/service_provider'
|
7
|
+
|
8
|
+
module SAML2
|
9
|
+
class Entity < Base
|
10
|
+
include OrganizationAndContacts
|
11
|
+
|
12
|
+
attr_writer :entity_id
|
13
|
+
|
14
|
+
def self.parse(xml)
|
15
|
+
document = Nokogiri::XML(xml)
|
16
|
+
|
17
|
+
# Root can be an array (EntitiesDescriptor), or a single Entity (EntityDescriptor)
|
18
|
+
entities = document.at_xpath("/md:EntitiesDescriptor", Namespaces::ALL)
|
19
|
+
entity = document.at_xpath("/md:EntityDescriptor", Namespaces::ALL)
|
20
|
+
if entities
|
21
|
+
Group.from_xml(entities)
|
22
|
+
elsif entity
|
23
|
+
from_xml(entity)
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Group < Array
|
30
|
+
def self.from_xml(node)
|
31
|
+
node && new(node)
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(root)
|
35
|
+
@root = root
|
36
|
+
replace(Base.load_object_array(@root, "md:EntityDescriptor|md:EntitiesDescriptor",
|
37
|
+
'EntityDescriptor' => Entity,
|
38
|
+
'EntitiesDescriptor' => Group))
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid_schema?
|
42
|
+
Schemas.metadata.valid?(@root.document)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.from_xml(node)
|
47
|
+
node && new(node)
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize(root = nil)
|
51
|
+
super
|
52
|
+
@root = root
|
53
|
+
unless @root
|
54
|
+
@roles = []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid_schema?
|
59
|
+
Schemas.metadata.valid?(@root.document)
|
60
|
+
end
|
61
|
+
|
62
|
+
def entity_id
|
63
|
+
@entity_id || @root && @root['entityID']
|
64
|
+
end
|
65
|
+
|
66
|
+
def roles
|
67
|
+
# TODO: load IdPSSODescriptors as well
|
68
|
+
@roles ||= load_object_array(@root, 'md:SPSSODescriptor', ServiceProvider)
|
69
|
+
end
|
70
|
+
|
71
|
+
def build(builder)
|
72
|
+
builder['md'].EntityDescriptor('entityID' => entity_id,
|
73
|
+
'xmlns:md' => Namespaces::METADATA,
|
74
|
+
'xmlns:dsig' => Namespaces::DSIG,
|
75
|
+
'xmlns:xenc' => Namespaces::XENC) do |builder|
|
76
|
+
roles.each do |role|
|
77
|
+
role.build(builder)
|
78
|
+
end
|
79
|
+
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'saml2/attribute'
|
2
|
+
require 'saml2/sso'
|
3
|
+
|
4
|
+
module SAML2
|
5
|
+
class IdentityProvider < SSO
|
6
|
+
attr_writer :want_authn_requests_signed, :single_sign_on_services, :attribute_profiles, :attributes
|
7
|
+
|
8
|
+
def initialize(node = nil)
|
9
|
+
super(node)
|
10
|
+
unless node
|
11
|
+
@want_authn_requests_signed = nil
|
12
|
+
@single_sign_on_services = []
|
13
|
+
@attribute_profiles = []
|
14
|
+
@attributes = []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def want_authn_requests_signed?
|
19
|
+
unless instance_variable_defined?(:@want_authn_requests_signed)
|
20
|
+
@want_authn_requests_signed = @root['WantAuthnRequestsSigned'] && @root['WantAuthnRequestsSigned'] == 'true'
|
21
|
+
end
|
22
|
+
@want_authn_requests_signed
|
23
|
+
end
|
24
|
+
|
25
|
+
def single_sign_on_services
|
26
|
+
@single_sign_on_services ||= load_object_array(@root, 'md:SingleSignOnService', Endpoint)
|
27
|
+
end
|
28
|
+
|
29
|
+
def attribute_profiles
|
30
|
+
@attribute_profiles ||= load_string_array(@root, 'md:AttributeProfile')
|
31
|
+
end
|
32
|
+
|
33
|
+
def attributes
|
34
|
+
@attributes ||= load_object_array(@root, 'saml:Attribute', Attribute)
|
35
|
+
end
|
36
|
+
|
37
|
+
def build(builder)
|
38
|
+
builder['md'].IDPSSODescriptor do |builder|
|
39
|
+
super(builder)
|
40
|
+
|
41
|
+
builder['WantAuthnRequestsSigned'] = want_authn_requests_signed? unless want_authn_requests_signed?.nil?
|
42
|
+
|
43
|
+
single_sign_on_services.each do |sso|
|
44
|
+
sso.build(builder, 'SingleSignOnService')
|
45
|
+
end
|
46
|
+
|
47
|
+
attribute_profiles.each do |ap|
|
48
|
+
builder['md'].AttributeProfile(ap)
|
49
|
+
end
|
50
|
+
|
51
|
+
attributes.each do |attr|
|
52
|
+
attr.build(builder)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'saml2/base'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
module IndexedObject
|
5
|
+
attr_reader :index
|
6
|
+
|
7
|
+
def eql?(rhs)
|
8
|
+
index == rhs.index &&
|
9
|
+
default? == rhs.default? &&
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def default?
|
14
|
+
@is_default
|
15
|
+
end
|
16
|
+
|
17
|
+
def from_xml(node)
|
18
|
+
@index = node['index'] && node['index'].to_i
|
19
|
+
@is_default = node['isDefault'] && node['isDefault'] == 'true'
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
class Array < ::Array
|
24
|
+
attr_reader :default
|
25
|
+
|
26
|
+
def self.from_xml(nodes)
|
27
|
+
new(nodes.map { |node| name.split('::')[1..-2].inject(SAML2) { |mod, klass| mod.const_get(klass) }.from_xml(node) })
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(objects)
|
31
|
+
replace(objects.sort_by { |object| object.index || 0 })
|
32
|
+
@index = {}
|
33
|
+
each { |object| @index[object.index] = object }
|
34
|
+
@default = find { |object| object.default? } || first
|
35
|
+
|
36
|
+
freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](index)
|
40
|
+
@index[index]
|
41
|
+
end
|
42
|
+
|
43
|
+
def resolve(index)
|
44
|
+
index ? self[index] : default
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def build(builder)
|
49
|
+
super
|
50
|
+
builder.parent.last['index'] = index
|
51
|
+
builder.parent.last['isDefault'] = default? unless default?.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def self.included(klass)
|
56
|
+
klass.const_set(:Array, Array.dup)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/saml2/key.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'saml2/namespaces'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
class Key
|
5
|
+
module Type
|
6
|
+
ENCRYPTION = 'encryption'.freeze
|
7
|
+
SIGNING = 'signing'.freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :use, :x509, :encryption_methods
|
11
|
+
|
12
|
+
def self.from_xml(node)
|
13
|
+
return nil unless node
|
14
|
+
|
15
|
+
x509 = node.at_xpath('dsig:KeyInfo/dsig:X509Data/dsig:X509Certificate', Namespaces::ALL)
|
16
|
+
methods = node.xpath('xenc:EncryptionMethod', Namespaces::ALL)
|
17
|
+
new(x509 && x509.content.strip, node['use'], methods.map { |m| m['Algorithm'] })
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(x509, use = nil, encryption_methods = [])
|
21
|
+
@use, @x509, @encryption_methods = use, x509, encryption_methods
|
22
|
+
end
|
23
|
+
|
24
|
+
def encryption?
|
25
|
+
use.nil? || use == Type::ENCRYPTION
|
26
|
+
end
|
27
|
+
|
28
|
+
def signing?
|
29
|
+
use.nil? || use == Type::SIGNING
|
30
|
+
end
|
31
|
+
|
32
|
+
def build(builder)
|
33
|
+
builder['md'].KeyDescriptor do |builder|
|
34
|
+
builder.parent['use'] = use if use
|
35
|
+
builder['dsig'].KeyInfo do |builder|
|
36
|
+
builder['dsig'].X509Data do |builder|
|
37
|
+
builder['dsig'].X509Certificate(x509)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
encryption_methods.each do |method|
|
41
|
+
builder['xenc'].EncryptionMethod('Algorithm' => method)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'saml2/namespaces'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
class NameID
|
5
|
+
module Format
|
6
|
+
EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress".freeze
|
7
|
+
ENTITY = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity".freeze
|
8
|
+
KERBEROS = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos".freeze # name[/instance]@REALM
|
9
|
+
PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent".freeze # opaque, pseudo-random, unique per SP-IdP pair
|
10
|
+
TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient".freeze # opaque, will likely change
|
11
|
+
UNSPECIFIED = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified".freeze
|
12
|
+
WINDOWS_DOMAIN_QUALIFIED_NAME = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName".freeze # [DomainName\]UserName
|
13
|
+
X509_SUBJECT_NAME = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName".freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
class Policy
|
17
|
+
attr_reader :format
|
18
|
+
|
19
|
+
def self.from_xml(node)
|
20
|
+
if node
|
21
|
+
allow_create = node['AllowCreate'].nil? ? nil : node['AllowCreate'] == 'true'
|
22
|
+
NameID::Policy.new(allow_create, node['Format'])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(allow_create, format)
|
27
|
+
@allow_create, @format = allow_create, format
|
28
|
+
end
|
29
|
+
|
30
|
+
def allow_create?
|
31
|
+
@allow_create
|
32
|
+
end
|
33
|
+
|
34
|
+
def ==(rhs)
|
35
|
+
format == rhs.format && allow_create? == rhs.allow_create?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :id, :format
|
40
|
+
|
41
|
+
def self.from_xml(node)
|
42
|
+
node && new(node.content.strip, node['Format'])
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(id = nil, format = nil)
|
46
|
+
@id, @format = id, format
|
47
|
+
end
|
48
|
+
|
49
|
+
def ==(rhs)
|
50
|
+
id == rhs.id && format == rhs.format
|
51
|
+
end
|
52
|
+
|
53
|
+
def build(builder, options = {})
|
54
|
+
args = {}
|
55
|
+
args['Format'] = format if format
|
56
|
+
args['xmlns:saml'] = Namespaces::SAML if options[:include_namespace]
|
57
|
+
builder['saml'].__send__(options.delete(:element) || 'NameID', id, args)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|