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
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