saml2 1.0.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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +13 -0
  3. data/app/views/saml2/http_post.html.erb +14 -0
  4. data/lib/saml2.rb +9 -0
  5. data/lib/saml2/assertion.rb +37 -0
  6. data/lib/saml2/attribute.rb +127 -0
  7. data/lib/saml2/attribute/x500.rb +79 -0
  8. data/lib/saml2/attribute_consuming_service.rb +76 -0
  9. data/lib/saml2/authn_request.rb +116 -0
  10. data/lib/saml2/authn_statement.rb +26 -0
  11. data/lib/saml2/base.rb +53 -0
  12. data/lib/saml2/contact.rb +50 -0
  13. data/lib/saml2/endpoint.rb +46 -0
  14. data/lib/saml2/engine.rb +4 -0
  15. data/lib/saml2/entity.rb +84 -0
  16. data/lib/saml2/identity_provider.rb +57 -0
  17. data/lib/saml2/indexed_object.rb +59 -0
  18. data/lib/saml2/key.rb +46 -0
  19. data/lib/saml2/name_id.rb +60 -0
  20. data/lib/saml2/namespaces.rb +21 -0
  21. data/lib/saml2/organization.rb +74 -0
  22. data/lib/saml2/organization_and_contacts.rb +35 -0
  23. data/lib/saml2/profiles.rb +7 -0
  24. data/lib/saml2/response.rb +92 -0
  25. data/lib/saml2/role.rb +53 -0
  26. data/lib/saml2/schemas.rb +18 -0
  27. data/lib/saml2/service_provider.rb +30 -0
  28. data/lib/saml2/sso.rb +36 -0
  29. data/lib/saml2/subject.rb +49 -0
  30. data/lib/saml2/version.rb +3 -0
  31. data/schemas/saml-schema-assertion-2.0.xsd +283 -0
  32. data/schemas/saml-schema-metadata-2.0.xsd +339 -0
  33. data/schemas/saml-schema-protocol-2.0.xsd +302 -0
  34. data/schemas/xenc-schema.xsd +136 -0
  35. data/schemas/xml.xsd +287 -0
  36. data/schemas/xmldsig-core-schema.xsd +309 -0
  37. data/spec/fixtures/authnrequest.xml +12 -0
  38. data/spec/fixtures/calculated.txt +1 -0
  39. data/spec/fixtures/certificate.pem +25 -0
  40. data/spec/fixtures/entities.xml +13 -0
  41. data/spec/fixtures/privatekey.key +27 -0
  42. data/spec/fixtures/response_signed.xml +47 -0
  43. data/spec/fixtures/response_with_attribute_signed.xml +47 -0
  44. data/spec/fixtures/service_provider.xml +79 -0
  45. data/spec/fixtures/xmlsec.txt +1 -0
  46. data/spec/lib/attribute_consuming_service_spec.rb +74 -0
  47. data/spec/lib/attribute_spec.rb +39 -0
  48. data/spec/lib/authn_request_spec.rb +52 -0
  49. data/spec/lib/entity_spec.rb +45 -0
  50. data/spec/lib/identity_provider_spec.rb +23 -0
  51. data/spec/lib/indexed_object_spec.rb +38 -0
  52. data/spec/lib/response_spec.rb +60 -0
  53. data/spec/lib/service_provider_spec.rb +30 -0
  54. data/spec/spec_helper.rb +6 -0
  55. 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
@@ -0,0 +1,4 @@
1
+ module SAML2
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -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