saml2 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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