google-sso 1.0.2
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.
- data/Manifest.txt +10 -0
- data/README.txt +41 -0
- data/Rakefile +17 -0
- data/lib/SamlResponseTemplate.xml +1 -0
- data/lib/SignatureTemplate.xml +19 -0
- data/lib/google-sso.rb +3 -0
- data/lib/google-sso/version.rb +9 -0
- data/lib/processresponse.rb +221 -0
- data/lib/xmlcanonicalizer.rb +405 -0
- data/test/test_GoogleSSO.rb +46 -0
- metadata +65 -0
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
Google SSO gem supported by ELC Technologies. Read more about our Ruby on Rails development services and commitment to the Rails community at elctech.com
|
2
|
+
|
3
|
+
GoogleSSO
|
4
|
+
by Thomas Ormerod
|
5
|
+
|
6
|
+
== DESCRIPTION:
|
7
|
+
|
8
|
+
Provides single-sign-on to Google premier services.
|
9
|
+
|
10
|
+
== FEATURES/PROBLEMS:
|
11
|
+
|
12
|
+
Uses an incomplete xml canonicalizer.
|
13
|
+
|
14
|
+
== REQUIREMENTS:
|
15
|
+
|
16
|
+
Core classes only
|
17
|
+
|
18
|
+
== INSTALL:
|
19
|
+
|
20
|
+
sudo gem install google-sso
|
21
|
+
|
22
|
+
== USAGE:
|
23
|
+
|
24
|
+
1. Generate an RSA key (DSA has issues with ruby/openssl)
|
25
|
+
1. openssl genrsa -out privkey.pem [strength ie. 1024, 2048]
|
26
|
+
2. Create a self-signed certificate (or signed your choice)
|
27
|
+
1. openssl req -new -x509 -key privkey.pem -out cacert.pem [optional -days ie. 1095]
|
28
|
+
3. Go to https://www.google.com/a/YOUR-DOMAIN and login as administrator
|
29
|
+
1. Advanced tools > Single sign-on
|
30
|
+
2. Enable Single Sign-on
|
31
|
+
3. Set your Sign-in page URL to your sites login page
|
32
|
+
4. Set your other URL's
|
33
|
+
5. Save
|
34
|
+
6. Upload your certificate (cacert.pem)
|
35
|
+
7. Logout
|
36
|
+
8. You have now enabled single sign on.
|
37
|
+
|
38
|
+
|
39
|
+
Anytime a user attempts to access a Google service with /a/[your domain]
|
40
|
+
on the end they will first have to authenticate through you, unless they
|
41
|
+
previously authenticated and a cookie is set.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hoe'
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__), 'lib')
|
4
|
+
# Gem::manage_gems
|
5
|
+
# require 'rake/gempackagetask'
|
6
|
+
require 'google-sso'
|
7
|
+
|
8
|
+
Hoe.new('GoogleSSO', GoogleSSO::VERSION::STRING) do |p|
|
9
|
+
p.rubyforge_name = 'google-sso'
|
10
|
+
p.name = 'google-sso'
|
11
|
+
p.author = 'Thomas Ormerod, David Palm, and Ryan Garver'
|
12
|
+
p.description = 'Google Apps Single Sign-on (SSO) support.'
|
13
|
+
p.email = 'google-sso@elctech.com'
|
14
|
+
p.summary = 'Google Apps Single Sign-on'
|
15
|
+
p.url = 'http://elctech.com'
|
16
|
+
p.rdoc_pattern = /\.txt$|lib\/.*\.rb$/
|
17
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?><samlp:Response ID="<RESPONSE_ID>" IssueInstant="<ISSUE_INSTANT>" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><Assertion ID="<ASSERTION_ID>" IssueInstant="<ISSUE_INSTANT>" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"><Issuer>https://www.opensaml.org/IDP</Issuer><Subject><NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress"><USERNAME_STRING></NameID><SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"/></Subject><Conditions NotBefore="<NOT_BEFORE>" NotOnOrAfter="<NOT_ON_OR_AFTER>"/><AuthnStatement AuthnInstant="<AUTHN_INSTANT>"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion></samlp:Response>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
|
2
|
+
<SignedInfo>
|
3
|
+
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
|
4
|
+
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
|
5
|
+
<Reference URI="">
|
6
|
+
<Transforms>
|
7
|
+
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
|
8
|
+
</Transforms>
|
9
|
+
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
|
10
|
+
<DigestValue></DigestValue>
|
11
|
+
</Reference>
|
12
|
+
</SignedInfo>
|
13
|
+
<SignatureValue></SignatureValue>
|
14
|
+
<KeyInfo>
|
15
|
+
<X509Data>
|
16
|
+
<X509Certificate></X509Certificate>
|
17
|
+
</X509Data>
|
18
|
+
</KeyInfo>
|
19
|
+
</Signature>
|
data/lib/google-sso.rb
ADDED
@@ -0,0 +1,221 @@
|
|
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
|
+
require 'log4r'
|
10
|
+
|
11
|
+
include Log4r
|
12
|
+
include REXML
|
13
|
+
include OpenSSL
|
14
|
+
|
15
|
+
class XmlProcessResponse
|
16
|
+
attr_reader :acs, :signed_response, :acs_form
|
17
|
+
|
18
|
+
# Create an XmlProcessResponse object
|
19
|
+
#
|
20
|
+
# certificate_path: path to the site certificate, previously uploaded to Google using the SSO admin panel
|
21
|
+
# private_key_path: path to the private key that will be used to sign the SAML Response
|
22
|
+
def initialize(certificate_path, private_key_path)
|
23
|
+
@certificate_path = certificate_path
|
24
|
+
@private_key_path = private_key_path
|
25
|
+
|
26
|
+
libpath = "#{Gem.dir}/gems/google-sso-#{GoogleSSO::VERSION::STRING}/lib" # FIXME: uses hardcoded gemname. Bad bad bad.
|
27
|
+
@signature_template_path = "#{libpath}/SignatureTemplate.xml"
|
28
|
+
@response_template_path = "#{libpath}/SamlResponseTemplate.xml"
|
29
|
+
|
30
|
+
@issue_instant = ""
|
31
|
+
@provider_name = ""
|
32
|
+
|
33
|
+
@acs = ""
|
34
|
+
@acs_form = ""
|
35
|
+
|
36
|
+
@signed_response = ""
|
37
|
+
|
38
|
+
@logger = Logger.new("response.log")
|
39
|
+
@logger.level = Logger::DEBUG
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# Builds a response SAML document to send back to Google.
|
44
|
+
#
|
45
|
+
# Takes three parameters: a SAMLRequest-document, the RelayState
|
46
|
+
# variable and the username to authenticate.
|
47
|
+
# samlRequest: a string containing the SAML request Google sends out when a user tries to login to a SSO enabled domain.
|
48
|
+
# relayState: a string containing various state parameters regarding the Google domain and the service required.
|
49
|
+
# username: a string with the username that needs to be authenticated.
|
50
|
+
#
|
51
|
+
# The method returns an HTML snippet containing a form with two
|
52
|
+
# <tt><textarea></tt> elements with the SAML response signed using the key, and
|
53
|
+
# the RelayState and the form action set to the Google Assertion Consumer Service URL.
|
54
|
+
# Both the SAMLResponse and the RelayState parameters has to be Base64 encoded when submited to Google; this is automatically
|
55
|
+
# handled for text inserted into <tt>textarea</tt> elements.
|
56
|
+
#
|
57
|
+
# The form should be inserted in the resulting page and submit()-ed by a onload javascript function such as:
|
58
|
+
# window.onload = function(){
|
59
|
+
# var f = document.getElementById('acsForm');
|
60
|
+
# if(f.nodeName=='FORM'){
|
61
|
+
# f.submit();
|
62
|
+
# }
|
63
|
+
# }
|
64
|
+
def process_response(raw_saml_request, relay_state, username)
|
65
|
+
saml_request = decode_authn_request(raw_saml_request)
|
66
|
+
get_request_attributes(saml_request)
|
67
|
+
|
68
|
+
saml_response = create_saml_response(username)
|
69
|
+
|
70
|
+
@logger.debug("\nSAML Response\n" + saml_response.to_s()) if @logger
|
71
|
+
|
72
|
+
@signed_response = sign_XML(saml_response)
|
73
|
+
|
74
|
+
@logger.debug("\nSigned Response\n" + signed_response.to_s()) if @logger
|
75
|
+
|
76
|
+
@acs_form = <<-ACSFORM
|
77
|
+
<form name="acsForm" id="acsForm" action="#{@acs}" method="post">
|
78
|
+
<textarea name="SAMLResponse">#{signed_response}</textarea>
|
79
|
+
<textarea name="RelayState">#{relay_state}</textarea>
|
80
|
+
</form>
|
81
|
+
ACSFORM
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# The SAMLRequest is zipped and Base64 encoded. This method unzips and decodes the request.
|
87
|
+
def decode_authn_request(encoded_saml_request) #:doc:
|
88
|
+
unzipper = Zlib::Inflate.new( -Zlib::MAX_WBITS )
|
89
|
+
unzipper.inflate(Base64.decode64(encoded_saml_request))
|
90
|
+
end
|
91
|
+
|
92
|
+
# Extracts three attributes from the SAMLRequest sent by Google and sets the corresponding ivars:
|
93
|
+
# - issueinstant: when was the request issued (resent in the SAMLResponse in order to establish validity in time of the auth request/response to assure the response cannot be used before the request was issued)
|
94
|
+
# - providername: this is the name of the service provider requesting authentication from us.
|
95
|
+
# - acs: the URL at the service provider site to which our SAMLResponse doc should be sent.
|
96
|
+
#
|
97
|
+
# Takes an XML string (the SAML Request) in input.
|
98
|
+
def get_request_attributes(xmlString) #:doc:
|
99
|
+
doc = Document.new( xmlString )
|
100
|
+
@issue_instant = doc.root.attributes["IssueInstant"]
|
101
|
+
@provider_name = doc.root.attributes["ProviderName"] # FIXME: This is not really used anywhere; is set to "google.com". Should we check for this? Or remove the ivar?
|
102
|
+
@acs = doc.root.attributes["AssertionConsumerServiceURL"]
|
103
|
+
end
|
104
|
+
|
105
|
+
# Create the SAMLResponse
|
106
|
+
#
|
107
|
+
# Loads the template and substitutes the markers USERNAME_STRING, RESPONSE_ID, ISSUE_INSTANT, AUTHN_INSTANT, NOT_BEFORE, NOT_ON_OR_AFTER and ASSERTION_ID.
|
108
|
+
#
|
109
|
+
# Takes a string in input with the username of the user requesting authentication.
|
110
|
+
# The SAMLResponse is valid for 20 minutes.
|
111
|
+
def create_saml_response(username) #:doc:
|
112
|
+
current_time = Time.new().utc().strftime("%Y-%m-%dT%H:%M:%SZ")
|
113
|
+
# 20 minutes after issued time
|
114
|
+
not_on_or_after = (Time.new().utc()+60*20).strftime("%Y-%m-%dT%H:%M:%SZ")
|
115
|
+
|
116
|
+
saml_response = File.read @response_template_path
|
117
|
+
|
118
|
+
saml_response.sub!("<USERNAME_STRING>", username)
|
119
|
+
saml_response.sub!("<RESPONSE_ID>", generate_unique_hexcode(42))
|
120
|
+
saml_response.gsub!("<ISSUE_INSTANT>", current_time)
|
121
|
+
saml_response.sub!("<AUTHN_INSTANT>", current_time)
|
122
|
+
saml_response.sub!("<NOT_BEFORE>", @issue_instant)
|
123
|
+
saml_response.sub!("<NOT_ON_OR_AFTER>", not_on_or_after)
|
124
|
+
saml_response.sub!("<ASSERTION_ID>", generate_unique_hexcode(42))
|
125
|
+
|
126
|
+
return saml_response
|
127
|
+
end
|
128
|
+
|
129
|
+
# Takes an XML string built from the SAMLResponse template and inserts a digest, signature and certificate into the SignatureTemplate. Returns a string rappresentation of the XML document.
|
130
|
+
def sign_XML(saml_response) #:doc:
|
131
|
+
signature = File.read @signature_template_path
|
132
|
+
|
133
|
+
saml_response_doc = Document.new(saml_response)
|
134
|
+
sig_doc = Document.new(signature)
|
135
|
+
|
136
|
+
# 3. Apply digesting algorithms over the resource, and calculate the digest value.
|
137
|
+
digest_value = calculate_digest(saml_response_doc)
|
138
|
+
|
139
|
+
# 4. Enclose the details in the <Reference> element.
|
140
|
+
# 5. Collect all <Reference> elements inside the <SignedInfo> element. Indicate the canonicalization and signature methodologies.
|
141
|
+
digest_element = XPath.first( sig_doc, "//DigestValue" )
|
142
|
+
digest_element.add_text( digest_value )
|
143
|
+
|
144
|
+
# 6. Canonicalize contents of <SignedInfo>, apply the signature algorithm, and generate the XML Digital signature.
|
145
|
+
signed_element = XPath.first( sig_doc, "//SignedInfo" )
|
146
|
+
signature_value = calculate_signature_value( signed_element )
|
147
|
+
|
148
|
+
# 7. Enclose the signature within the <SignatureValue> element.
|
149
|
+
signature_value_element = XPath.first( sig_doc, "//SignatureValue" )
|
150
|
+
signature_value_element.add_text( signature_value )
|
151
|
+
|
152
|
+
# 8. Add relevant key information, if any, and produce the <Signature> element.
|
153
|
+
cert = File.read @certificate_path
|
154
|
+
|
155
|
+
cert.sub!(/.*BEGIN CERTIFICATE-----\s*(.*)\s*-----END CERT.*/m, '\1')
|
156
|
+
|
157
|
+
cert_node = XPath.first(sig_doc, "//X509Certificate")
|
158
|
+
cert_node.add_text(cert)
|
159
|
+
|
160
|
+
# 9. put it all together
|
161
|
+
status_child = saml_response_doc.elements["//samlp:Status"]
|
162
|
+
status_child.parent.insert_before(status_child, sig_doc.root)
|
163
|
+
retval = saml_response_doc.to_s()
|
164
|
+
|
165
|
+
return retval
|
166
|
+
end
|
167
|
+
|
168
|
+
# Builds a canonicalized version of the passed DOM element, signs it and returns a Base64-encoded version.
|
169
|
+
# The element is temporarily provided with the following namespaces:
|
170
|
+
# - http://www.w3.org/2000/09/xmldsig#
|
171
|
+
# - urn:oasis:names:tc:SAML:2.0:protocol
|
172
|
+
# - http://www.w3.org/2001/04/xmlenc#
|
173
|
+
#
|
174
|
+
def calculate_signature_value(element)
|
175
|
+
element.add_namespace "http://www.w3.org/2000/09/xmldsig#"
|
176
|
+
element.add_namespace "xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol"
|
177
|
+
element.add_namespace "xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#"
|
178
|
+
|
179
|
+
canoner = XmlCanonicalizer.new(false, true)
|
180
|
+
canon_element = canoner.canonicalize( element )
|
181
|
+
|
182
|
+
pkey = PKey::RSA.new( File::read( @private_key_path ) )
|
183
|
+
signature = Base64.encode64( pkey.sign( OpenSSL::Digest::SHA1.new, canon_element.to_s.chomp ).chomp ).chomp
|
184
|
+
|
185
|
+
element.delete_attribute "xmlns"
|
186
|
+
element.delete_attribute "xmlns:samlp"
|
187
|
+
element.delete_attribute "xmlns:xenc"
|
188
|
+
return signature
|
189
|
+
end
|
190
|
+
|
191
|
+
# Builds a canonicalized version of the passed DOM element and returns a
|
192
|
+
# SHA1-digest of the element (Base64 encoded string).
|
193
|
+
def calculate_digest(element) #:doc:
|
194
|
+
canoner = XmlCanonicalizer.new(false, true)
|
195
|
+
canon_element = canoner.canonicalize( element )
|
196
|
+
Base64.encode64( Digest::SHA1.digest( canon_element.to_s.chomp ).chomp ).chomp
|
197
|
+
end
|
198
|
+
|
199
|
+
# Generate an ID string for use in two attributes in the SAMLResponse
|
200
|
+
# (response ID and assertion ID); these has to obey specific rules
|
201
|
+
# (see Google SSO documentation).
|
202
|
+
#
|
203
|
+
# Returns a random hexadecimal string <tt>code_length</tt> long.
|
204
|
+
def generate_unique_hexcode( code_length ) #:doc:
|
205
|
+
valid_chars = ("A".."F").to_a + ("0".."9").to_a
|
206
|
+
length = valid_chars.size
|
207
|
+
|
208
|
+
valid_start_chars = ("A".."F").to_a
|
209
|
+
start_length = valid_start_chars.size
|
210
|
+
|
211
|
+
hexcode = ""
|
212
|
+
hexcode << valid_start_chars[rand(start_length-1)]
|
213
|
+
|
214
|
+
1.upto(code_length-1){|i|
|
215
|
+
hexcode << valid_chars[rand(length-1)]
|
216
|
+
}
|
217
|
+
|
218
|
+
return hexcode
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
@@ -0,0 +1,405 @@
|
|
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
|
+
# From the IBM DeveloperWorks website[http://www.ibm.com/developerworks/xml/library/x-c14n/]:
|
78
|
+
#
|
79
|
+
# <em>XML is careful to separate details of a file or other data source, bit-by-bit, from the
|
80
|
+
# abstract model of an XML document. This can be an inconvenience when comparing two
|
81
|
+
# XML documents for equality -- either directly (for instance, as part of a test suite)
|
82
|
+
# or by comparing digital signatures for security purposes -- to determine whether an
|
83
|
+
# XML document has been tampered with in some way. The W3C addresses this problem with the
|
84
|
+
# XML Canonicalization spec (c14n), which defines a standard form for an XML document that
|
85
|
+
# is guaranteed to provide proper bit-wise comparisons and thus consistent digital signatures.</em>
|
86
|
+
|
87
|
+
class XmlCanonicalizer
|
88
|
+
attr_accessor :prefix_list, :logger
|
89
|
+
|
90
|
+
BEFORE_DOC_ELEMENT = 0
|
91
|
+
INSIDE_DOC_ELEMENT = 1
|
92
|
+
AFTER_DOC_ELEMENT = 2
|
93
|
+
|
94
|
+
NODE_TYPE_ATTRIBUTE = 3
|
95
|
+
NODE_TYPE_WHITESPACE = 4
|
96
|
+
NODE_TYPE_COMMENT = 5
|
97
|
+
NODE_TYPE_PI = 6
|
98
|
+
NODE_TYPE_TEXT = 7
|
99
|
+
|
100
|
+
|
101
|
+
def initialize(with_comments, excl_c14n)
|
102
|
+
@with_comments = with_comments
|
103
|
+
@exclusive = excl_c14n
|
104
|
+
@res = ""
|
105
|
+
@state = BEFORE_DOC_ELEMENT
|
106
|
+
@xnl = Array.new()
|
107
|
+
@prevVisibleNamespacesStart = 0
|
108
|
+
@prevVisibleNamespacesEnd = 0
|
109
|
+
@visibleNamespaces = Array.new()
|
110
|
+
@inclusive_namespaces = Array.new()
|
111
|
+
@prefix_list = nil
|
112
|
+
@rendered_prefixes = Array.new()
|
113
|
+
@logger = Logger.new("xmlcanonicalizer.log")
|
114
|
+
@logger.level = Logger::DEBUG
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_inclusive_namespaces(prefix_list, element, visible_namespaces)
|
118
|
+
namespaces = element.attributes()
|
119
|
+
namespaces.each_attribute{|ns|
|
120
|
+
if (ns.prefix=="xmlns")
|
121
|
+
if (prefix_list.include?(ns.local_name()))
|
122
|
+
visible_namespaces.push(NamespaceNode.new("xmlns:"+ns.local_name(), ns.value()))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
}
|
126
|
+
parent = element.parent()
|
127
|
+
add_inclusive_namespaces(prefix_list, parent, visible_namespaces) if (parent)
|
128
|
+
visible_namespaces
|
129
|
+
end
|
130
|
+
|
131
|
+
def canonicalize(document)
|
132
|
+
write_document_node(document)
|
133
|
+
@logger.debug("\nCanonicalized result\n" + @res.to_s()) if @logger
|
134
|
+
@res
|
135
|
+
end
|
136
|
+
|
137
|
+
def canonicalize_element(element, logging = true)
|
138
|
+
@logger.debug("Canonicalize element:\n" + element.to_s()) if @logger
|
139
|
+
@inclusive_namespaces = add_inclusive_namespaces(@prefix_list, element, @inclusive_namespaces) if (@prefix_list)
|
140
|
+
@preserve_document = element.document()
|
141
|
+
tmp_parent = element.parent()
|
142
|
+
body_string = remove_whitespace(element.to_s().gsub("\n","").gsub("\t","").gsub("\r",""))
|
143
|
+
document = Document.new(body_string)
|
144
|
+
tmp_parent.delete_element(element)
|
145
|
+
element = tmp_parent.add_element(document.root())
|
146
|
+
@preserve_element = element
|
147
|
+
document = Document.new(element.to_s())
|
148
|
+
ns = element.namespace(element.prefix())
|
149
|
+
document.root().add_namespace(element.prefix(), ns)
|
150
|
+
write_document_node(document)
|
151
|
+
@logger.debug("Canonicalized result:\n" + @res.to_s()) if @logger
|
152
|
+
@res
|
153
|
+
end
|
154
|
+
|
155
|
+
def write_document_node(document)
|
156
|
+
@state = BEFORE_DOC_ELEMENT
|
157
|
+
if (document.class().to_s() == "REXML::Element")
|
158
|
+
write_node(document)
|
159
|
+
else
|
160
|
+
document.each_child{|child|
|
161
|
+
write_node(child)
|
162
|
+
}
|
163
|
+
end
|
164
|
+
@res
|
165
|
+
end
|
166
|
+
|
167
|
+
def write_node(node)
|
168
|
+
visible = is_node_visible(node)
|
169
|
+
if ((node.node_type() == :text) && white_text?(node.value()))
|
170
|
+
res = node.value()
|
171
|
+
res.gsub("\r\n","\n")
|
172
|
+
#res = res.delete(" ").delete("\t")
|
173
|
+
res.delete("\r")
|
174
|
+
@res = @res + res
|
175
|
+
#write_text_node(node,visible) if (@state == INSIDE_DOC_ELEMENT)
|
176
|
+
return
|
177
|
+
end
|
178
|
+
if (node.node_type() == :text)
|
179
|
+
write_text_node(node, visible)
|
180
|
+
return
|
181
|
+
end
|
182
|
+
if (node.node_type() == :element)
|
183
|
+
write_element_node(node, visible) if (!node.rendered?())
|
184
|
+
node.rendered=(true)
|
185
|
+
end
|
186
|
+
if (node.node_type() == :processing_instruction)
|
187
|
+
end
|
188
|
+
if (node.node_type() == :comment)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def write_element_node(node, visible)
|
193
|
+
savedPrevVisibleNamespacesStart = @prevVisibleNamespacesStart
|
194
|
+
savedPrevVisibleNamespacesEnd = @prevVisibleNamespacesEnd
|
195
|
+
savedVisibleNamespacesSize = @visibleNamespaces.size()
|
196
|
+
state = @state
|
197
|
+
state = INSIDE_DOC_ELEMENT if (visible && state == BEFORE_DOC_ELEMENT)
|
198
|
+
@res = @res + "<" + node.expanded_name() if (visible)
|
199
|
+
write_namespace_axis(node, visible)
|
200
|
+
write_attribute_axis(node)
|
201
|
+
@res = @res + ">" if (visible)
|
202
|
+
node.each_child{|child|
|
203
|
+
write_node(child)
|
204
|
+
}
|
205
|
+
@res = @res + "</" +node.expanded_name() + ">" if (visible)
|
206
|
+
@state = AFTER_DOC_ELEMENT if (visible && state == BEFORE_DOC_ELEMENT)
|
207
|
+
@prevVisibleNamespacesStart = savedPrevVisibleNamespacesStart
|
208
|
+
@prevVisibleNamespacesEnd = savedPrevVisibleNamespacesEnd
|
209
|
+
@visibleNamespaces.slice!(savedVisibleNamespacesSize, @visibleNamespaces.size() - savedVisibleNamespacesSize) if (@visibleNamespaces.size() > savedVisibleNamespacesSize)
|
210
|
+
end
|
211
|
+
|
212
|
+
def write_namespace_axis(node, visible)
|
213
|
+
doc = node.document()
|
214
|
+
has_empty_namespace = false
|
215
|
+
list = Array.new()
|
216
|
+
cur = node
|
217
|
+
#while ((cur != nil) && (cur != doc) && (cur.node_type() != :document))
|
218
|
+
namespaces = cur.node_namespaces()
|
219
|
+
namespaces.each{|prefix|
|
220
|
+
next if ((prefix == "xmlns") && (node.namespace(prefix) == ""))
|
221
|
+
namespace = cur.namespace(prefix)
|
222
|
+
next if (is_namespace_node(namespace))
|
223
|
+
next if (node.namespace(prefix) != cur.namespace(prefix))
|
224
|
+
next if (prefix == "xml" && namespace == "http://www.w3.org/XML/1998/namespace")
|
225
|
+
next if (!is_node_visible(cur))
|
226
|
+
rendered = is_namespace_rendered(prefix, namespace)
|
227
|
+
@visibleNamespaces.push(NamespaceNode.new("xmlns:"+prefix,namespace)) if (visible)
|
228
|
+
if ((!rendered) && !list.include?(prefix))
|
229
|
+
list.push(prefix)
|
230
|
+
end
|
231
|
+
has_empty_namespace = true if (prefix == nil)
|
232
|
+
}
|
233
|
+
if (visible && !has_empty_namespace && !is_namespace_rendered(nil, nil))
|
234
|
+
@res = @res + ' xmlns=""'
|
235
|
+
end
|
236
|
+
#TODO: ns of inclusive_list
|
237
|
+
#=begin
|
238
|
+
if ((@prefix_list) && (node.to_s() == node.parent().to_s()))
|
239
|
+
#list.push(node.prefix())
|
240
|
+
@inclusive_namespaces.each{|ns|
|
241
|
+
prefix = ns.prefix().split(":")[1]
|
242
|
+
list.push(prefix) if (!list.include?(prefix) && (!node.attributes.prefixes.include?(prefix)))
|
243
|
+
}
|
244
|
+
@prefix_list = nil
|
245
|
+
end
|
246
|
+
#=end
|
247
|
+
list.sort!()
|
248
|
+
list.insert(0, "xmlns") unless list.delete("xmlns").nil?
|
249
|
+
list.each{|prefix|
|
250
|
+
next if (prefix == "")
|
251
|
+
next if (@rendered_prefixes.include?(prefix))
|
252
|
+
@rendered_prefixes.push(prefix)
|
253
|
+
ns = node.namespace(prefix)
|
254
|
+
ns = @preserve_element.namespace(prefix) if (ns == nil)
|
255
|
+
@res = @res + normalize_string(" " + prefix + '="' + ns + '"', NODE_TYPE_TEXT) if (prefix == "xmlns")
|
256
|
+
@res = @res + normalize_string(" xmlns:" + prefix + '="' + ns + '"', NODE_TYPE_TEXT) if (prefix != nil && prefix != "xmlns")
|
257
|
+
}
|
258
|
+
if (visible)
|
259
|
+
@prevVisibleNamespacesStart = @prevVisibleNamespacesEnd
|
260
|
+
@prevVisibleNamespacesEnd = @visibleNamespaces.size()
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def write_attribute_axis(node)
|
265
|
+
list = Array.new()
|
266
|
+
node.attributes().sort.each{|key, attr|
|
267
|
+
list.push(attr) if (!is_namespace_node(attr.value()) && !is_namespace_decl(attr)) # && is_node_visible(
|
268
|
+
}
|
269
|
+
if (!@exclusive && node.parent() != nil && node.parent().parent() != nil)
|
270
|
+
cur = node.parent()
|
271
|
+
while (cur != nil)
|
272
|
+
#next if (cur.attributes() == nil)
|
273
|
+
cur.each_attribute{|attribute|
|
274
|
+
next if (attribute.prefix() != "xml")
|
275
|
+
next if (attribute.prefix().index("xmlns") == 0)
|
276
|
+
next if (node.namespace(attribute.prefix()) == attribute.value())
|
277
|
+
found = true
|
278
|
+
list.each{|n|
|
279
|
+
if (n.prefix() == "xml" && n.value() == attritbute.value())
|
280
|
+
found = true
|
281
|
+
break
|
282
|
+
end
|
283
|
+
}
|
284
|
+
next if (found)
|
285
|
+
list.push(attribute)
|
286
|
+
}
|
287
|
+
end
|
288
|
+
end
|
289
|
+
list.each{|attribute|
|
290
|
+
if (attribute != nil)
|
291
|
+
if (attribute.name() != "xmlns")
|
292
|
+
@res = @res + " " + normalize_string(attribute.to_string(), NODE_TYPE_ATTRIBUTE).gsub("'",'"')
|
293
|
+
end
|
294
|
+
# else
|
295
|
+
# @res = @res + " " + normalize_string(attribute.name()+'="'+attribute.to_s()+'"', NODE_TYPE_ATTRIBUTE).gsub("'",'"')
|
296
|
+
#end
|
297
|
+
end
|
298
|
+
}
|
299
|
+
end
|
300
|
+
|
301
|
+
def is_namespace_node(namespace_uri)
|
302
|
+
return (namespace_uri == "http://www.w3.org/2000/xmlns/")
|
303
|
+
end
|
304
|
+
|
305
|
+
def is_namespace_rendered(prefix, uri)
|
306
|
+
is_empty_ns = prefix == nil && uri == nil
|
307
|
+
if (is_empty_ns)
|
308
|
+
start = 0
|
309
|
+
else
|
310
|
+
start = @prevVisibleNamespacesStart
|
311
|
+
end
|
312
|
+
@visibleNamespaces.each{|ns|
|
313
|
+
if (ns.prefix() == "xmlns:"+prefix.to_s() && ns.uri() == uri)
|
314
|
+
return true
|
315
|
+
end
|
316
|
+
}
|
317
|
+
return is_empty_ns
|
318
|
+
#(@visibleNamespaces.size()-1).downto(start) {|i|
|
319
|
+
# ns = @visibleNamespaces[i]
|
320
|
+
# return true if (ns.prefix() == "xmlns:"+prefix.to_s() && ns.uri() == uri)
|
321
|
+
# #p = ns.prefix() if (ns.prefix().index("xmlns") == 0)
|
322
|
+
# #return ns.uri() == uri if (p == prefix)
|
323
|
+
#}
|
324
|
+
#return is_empty_ns
|
325
|
+
end
|
326
|
+
|
327
|
+
def is_node_visible(node)
|
328
|
+
return true if (@xnl.size() == 0)
|
329
|
+
@xnl.each{|element|
|
330
|
+
return true if (element == node)
|
331
|
+
}
|
332
|
+
return false
|
333
|
+
end
|
334
|
+
|
335
|
+
def normalize_string(input, type)
|
336
|
+
sb = ""
|
337
|
+
return input
|
338
|
+
end
|
339
|
+
#input.each_byte{|b|
|
340
|
+
# if (b ==60 && (type == NODE_TYPE_ATTRIBUTE || is_text_node(type)))
|
341
|
+
# sb = sb + "<"
|
342
|
+
# elsif (b == 62 && is_text_node(type))
|
343
|
+
# sb = sb + ">"
|
344
|
+
# elsif (b == 38 && (is_text_node(type) || is_text_node(type))) #Ampersand
|
345
|
+
# sb = sb + "&"
|
346
|
+
# elsif (b == 34 && is_text_node(type)) #Quote
|
347
|
+
# sb = sb + """
|
348
|
+
# elsif (b == 9 && is_text_node(type)) #Tabulator
|
349
|
+
# sb = sb + "	"
|
350
|
+
# elsif (b == 11 && is_text_node(type)) #CR
|
351
|
+
# sb = sb + "
"
|
352
|
+
# elsif (b == 13 && (type == NODE_TYPE_ATTRIBUTE || (is_text_node(type) && type != NODE_TYPE_WHITESPACE) || type == NODE_TYPE_COMMENT || type == NODE_TYPE_PI))
|
353
|
+
# sb = sb + "
"
|
354
|
+
# elsif (b == 13)
|
355
|
+
# next
|
356
|
+
# else
|
357
|
+
# sb = sb.concat(b)
|
358
|
+
# end
|
359
|
+
#}
|
360
|
+
#sb
|
361
|
+
#end
|
362
|
+
|
363
|
+
def write_text_node(node, visible)
|
364
|
+
if (visible)
|
365
|
+
@res = @res + normalize_string(node.value(), node.node_type())
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def white_text?(text)
|
370
|
+
return true if ((text.strip() == "") || (text.strip() == nil))
|
371
|
+
return false
|
372
|
+
end
|
373
|
+
|
374
|
+
def is_namespace_decl(attribute)
|
375
|
+
#return true if (attribute.name() == "xmlns")
|
376
|
+
return true if (attribute.prefix().index("xmlns") == 0)
|
377
|
+
return false
|
378
|
+
end
|
379
|
+
|
380
|
+
def is_text_node(type)
|
381
|
+
return true if (type == NODE_TYPE_TEXT || type == NODE_TYPE_CDATA || type == NODE_TYPE_WHITESPACE)
|
382
|
+
return false
|
383
|
+
end
|
384
|
+
|
385
|
+
def remove_whitespace(string)
|
386
|
+
new_string = ""
|
387
|
+
in_white = false
|
388
|
+
string.each_byte{|b|
|
389
|
+
#if (in_white && b == 32)
|
390
|
+
#else
|
391
|
+
if !(in_white && b == 32)
|
392
|
+
new_string = new_string + b.chr()
|
393
|
+
end
|
394
|
+
if (b == 62) #>
|
395
|
+
in_white = true
|
396
|
+
end
|
397
|
+
if (b == 60) #<
|
398
|
+
in_white = false
|
399
|
+
end
|
400
|
+
}
|
401
|
+
new_string
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'google-sso'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
class TestSSO < Test::Unit::TestCase
|
7
|
+
RelayState = 'https://www.google.com/a/davidpalm.net/ServiceLogin?service=mail&passive=true&rm=false&continue=http%3A%2F%2Fmail.google.com%2Fa%2Fdavidpalm.net<mpl=default<mplcache=2'
|
8
|
+
SAMLRequest_encoded = 'fVJLT8MwDL4j8R+i3NeuIAREa6cBQkziUW2FA7cscduMNC5xusG/p+tAwAGknJzP38P2ZPrWWLYBTwZdypNozBk4hdq4KuWPxfXojE+zw4MJyca2YtaF2i3gtQMKrO90JIaPlHfeCZRkSDjZAImgxHJ2dyuOorFoPQZUaDmbX6X8RSqrDSpnpUZZrY0tS21rXFftql6bF12ZtqwVcvb0ZetoZ2tO1MHcUZAu9KXx+HQ0vCJJxPGJOD5/5iz/VLowbp/gP1urPYjETVHko/xhWQwEG6PB3/folFeIlYVIYcPZjAh86O1coqOuAb8EvzEKHhe3Ka9DaEnE8Xa7jb6bYhlr2dO10jaRgxBLRbscuSQym56/lJaAZ8NwxZDP/5jq/+7llx+efStO4h9U2efSdlnmVzlao97ZzFrcXnqQodcPvgPOrtE3MvytlkTJUDF6VA5Q0TlqQZnSgOYszvaqv6+jv5kP'
|
9
|
+
SAMLRequest_decoded = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"kacldiocnladoagjilffdlhojgpbhjikdgipfhco\" Version=\"2.0\" IssueInstant=\"2007-07-07T11:35:39Z\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" ProviderName=\"google.com\" AssertionConsumerServiceURL=\"https://www.google.com/a/davidpalm.net/acs\" IsPassive=\"false\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">google.com</saml:Issuer><samlp:NameIDPolicy AllowCreate=\"true\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\" /></samlp:AuthnRequest>\r\n"
|
10
|
+
|
11
|
+
SAMLResponse = '<?xml version=\"1.0\" encoding=\"UTF-8\"?><samlp:Response ID=\"E4B3A6F788CA08C40ED6DC62B8B1A1CF026C05CA51\" IssueInstant=\"2007-07-07T13:54:39Z\" Version=\"2.0\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:xenc=\"http://www.w3.org/2001/04/xmlenc#\"><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status><Assertion ID=\"CB811AFE287D86586677DF7B7BCA7F278840330164\" IssueInstant=\"2007-07-07T13:54:39Z\" Version=\"2.0\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\"><Issuer>https://www.opensaml.org/IDP</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress\">sso</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"/></Subject><Conditions NotBefore=\"2007-07-07T11:35:39Z\" NotOnOrAfter=\"2007-07-07T14:14:39Z\"/><AuthnStatement AuthnInstant=\"2007-07-07T13:54:39Z\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion></samlp:Response>'
|
12
|
+
|
13
|
+
SAMLResponse_re = /<\?xml version=\"1\.0\" encoding=\"UTF-8\"\?><samlp:Response ID=\"[A-Z]{1}[A-Z0-9]{41}\" IssueInstant=\"[\d]{4}-[\d]{2}-[\d]{2}[A-Z]{1}[\d]{2}:[\d]{2}:[\d]{2}[A-Z]{1}\" Version=\"2\.0\" xmlns=\"urn:oasis:names:tc:SAML:2\.0:assertion\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2\.0:protocol\" xmlns:xenc=\"http:\/\/www\.w3\.org\/2001\/04\/xmlenc#\"><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2\.0:status:Success\"\/><\/samlp:Status><Assertion ID=\"[A-Z]{1}[A-Z0-9]{41}\" IssueInstant=\"[\d]{4}-[\d]{2}-[\d]{2}[A-Z]{1}[\d]{2}:[\d]{2}:[\d]{2}[A-Z]{1}\" Version=\"2\.0\" xmlns=\"urn:oasis:names:tc:SAML:2\.0:assertion\"><Issuer>https:\/\/www\.opensaml\.org\/IDP<\/Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:2\.0:nameid-format:emailAddress\">sso<\/NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2\.0:cm:bearer\"\/><\/Subject><Conditions NotBefore=\"\" NotOnOrAfter=\"[\d]{4}-[\d]{2}-[\d]{2}[A-Z]{1}[\d]{2}:[\d]{2}:[\d]{2}[A-Z]{1}\"\/><AuthnStatement AuthnInstant=\"[\d]{4}-[\d]{2}-[\d]{2}[A-Z]{1}[\d]{2}:[\d]{2}:[\d]{2}[A-Z]{1}\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2\.0:ac:classes:Password<\/AuthnContextClassRef><\/AuthnContext><\/AuthnStatement><\/Assertion><\/samlp:Response>/
|
14
|
+
|
15
|
+
Signature_re = /<Signature xmlns='http:\/\/www\.w3\.org\/2000\/09\/xmldsig#'>\n<SignedInfo>\n<CanonicalizationMethod Algorithm='http:\/\/www\.w3\.org\/TR\/2001\/REC-xml-c14n-20010315#WithComments'\/>\n<SignatureMethod Algorithm='http:\/\/www\.w3\.org\/2000\/09\/xmldsig#rsa-sha1'\/>\n<Reference URI=''>\n<Transforms>\n<Transform Algorithm='http:\/\/www\.w3\.org\/2000\/09\/xmldsig#enveloped-signature'\/>\n<\/Transforms>\n<DigestMethod Algorithm='http:\/\/www\.w3\.org\/2000\/09\/xmldsig#sha1'\/>\n<DigestValue>.{28}<\/DigestValue>\n<\/Reference>\n<\/SignedInfo>\n<SignatureValue>.*<\/SignatureValue>\n<KeyInfo>\n<X509Data>\n<X509Certificate>.*=\n<\/X509Certificate>\n<\/X509Data>\n<\/KeyInfo>\n<\/Signature>/m
|
16
|
+
|
17
|
+
Username = 'sso'
|
18
|
+
|
19
|
+
FakeCACertPath = File.dirname(__FILE__)+'/fake_cacert.pem'
|
20
|
+
FakePrivKeyPath = File.dirname(__FILE__)+'/fake_privkey.pem'
|
21
|
+
|
22
|
+
def setup
|
23
|
+
@sso = XmlProcessResponse.new(FakeCACertPath, FakePrivKeyPath)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_decode_authn_request
|
27
|
+
saml_request_decoded = @sso.send :decode_authn_request, SAMLRequest_encoded
|
28
|
+
assert_equal saml_request_decoded, SAMLRequest_decoded
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_get_request_attributes
|
32
|
+
acs = @sso.send(:get_request_attributes, (@sso.send :decode_authn_request, SAMLRequest_encoded))
|
33
|
+
assert_match(/https:\/\/www.google.com\/a\/.*\/acs/, acs)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_create_saml_response
|
37
|
+
saml_response = @sso.send(:create_saml_response, Username)
|
38
|
+
assert_match SAMLResponse_re, saml_response
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_sign_XML
|
42
|
+
signed = @sso.send(:sign_XML, @sso.send(:create_saml_response, Username))
|
43
|
+
assert_match Signature_re, signed
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.2
|
3
|
+
specification_version: 1
|
4
|
+
name: google-sso
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 1.0.2
|
7
|
+
date: 2007-07-07 00:00:00 +02:00
|
8
|
+
summary: Google Apps Single Sign-on
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: google-sso@elctech.com
|
12
|
+
homepage: http://elctech.com
|
13
|
+
rubyforge_project: google-sso
|
14
|
+
description: Google Apps Single Sign-on (SSO) support.
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
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
|
+
- Thomas Ormerod, David Palm, and Ryan Garver
|
31
|
+
files:
|
32
|
+
- Manifest.txt
|
33
|
+
- README.txt
|
34
|
+
- Rakefile
|
35
|
+
- lib/SamlResponseTemplate.xml
|
36
|
+
- lib/SignatureTemplate.xml
|
37
|
+
- lib/google-sso.rb
|
38
|
+
- lib/google-sso/version.rb
|
39
|
+
- lib/processresponse.rb
|
40
|
+
- lib/xmlcanonicalizer.rb
|
41
|
+
- test/test_GoogleSSO.rb
|
42
|
+
test_files:
|
43
|
+
- test/test_GoogleSSO.rb
|
44
|
+
rdoc_options:
|
45
|
+
- --main
|
46
|
+
- README.txt
|
47
|
+
extra_rdoc_files:
|
48
|
+
- Manifest.txt
|
49
|
+
- README.txt
|
50
|
+
executables: []
|
51
|
+
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
dependencies:
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: hoe
|
59
|
+
version_requirement:
|
60
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 1.2.1
|
65
|
+
version:
|