GoogleSSO 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require 'xmlcanonicalizer'
2
+ require 'processresponse'
3
+
4
+ class GoogleSSO
5
+ VERSION = '1.0'
6
+ end
@@ -0,0 +1,183 @@
1
+ require "openssl"
2
+ require "openssl/x509"
3
+ require "base64"
4
+ require "rexml/document"
5
+ require "rexml/xpath"
6
+ require "zlib"
7
+ require "digest/sha1"
8
+ require "xmlcanonicalizer"
9
+
10
+ include REXML
11
+ include OpenSSL
12
+
13
+ class XmlProcessResponse
14
+ attr_accessor :acs, :encodedresponse, :authenticateform, :logger
15
+
16
+ def initialize(certificate_path, private_key_path)
17
+ @certificate_path = certificate_path
18
+ @private_key_path = private_key_path
19
+ libpath = "#{Gem.dir}/gems/GoogleSSO-#{GoogleSSO::VERSION}/lib"
20
+ @signature_template_path = "#{libpath}/SignatureTemplate.xml"
21
+ @response_template_path = "#{libpath}/SamlResponseTemplate.xml"
22
+
23
+ @issueinstant = ""
24
+ @providername = ""
25
+
26
+ @acs = ""
27
+ @encodedresponse = ""
28
+ @authenticateform = ""
29
+
30
+ @logger = Logger.new("response.log")
31
+ @logger.level = Logger::DEBUG
32
+ end
33
+
34
+ def process_response(samlRequest, relayState, username)
35
+ xml_string = decodeAuthnRequestXML(samlRequest)
36
+ getRequestAttributes(xml_string)
37
+
38
+ samlResponse = createSamlResponse(username)
39
+
40
+ @logger.debug("\nSAML Response\n" + samlResponse.to_s()) if @logger
41
+
42
+ signedResponse = signXML(samlResponse)
43
+
44
+ @logger.debug("\nSigned Response\n" + signedResponse.to_s()) if @logger
45
+
46
+ @encodedresponse = Base64.encode64(signedResponse)
47
+ # @authenticateform = "<form name='acsForm' id='acsForm' action='#{@acs}' method='post'>"+
48
+ # WHY IS THIS ENCODED??
49
+ #" <textarea name='SAMLResponse'>#{@encodedresponse}</textarea>"+
50
+
51
+ @authenticateform = "<form name='acsForm' id='acsForm' action='#{@acs}' method='post'>"+
52
+ " SAMLResponse:<textarea rows='10' cols='80' name='SAMLResponse'>#{signedResponse}</textarea>"+
53
+ " RelayState:<textarea rows='10' cols='80' name='RelayState'>#{relayState}</textarea>"+
54
+ " <p><input type='submit' /></p>"+
55
+ "</form>"
56
+ end
57
+
58
+ # private
59
+
60
+ def decodeAuthnRequestXML(encodedRequestXmlString)
61
+ unzipper = Zlib::Inflate.new( -Zlib::MAX_WBITS )
62
+ return unzipper.inflate(Base64.decode64( encodedRequestXmlString ))
63
+ end
64
+
65
+ def getRequestAttributes(xmlString)
66
+ doc = Document.new( xmlString )
67
+ @issueinstant = doc.root.attributes["IssueInstant"]
68
+ @providername = doc.root.attributes["ProviderName"]
69
+ @acs = doc.root.attributes["AssertionConsumerServiceURL"]
70
+ end
71
+
72
+ def createSamlResponse(authenticatedUser)
73
+ current_time = Time.new().utc().strftime("%Y-%m-%dT%H:%M:%SZ")
74
+ # 20 minutes after issued time
75
+ notOnOrAfter = (Time.new().utc()+60*20).strftime("%Y-%m-%dT%H:%M:%SZ")
76
+
77
+ samlResponse = ""
78
+ File.open(@response_template_path).each { |line|
79
+ samlResponse += line
80
+ }
81
+
82
+ samlResponse.sub!("<USERNAME_STRING>", authenticatedUser)
83
+ samlResponse.sub!("<RESPONSE_ID>", generateUniqueHexCode(42))
84
+ samlResponse.gsub!("<ISSUE_INSTANT>", current_time)
85
+ samlResponse.sub!("<AUTHN_INSTANT>", current_time)
86
+ samlResponse.sub!("<NOT_BEFORE>", @issueinstant)
87
+ samlResponse.sub!("<NOT_ON_OR_AFTER>", notOnOrAfter)
88
+ samlResponse.sub!("<ASSERTION_ID>", generateUniqueHexCode(42))
89
+
90
+ return samlResponse
91
+ end
92
+
93
+ def signXML(xml)
94
+ signature = ""
95
+ File.open(@signature_template_path).each { |line|
96
+ signature += line
97
+ }
98
+
99
+ document = Document.new(xml)
100
+ sigDoc = Document.new(signature)
101
+
102
+ # 3. Apply digesting algorithms over the resource, and calculate the digest value.
103
+ digestValue = calculateDigest(document)
104
+
105
+ # 4. Enclose the details in the <Reference> element.
106
+ # 5. Collect all <Reference> elements inside the <SignedInfo> element. Indicate the canonicalization and signature methodologies.
107
+ digestElement = XPath.first(sigDoc, "//DigestValue")
108
+ digestElement.add_text(digestValue)
109
+
110
+ # 6. Canonicalize contents of <SignedInfo>, apply the signature algorithm, and generate the XML Digital signature.
111
+ signedElement = XPath.first(sigDoc, "//SignedInfo")
112
+ signatureValue = calculateSignatureValue(signedElement)
113
+
114
+ # 7. Enclose the signature within the <SignatureValue> element.
115
+ signatureValueElement = XPath.first(sigDoc, "//SignatureValue")
116
+ signatureValueElement.add_text(signatureValue)
117
+
118
+ # 8. Add relevant key information, if any, and produce the <Signature> element.
119
+ cert = ""
120
+ File.open(@certificate_path).each { |line|
121
+ cert += line
122
+ }
123
+
124
+ cert.sub!(/.*BEGIN CERTIFICATE-----\s*(.*)\s*-----END CERT.*/m, '\1')
125
+
126
+ certNode = XPath.first(sigDoc, "//X509Certificate")
127
+ certNode.add_text(cert)
128
+
129
+ # 9. put it all together
130
+ status_child = document.elements["//samlp:Status"]
131
+ status_child.parent.insert_before(status_child, sigDoc.root)
132
+ retval = document.to_s()
133
+
134
+ # XPath.first(document, "//Signature").remove
135
+
136
+ # canoner = XmlCanonicalizer.new(false, true)
137
+ # canon_element = canoner.canonicalize(document)
138
+
139
+ return retval
140
+ end
141
+
142
+ def calculateSignatureValue(element)
143
+ element.add_namespace("http://www.w3.org/2000/09/xmldsig#")
144
+ element.add_namespace("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol")
145
+ element.add_namespace("xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#")
146
+
147
+ canoner = XmlCanonicalizer.new(false, true)
148
+ canon_element = canoner.canonicalize(element)
149
+
150
+ pkey = PKey::RSA.new(File::read(@private_key_path))
151
+ signature = Base64.encode64(pkey.sign(OpenSSL::Digest::SHA1.new, canon_element.to_s.chomp).chomp).chomp
152
+
153
+ element.delete_attribute("xmlns")
154
+ element.delete_attribute("xmlns:samlp")
155
+ element.delete_attribute("xmlns:xenc")
156
+ return signature
157
+ end
158
+
159
+ def calculateDigest(element)
160
+ canoner = XmlCanonicalizer.new(false, true)
161
+ canon_element = canoner.canonicalize(element)
162
+
163
+ element_hash = Base64.encode64(Digest::SHA1.digest(canon_element.to_s.chomp).chomp).chomp
164
+
165
+ return element_hash
166
+ end
167
+
168
+ def generateUniqueHexCode( codeLength )
169
+ validChars = ("A".."F").to_a + ("0".."9").to_a
170
+ length = validChars.size
171
+
172
+ validStartChars = ("A".."F").to_a
173
+ startLength = validStartChars.size
174
+
175
+ hexCode = ""
176
+ hexCode << validStartChars[rand(startLength-1)]
177
+
178
+ 1.upto(codeLength-1) { |i| hexCode << validChars[rand(length-1)] }
179
+
180
+ return hexCode
181
+ end
182
+
183
+ end
@@ -0,0 +1,395 @@
1
+ require "rexml/document"
2
+ require "base64"
3
+
4
+ include REXML
5
+
6
+
7
+ class REXML::Instruction
8
+ def write(writer, indent=-1, transitive=false, ie_hack=false)
9
+ indent(writer, indent)
10
+ writer << START.sub(/\\/u, '')
11
+ writer << @target
12
+ writer << ' '
13
+ writer << @content if @content != nil
14
+ writer << STOP.sub(/\\/u, '')
15
+ end
16
+ end
17
+
18
+ class REXML::Attribute
19
+ def <=>(a2)
20
+ if (self === a2)
21
+ return 0
22
+ elsif (self == nil)
23
+ return -1
24
+ elsif (a2 == nil)
25
+ return 1
26
+ elsif (self.prefix() == a2.prefix())
27
+ return self.name()<=>a2.name()
28
+ end
29
+ if (self.prefix() == nil)
30
+ return -1
31
+ elsif (a2.prefix() == nil)
32
+ return 1
33
+ end
34
+ ret = self.namespace()<=>a2.namespace()
35
+ if (ret == 0)
36
+ ret = self.prefix()<=>a2.prefix()
37
+ end
38
+ return ret
39
+ end
40
+ end
41
+
42
+ class REXML::Element
43
+ def search_namespace(prefix)
44
+ if (self.namespace(prefix) == nil)
45
+ return (self.parent().search_namespace(prefix)) if (self.parent() != nil)
46
+ else
47
+ return self.namespace(prefix)
48
+ end
49
+ end
50
+ def rendered=(rendered)
51
+ @rendered = rendered
52
+ end
53
+ def rendered?()
54
+ return @rendered
55
+ end
56
+ def node_namespaces()
57
+ ns = Array.new()
58
+ ns.push(self.prefix())
59
+ self.attributes().each_attribute{|a|
60
+ if (a.prefix() == "xmlns" or (a.prefix() == "" && a.local_name() == "xmlns"))
61
+ ns.push("xmlns")
62
+ end
63
+ }
64
+ self.prefixes().each { |prefix| ns.push(prefix) }
65
+ ns
66
+ end
67
+ end
68
+
69
+ class NamespaceNode
70
+ attr_reader :prefix, :uri
71
+ def initialize(prefix, uri)
72
+ @prefix = prefix
73
+ @uri = uri
74
+ end
75
+ end
76
+
77
+ class XmlCanonicalizer
78
+ attr_accessor :prefix_list, :logger
79
+
80
+ BEFORE_DOC_ELEMENT = 0
81
+ INSIDE_DOC_ELEMENT = 1
82
+ AFTER_DOC_ELEMENT = 2
83
+
84
+ NODE_TYPE_ATTRIBUTE = 3
85
+ NODE_TYPE_WHITESPACE = 4
86
+ NODE_TYPE_COMMENT = 5
87
+ NODE_TYPE_PI = 6
88
+ NODE_TYPE_TEXT = 7
89
+
90
+
91
+ def initialize(with_comments, excl_c14n)
92
+ @with_comments = with_comments
93
+ @exclusive = excl_c14n
94
+ @res = ""
95
+ @state = BEFORE_DOC_ELEMENT
96
+ @xnl = Array.new()
97
+ @prevVisibleNamespacesStart = 0
98
+ @prevVisibleNamespacesEnd = 0
99
+ @visibleNamespaces = Array.new()
100
+ @inclusive_namespaces = Array.new()
101
+ @prefix_list = nil
102
+ @rendered_prefixes = Array.new()
103
+ @logger = Logger.new("xmlcanonicalizer.log")
104
+ @logger.level = Logger::DEBUG
105
+ end
106
+
107
+ def add_inclusive_namespaces(prefix_list, element, visible_namespaces)
108
+ namespaces = element.attributes()
109
+ namespaces.each_attribute{|ns|
110
+ if (ns.prefix=="xmlns")
111
+ if (prefix_list.include?(ns.local_name()))
112
+ visible_namespaces.push(NamespaceNode.new("xmlns:"+ns.local_name(), ns.value()))
113
+ end
114
+ end
115
+ }
116
+ parent = element.parent()
117
+ add_inclusive_namespaces(prefix_list, parent, visible_namespaces) if (parent)
118
+ visible_namespaces
119
+ end
120
+
121
+ def canonicalize(document)
122
+ write_document_node(document)
123
+ @logger.debug("\nCanonicalized result\n" + @res.to_s()) if @logger
124
+ @res
125
+ end
126
+
127
+ def canonicalize_element(element, logging = true)
128
+ @logger.debug("Canonicalize element:\n" + element.to_s()) if @logger
129
+ @inclusive_namespaces = add_inclusive_namespaces(@prefix_list, element, @inclusive_namespaces) if (@prefix_list)
130
+ @preserve_document = element.document()
131
+ tmp_parent = element.parent()
132
+ body_string = remove_whitespace(element.to_s().gsub("\n","").gsub("\t","").gsub("\r",""))
133
+ document = Document.new(body_string)
134
+ tmp_parent.delete_element(element)
135
+ element = tmp_parent.add_element(document.root())
136
+ @preserve_element = element
137
+ document = Document.new(element.to_s())
138
+ ns = element.namespace(element.prefix())
139
+ document.root().add_namespace(element.prefix(), ns)
140
+ write_document_node(document)
141
+ @logger.debug("Canonicalized result:\n" + @res.to_s()) if @logger
142
+ @res
143
+ end
144
+
145
+ def write_document_node(document)
146
+ @state = BEFORE_DOC_ELEMENT
147
+ if (document.class().to_s() == "REXML::Element")
148
+ write_node(document)
149
+ else
150
+ document.each_child{|child|
151
+ write_node(child)
152
+ }
153
+ end
154
+ @res
155
+ end
156
+
157
+ def write_node(node)
158
+ visible = is_node_visible(node)
159
+ if ((node.node_type() == :text) && white_text?(node.value()))
160
+ res = node.value()
161
+ res.gsub("\r\n","\n")
162
+ #res = res.delete(" ").delete("\t")
163
+ res.delete("\r")
164
+ @res = @res + res
165
+ #write_text_node(node,visible) if (@state == INSIDE_DOC_ELEMENT)
166
+ return
167
+ end
168
+ if (node.node_type() == :text)
169
+ write_text_node(node, visible)
170
+ return
171
+ end
172
+ if (node.node_type() == :element)
173
+ write_element_node(node, visible) if (!node.rendered?())
174
+ node.rendered=(true)
175
+ end
176
+ if (node.node_type() == :processing_instruction)
177
+ end
178
+ if (node.node_type() == :comment)
179
+ end
180
+ end
181
+
182
+ def write_element_node(node, visible)
183
+ savedPrevVisibleNamespacesStart = @prevVisibleNamespacesStart
184
+ savedPrevVisibleNamespacesEnd = @prevVisibleNamespacesEnd
185
+ savedVisibleNamespacesSize = @visibleNamespaces.size()
186
+ state = @state
187
+ state = INSIDE_DOC_ELEMENT if (visible && state == BEFORE_DOC_ELEMENT)
188
+ @res = @res + "<" + node.expanded_name() if (visible)
189
+ write_namespace_axis(node, visible)
190
+ write_attribute_axis(node)
191
+ @res = @res + ">" if (visible)
192
+ node.each_child{|child|
193
+ write_node(child)
194
+ }
195
+ @res = @res + "</" +node.expanded_name() + ">" if (visible)
196
+ @state = AFTER_DOC_ELEMENT if (visible && state == BEFORE_DOC_ELEMENT)
197
+ @prevVisibleNamespacesStart = savedPrevVisibleNamespacesStart
198
+ @prevVisibleNamespacesEnd = savedPrevVisibleNamespacesEnd
199
+ @visibleNamespaces.slice!(savedVisibleNamespacesSize, @visibleNamespaces.size() - savedVisibleNamespacesSize) if (@visibleNamespaces.size() > savedVisibleNamespacesSize)
200
+ end
201
+
202
+ def write_namespace_axis(node, visible)
203
+ doc = node.document()
204
+ has_empty_namespace = false
205
+ list = Array.new()
206
+ cur = node
207
+ #while ((cur != nil) && (cur != doc) && (cur.node_type() != :document))
208
+ namespaces = cur.node_namespaces()
209
+ namespaces.each{|prefix|
210
+ next if ((prefix == "xmlns") && (node.namespace(prefix) == ""))
211
+ namespace = cur.namespace(prefix)
212
+ next if (is_namespace_node(namespace))
213
+ next if (node.namespace(prefix) != cur.namespace(prefix))
214
+ next if (prefix == "xml" && namespace == "http://www.w3.org/XML/1998/namespace")
215
+ next if (!is_node_visible(cur))
216
+ rendered = is_namespace_rendered(prefix, namespace)
217
+ @visibleNamespaces.push(NamespaceNode.new("xmlns:"+prefix,namespace)) if (visible)
218
+ if ((!rendered) && !list.include?(prefix))
219
+ list.push(prefix)
220
+ end
221
+ has_empty_namespace = true if (prefix == nil)
222
+ }
223
+ if (visible && !has_empty_namespace && !is_namespace_rendered(nil, nil))
224
+ @res = @res + ' xmlns=""'
225
+ end
226
+ #TODO: ns of inclusive_list
227
+ #=begin
228
+ if ((@prefix_list) && (node.to_s() == node.parent().to_s()))
229
+ #list.push(node.prefix())
230
+ @inclusive_namespaces.each{|ns|
231
+ prefix = ns.prefix().split(":")[1]
232
+ list.push(prefix) if (!list.include?(prefix) && (!node.attributes.prefixes.include?(prefix)))
233
+ }
234
+ @prefix_list = nil
235
+ end
236
+ #=end
237
+ list.sort!()
238
+ list.insert(0, "xmlns") unless list.delete("xmlns").nil?
239
+ list.each{|prefix|
240
+ next if (prefix == "")
241
+ next if (@rendered_prefixes.include?(prefix))
242
+ @rendered_prefixes.push(prefix)
243
+ ns = node.namespace(prefix)
244
+ ns = @preserve_element.namespace(prefix) if (ns == nil)
245
+ @res = @res + normalize_string(" " + prefix + '="' + ns + '"', NODE_TYPE_TEXT) if (prefix == "xmlns")
246
+ @res = @res + normalize_string(" xmlns:" + prefix + '="' + ns + '"', NODE_TYPE_TEXT) if (prefix != nil && prefix != "xmlns")
247
+ }
248
+ if (visible)
249
+ @prevVisibleNamespacesStart = @prevVisibleNamespacesEnd
250
+ @prevVisibleNamespacesEnd = @visibleNamespaces.size()
251
+ end
252
+ end
253
+
254
+ def write_attribute_axis(node)
255
+ list = Array.new()
256
+ node.attributes().sort.each{|key, attr|
257
+ list.push(attr) if (!is_namespace_node(attr.value()) && !is_namespace_decl(attr)) # && is_node_visible(
258
+ }
259
+ if (!@exclusive && node.parent() != nil && node.parent().parent() != nil)
260
+ cur = node.parent()
261
+ while (cur != nil)
262
+ #next if (cur.attributes() == nil)
263
+ cur.each_attribute{|attribute|
264
+ next if (attribute.prefix() != "xml")
265
+ next if (attribute.prefix().index("xmlns") == 0)
266
+ next if (node.namespace(attribute.prefix()) == attribute.value())
267
+ found = true
268
+ list.each{|n|
269
+ if (n.prefix() == "xml" && n.value() == attritbute.value())
270
+ found = true
271
+ break
272
+ end
273
+ }
274
+ next if (found)
275
+ list.push(attribute)
276
+ }
277
+ end
278
+ end
279
+ list.each{|attribute|
280
+ if (attribute != nil)
281
+ if (attribute.name() != "xmlns")
282
+ @res = @res + " " + normalize_string(attribute.to_string(), NODE_TYPE_ATTRIBUTE).gsub("'",'"')
283
+ end
284
+ # else
285
+ # @res = @res + " " + normalize_string(attribute.name()+'="'+attribute.to_s()+'"', NODE_TYPE_ATTRIBUTE).gsub("'",'"')
286
+ #end
287
+ end
288
+ }
289
+ end
290
+
291
+ def is_namespace_node(namespace_uri)
292
+ return (namespace_uri == "http://www.w3.org/2000/xmlns/")
293
+ end
294
+
295
+ def is_namespace_rendered(prefix, uri)
296
+ is_empty_ns = prefix == nil && uri == nil
297
+ if (is_empty_ns)
298
+ start = 0
299
+ else
300
+ start = @prevVisibleNamespacesStart
301
+ end
302
+ @visibleNamespaces.each{|ns|
303
+ if (ns.prefix() == "xmlns:"+prefix.to_s() && ns.uri() == uri)
304
+ return true
305
+ end
306
+ }
307
+ return is_empty_ns
308
+ #(@visibleNamespaces.size()-1).downto(start) {|i|
309
+ # ns = @visibleNamespaces[i]
310
+ # return true if (ns.prefix() == "xmlns:"+prefix.to_s() && ns.uri() == uri)
311
+ # #p = ns.prefix() if (ns.prefix().index("xmlns") == 0)
312
+ # #return ns.uri() == uri if (p == prefix)
313
+ #}
314
+ #return is_empty_ns
315
+ end
316
+
317
+ def is_node_visible(node)
318
+ return true if (@xnl.size() == 0)
319
+ @xnl.each{|element|
320
+ return true if (element == node)
321
+ }
322
+ return false
323
+ end
324
+
325
+ def normalize_string(input, type)
326
+ sb = ""
327
+ return input
328
+ end
329
+ #input.each_byte{|b|
330
+ # if (b ==60 && (type == NODE_TYPE_ATTRIBUTE || is_text_node(type)))
331
+ # sb = sb + "&lt;"
332
+ # elsif (b == 62 && is_text_node(type))
333
+ # sb = sb + "&gt;"
334
+ # elsif (b == 38 && (is_text_node(type) || is_text_node(type))) #Ampersand
335
+ # sb = sb + "&amp;"
336
+ # elsif (b == 34 && is_text_node(type)) #Quote
337
+ # sb = sb + "&quot;"
338
+ # elsif (b == 9 && is_text_node(type)) #Tabulator
339
+ # sb = sb + "&#x9;"
340
+ # elsif (b == 11 && is_text_node(type)) #CR
341
+ # sb = sb + "&#xA;"
342
+ # elsif (b == 13 && (type == NODE_TYPE_ATTRIBUTE || (is_text_node(type) && type != NODE_TYPE_WHITESPACE) || type == NODE_TYPE_COMMENT || type == NODE_TYPE_PI))
343
+ # sb = sb + "&#xD;"
344
+ # elsif (b == 13)
345
+ # next
346
+ # else
347
+ # sb = sb.concat(b)
348
+ # end
349
+ #}
350
+ #sb
351
+ #end
352
+
353
+ def write_text_node(node, visible)
354
+ if (visible)
355
+ @res = @res + normalize_string(node.value(), node.node_type())
356
+ end
357
+ end
358
+
359
+ def white_text?(text)
360
+ return true if ((text.strip() == "") || (text.strip() == nil))
361
+ return false
362
+ end
363
+
364
+ def is_namespace_decl(attribute)
365
+ #return true if (attribute.name() == "xmlns")
366
+ return true if (attribute.prefix().index("xmlns") == 0)
367
+ return false
368
+ end
369
+
370
+ def is_text_node(type)
371
+ return true if (type == NODE_TYPE_TEXT || type == NODE_TYPE_CDATA || type == NODE_TYPE_WHITESPACE)
372
+ return false
373
+ end
374
+
375
+ def remove_whitespace(string)
376
+ new_string = ""
377
+ in_white = false
378
+ string.each_byte{|b|
379
+ #if (in_white && b == 32)
380
+ #else
381
+ if !(in_white && b == 32)
382
+ new_string = new_string + b.chr()
383
+ end
384
+ if (b == 62) #>
385
+ in_white = true
386
+ end
387
+ if (b == 60) #<
388
+ in_white = false
389
+ end
390
+ }
391
+ new_string
392
+ end
393
+ end
394
+
395
+
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.2
3
+ specification_version: 1
4
+ name: GoogleSSO
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.1
7
+ date: 2007-06-14 00:00:00 +02:00
8
+ summary: "Google SSO: SAML signing and processing."
9
+ require_paths:
10
+ - lib
11
+ email: NN
12
+ homepage: http://rubyforge.org
13
+ rubyforge_project:
14
+ description: "Google SSO: SAML signing and processing."
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - NN
31
+ files:
32
+ - lib/GoogleSSO.rb
33
+ - lib/processresponse.rb
34
+ - lib/xmlcanonicalizer.rb
35
+ test_files: []
36
+
37
+ rdoc_options: []
38
+
39
+ extra_rdoc_files: []
40
+
41
+ executables: []
42
+
43
+ extensions: []
44
+
45
+ requirements: []
46
+
47
+ dependencies: []
48
+