saml-kit 0.1.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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +39 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/exe/saml-kit-decode-http-redirect +7 -0
  12. data/lib/saml/kit.rb +60 -0
  13. data/lib/saml/kit/authentication_request.rb +78 -0
  14. data/lib/saml/kit/binding.rb +40 -0
  15. data/lib/saml/kit/configuration.rb +36 -0
  16. data/lib/saml/kit/default_registry.rb +49 -0
  17. data/lib/saml/kit/document.rb +96 -0
  18. data/lib/saml/kit/fingerprint.rb +40 -0
  19. data/lib/saml/kit/http_post_binding.rb +27 -0
  20. data/lib/saml/kit/http_redirect_binding.rb +58 -0
  21. data/lib/saml/kit/identity_provider_metadata.rb +122 -0
  22. data/lib/saml/kit/invalid_document.rb +13 -0
  23. data/lib/saml/kit/locales/en.yml +25 -0
  24. data/lib/saml/kit/logout_request.rb +78 -0
  25. data/lib/saml/kit/logout_response.rb +63 -0
  26. data/lib/saml/kit/metadata.rb +171 -0
  27. data/lib/saml/kit/namespaces.rb +47 -0
  28. data/lib/saml/kit/requestable.rb +14 -0
  29. data/lib/saml/kit/respondable.rb +35 -0
  30. data/lib/saml/kit/response.rb +197 -0
  31. data/lib/saml/kit/self_signed_certificate.rb +30 -0
  32. data/lib/saml/kit/serializable.rb +31 -0
  33. data/lib/saml/kit/service_provider_metadata.rb +99 -0
  34. data/lib/saml/kit/signature.rb +75 -0
  35. data/lib/saml/kit/trustable.rb +62 -0
  36. data/lib/saml/kit/url_builder.rb +38 -0
  37. data/lib/saml/kit/version.rb +5 -0
  38. data/lib/saml/kit/xml.rb +57 -0
  39. data/lib/saml/kit/xsd/MetadataExchange.xsd +95 -0
  40. data/lib/saml/kit/xsd/oasis-200401-wss-wssecurity-secext-1.0.xsd +196 -0
  41. data/lib/saml/kit/xsd/oasis-200401-wss-wssecurity-utility-1.0.xsd +95 -0
  42. data/lib/saml/kit/xsd/saml-schema-assertion-2.0.xsd +283 -0
  43. data/lib/saml/kit/xsd/saml-schema-authn-context-2.0.xsd +23 -0
  44. data/lib/saml/kit/xsd/saml-schema-authn-context-types-2.0.xsd +821 -0
  45. data/lib/saml/kit/xsd/saml-schema-metadata-2.0.xsd +335 -0
  46. data/lib/saml/kit/xsd/saml-schema-protocol-2.0.xsd +302 -0
  47. data/lib/saml/kit/xsd/sstc-metadata-attr.xsd +35 -0
  48. data/lib/saml/kit/xsd/sstc-saml-attribute-ext.xsd +25 -0
  49. data/lib/saml/kit/xsd/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
  50. data/lib/saml/kit/xsd/sstc-saml-metadata-ui-v1.0.xsd +89 -0
  51. data/lib/saml/kit/xsd/ws-addr.xsd +120 -0
  52. data/lib/saml/kit/xsd/ws-authorization.xsd +145 -0
  53. data/lib/saml/kit/xsd/ws-federation.xsd +471 -0
  54. data/lib/saml/kit/xsd/ws-securitypolicy-1.2.xsd +900 -0
  55. data/lib/saml/kit/xsd/xenc-schema.xsd +136 -0
  56. data/lib/saml/kit/xsd/xml.xsd +287 -0
  57. data/lib/saml/kit/xsd/xmldsig-core-schema.xsd +309 -0
  58. data/lib/saml/kit/xsd_validatable.rb +19 -0
  59. data/saml-kit.gemspec +35 -0
  60. metadata +243 -0
@@ -0,0 +1,47 @@
1
+ module Saml
2
+ module Kit
3
+ module Namespaces
4
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
5
+ ATTR_SPLAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:*"
6
+ BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
7
+ BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
8
+ EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
9
+ ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
10
+ HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
11
+ HTTP_POST = POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
12
+ HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
13
+ METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
14
+ PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
15
+ PASSWORD_PROTECTED = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
16
+ PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
17
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
18
+ REQUESTER_ERROR = "urn:oasis:names:tc:SAML:2.0:status:Requester"
19
+ RESPONDER_ERROR = "urn:oasis:names:tc:SAML:2.0:status:Responder"
20
+ RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
21
+ RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
22
+ RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
23
+ RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
24
+ SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
25
+ SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
26
+ SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
27
+ SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
28
+ SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
29
+ TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
30
+ UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:consent:unspecified"
31
+ UNSPECIFIED_NAMEID = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
32
+ URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
33
+ VERSION_MISMATCH_ERROR = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
34
+ XMLDSIG = "http://www.w3.org/2000/09/xmldsig#"
35
+
36
+ def self.binding_for(binding)
37
+ if :post == binding
38
+ Namespaces::HTTP_POST
39
+ elsif :http_redirect == binding
40
+ Namespaces::HTTP_REDIRECT
41
+ else
42
+ nil
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,14 @@
1
+ module Saml
2
+ module Kit
3
+ module Requestable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ end
8
+
9
+ def query_string_parameter
10
+ 'SAMLRequest'
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,35 @@
1
+ module Saml
2
+ module Kit
3
+ module Respondable
4
+ extend ActiveSupport::Concern
5
+ attr_reader :request_id
6
+
7
+ included do
8
+ validates_inclusion_of :status_code, in: [Namespaces::SUCCESS]
9
+ validate :must_match_request_id
10
+ end
11
+
12
+ def query_string_parameter
13
+ 'SAMLResponse'
14
+ end
15
+
16
+ def status_code
17
+ to_h.fetch(name, {}).fetch('Status', {}).fetch('StatusCode', {}).fetch('Value', nil)
18
+ end
19
+
20
+ def in_response_to
21
+ to_h.fetch(name, {}).fetch('InResponseTo', nil)
22
+ end
23
+
24
+ private
25
+
26
+ def must_match_request_id
27
+ return if request_id.nil?
28
+
29
+ if in_response_to != request_id
30
+ errors[:in_response_to] << error_message(:invalid_response_to)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,197 @@
1
+ module Saml
2
+ module Kit
3
+ class Response < Document
4
+ include Respondable
5
+
6
+ validate :must_be_active_session
7
+ validate :must_match_issuer
8
+
9
+ def initialize(xml, request_id: nil)
10
+ @request_id = request_id
11
+ super(xml, name: "Response")
12
+ end
13
+
14
+ def name_id
15
+ to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Subject', {}).fetch('NameID', nil)
16
+ end
17
+
18
+ def [](key)
19
+ attributes[key]
20
+ end
21
+
22
+ def attributes
23
+ @attributes ||= Hash[to_h.fetch(name, {}).fetch('Assertion', {}).fetch('AttributeStatement', {}).fetch('Attribute', []).map do |item|
24
+ [item['Name'].to_sym, item['AttributeValue']]
25
+ end].with_indifferent_access
26
+ end
27
+
28
+
29
+ def started_at
30
+ parse_date(to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Conditions', {}).fetch('NotBefore', nil))
31
+ end
32
+
33
+ def expired_at
34
+ parse_date(to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Conditions', {}).fetch('NotOnOrAfter', nil))
35
+ end
36
+
37
+ def expired?
38
+ Time.current > expired_at
39
+ end
40
+
41
+ def active?
42
+ Time.current > started_at && !expired?
43
+ end
44
+
45
+ private
46
+
47
+ def must_be_active_session
48
+ return unless expected_type?
49
+ errors[:base] << error_message(:expired) unless active?
50
+ end
51
+
52
+ def must_match_issuer
53
+ return unless expected_type?
54
+
55
+ unless audiences.include?(Saml::Kit.configuration.issuer)
56
+ errors[:audience] << error_message(:must_match_issuer)
57
+ end
58
+ end
59
+
60
+ def audiences
61
+ Array(to_h[name]['Assertion']['Conditions']['AudienceRestriction']['Audience'])
62
+ rescue => error
63
+ Saml::Kit.logger.error(error)
64
+ []
65
+ end
66
+
67
+ def parse_date(value)
68
+ DateTime.parse(value)
69
+ rescue => error
70
+ Saml::Kit.logger.error(error)
71
+ Time.at(0).to_datetime
72
+ end
73
+
74
+ class Builder
75
+ attr_reader :user, :request
76
+ attr_accessor :id, :reference_id, :now
77
+ attr_accessor :version, :status_code
78
+ attr_accessor :issuer, :sign, :destination
79
+
80
+ def initialize(user, request)
81
+ @user = user
82
+ @request = request
83
+ @id = SecureRandom.uuid
84
+ @reference_id = SecureRandom.uuid
85
+ @now = Time.now.utc
86
+ @version = "2.0"
87
+ @status_code = Namespaces::SUCCESS
88
+ @issuer = configuration.issuer
89
+ @destination = request.acs_url
90
+ @sign = want_assertions_signed
91
+ end
92
+
93
+ def want_assertions_signed
94
+ request.provider.want_assertions_signed
95
+ rescue => error
96
+ Saml::Kit.logger.error(error)
97
+ true
98
+ end
99
+
100
+ def to_xml
101
+ Signature.sign(id, sign: sign) do |xml, signature|
102
+ xml.Response response_options do
103
+ xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
104
+ signature.template(xml)
105
+ xml.Status do
106
+ xml.StatusCode Value: status_code
107
+ end
108
+ xml.Assertion(assertion_options) do
109
+ xml.Issuer issuer
110
+ xml.Subject do
111
+ xml.NameID user.name_id_for(request.name_id_format), Format: request.name_id_format
112
+ xml.SubjectConfirmation Method: Namespaces::BEARER do
113
+ xml.SubjectConfirmationData "", subject_confirmation_data_options
114
+ end
115
+ end
116
+ xml.Conditions conditions_options do
117
+ xml.AudienceRestriction do
118
+ xml.Audience request.issuer
119
+ end
120
+ end
121
+ xml.AuthnStatement authn_statement_options do
122
+ xml.AuthnContext do
123
+ xml.AuthnContextClassRef Namespaces::PASSWORD
124
+ end
125
+ end
126
+ assertion_attributes = user.assertion_attributes_for(request)
127
+ if assertion_attributes.any?
128
+ xml.AttributeStatement do
129
+ assertion_attributes.each do |key, value|
130
+ xml.Attribute Name: key, NameFormat: Namespaces::URI, FriendlyName: key do
131
+ xml.AttributeValue value.to_s
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def build
142
+ Response.new(to_xml, request_id: request.id)
143
+ end
144
+
145
+ private
146
+
147
+ def configuration
148
+ Saml::Kit.configuration
149
+ end
150
+
151
+ def response_options
152
+ {
153
+ ID: id.present? ? "_#{id}" : nil,
154
+ Version: version,
155
+ IssueInstant: now.iso8601,
156
+ Destination: destination,
157
+ Consent: Namespaces::UNSPECIFIED,
158
+ InResponseTo: request.id,
159
+ xmlns: Namespaces::PROTOCOL,
160
+ }
161
+ end
162
+
163
+ def assertion_options
164
+ {
165
+ ID: "_#{reference_id}",
166
+ IssueInstant: now.iso8601,
167
+ Version: "2.0",
168
+ xmlns: Namespaces::ASSERTION,
169
+ }
170
+ end
171
+
172
+ def subject_confirmation_data_options
173
+ {
174
+ InResponseTo: request.id,
175
+ NotOnOrAfter: 3.hours.since(now).utc.iso8601,
176
+ Recipient: request.acs_url,
177
+ }
178
+ end
179
+
180
+ def conditions_options
181
+ {
182
+ NotBefore: now.utc.iso8601,
183
+ NotOnOrAfter: Saml::Kit.configuration.session_timeout.from_now.utc.iso8601,
184
+ }
185
+ end
186
+
187
+ def authn_statement_options
188
+ {
189
+ AuthnInstant: now.iso8601,
190
+ SessionIndex: assertion_options[:ID],
191
+ SessionNotOnOrAfter: 3.hours.since(now).utc.iso8601,
192
+ }
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,30 @@
1
+ module Saml
2
+ module Kit
3
+ class SelfSignedCertificate
4
+ def initialize(password)
5
+ @password = password
6
+ end
7
+
8
+ def create
9
+ rsa_key = OpenSSL::PKey::RSA.new(2048)
10
+ public_key = rsa_key.public_key
11
+ certificate = OpenSSL::X509::Certificate.new
12
+ certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse("/C=CA/ST=Alberta/L=Calgary/O=Xsig/OU=Xsig/CN=Xsig")
13
+ certificate.not_before = DateTime.now.beginning_of_day
14
+ certificate.not_after = 1.year.from_now.end_of_day
15
+ certificate.public_key = public_key
16
+ certificate.serial = 0x0
17
+ certificate.version = 2
18
+ factory = OpenSSL::X509::ExtensionFactory.new
19
+ factory.subject_certificate = factory.issuer_certificate = certificate
20
+ certificate.extensions = [ factory.create_extension("basicConstraints","CA:TRUE", true), factory.create_extension("subjectKeyIdentifier", "hash"), ]
21
+ certificate.add_extension(factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always"))
22
+ certificate.sign(rsa_key, OpenSSL::Digest::SHA256.new)
23
+ [
24
+ certificate.to_pem,
25
+ rsa_key.to_pem(OpenSSL::Cipher::Cipher.new('des3'), @password)
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ module Saml
2
+ module Kit
3
+ module Serializable
4
+ def decode(value)
5
+ Base64.decode64(value)
6
+ end
7
+
8
+ def encode(value)
9
+ Base64.strict_encode64(value)
10
+ end
11
+
12
+ def inflate(value)
13
+ inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
14
+ inflater.inflate(value)
15
+ end
16
+
17
+ # drop header and checksum as per spec.
18
+ def deflate(value, level: Zlib::BEST_COMPRESSION)
19
+ Zlib::Deflate.deflate(value, level)[2..-5]
20
+ end
21
+
22
+ def unescape(value)
23
+ CGI.unescape(value)
24
+ end
25
+
26
+ def escape(value)
27
+ CGI.escape(value)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,99 @@
1
+ module Saml
2
+ module Kit
3
+ class ServiceProviderMetadata < Metadata
4
+ def initialize(xml)
5
+ super("SPSSODescriptor", xml)
6
+ end
7
+
8
+ def assertion_consumer_services
9
+ services('AssertionConsumerService')
10
+ end
11
+
12
+ def assertion_consumer_service_for(binding:)
13
+ service_for(binding: binding, type: 'AssertionConsumerService')
14
+ end
15
+
16
+ def want_assertions_signed
17
+ attribute = find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
18
+ attribute.text.downcase == "true"
19
+ end
20
+
21
+ private
22
+
23
+ class Builder
24
+ attr_accessor :id, :entity_id, :acs_urls, :logout_urls, :name_id_formats, :sign
25
+ attr_accessor :want_assertions_signed
26
+
27
+ def initialize(configuration = Saml::Kit.configuration)
28
+ @id = SecureRandom.uuid
29
+ @configuration = configuration
30
+ @entity_id = configuration.issuer
31
+ @acs_urls = []
32
+ @logout_urls = []
33
+ @name_id_formats = [Namespaces::PERSISTENT]
34
+ @sign = true
35
+ @want_assertions_signed = true
36
+ end
37
+
38
+ def add_assertion_consumer_service(url, binding: :post)
39
+ @acs_urls.push(location: url, binding: Namespaces.binding_for(binding))
40
+ end
41
+
42
+ def add_single_logout_service(url, binding: :post)
43
+ @logout_urls.push(location: url, binding: Namespaces.binding_for(binding))
44
+ end
45
+
46
+ def to_xml
47
+ Signature.sign(id, sign: sign) do |xml, signature|
48
+ xml.instruct!
49
+ xml.EntityDescriptor entity_descriptor_options do
50
+ signature.template(xml)
51
+ xml.SPSSODescriptor descriptor_options do
52
+ if @configuration.signing_certificate_pem.present?
53
+ xml.KeyDescriptor use: "signing" do
54
+ xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
55
+ xml.X509Data do
56
+ xml.X509Certificate @configuration.stripped_signing_certificate
57
+ end
58
+ end
59
+ end
60
+ end
61
+ logout_urls.each do |item|
62
+ xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
63
+ end
64
+ name_id_formats.each do |format|
65
+ xml.NameIDFormat format
66
+ end
67
+ acs_urls.each_with_index do |item, index|
68
+ xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index == 0 ? true : false
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ def build
76
+ ServiceProviderMetadata.new(to_xml)
77
+ end
78
+
79
+ private
80
+
81
+ def entity_descriptor_options
82
+ {
83
+ 'xmlns': Namespaces::METADATA,
84
+ ID: "_#{id}",
85
+ entityID: entity_id,
86
+ }
87
+ end
88
+
89
+ def descriptor_options
90
+ {
91
+ AuthnRequestsSigned: sign,
92
+ WantAssertionsSigned: want_assertions_signed,
93
+ protocolSupportEnumeration: Namespaces::PROTOCOL,
94
+ }
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end