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
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,13 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rake'
6
+ require 'rake/testtask'
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.name = "spec"
10
+ t.pattern = "spec/**/*_spec.rb"
11
+ end
12
+
13
+ task :default => :spec
@@ -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,9 @@
1
+ require 'saml2/authn_request'
2
+ require 'saml2/entity'
3
+ require 'saml2/response'
4
+ require 'saml2/version'
5
+
6
+ require 'saml2/engine' if defined?(::Rails) && Rails::VERSION::MAJOR > 2
7
+
8
+ module SAML2
9
+ end
@@ -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