gappster 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,315 @@
1
+ require "openid"
2
+ require "openid/fetchers"
3
+ require "openid/consumer/discovery"
4
+ require 'rexml/document'
5
+ require 'rexml/element'
6
+ require 'rexml/xpath'
7
+ require 'openssl'
8
+ require 'base64'
9
+
10
+ # Copyright 2009 Google Inc.
11
+ #
12
+ # Licensed under the Apache License, Version 2.0 (the "License")
13
+ # you may not use this file except in compliance with the License.
14
+ # You may obtain a copy of the License at
15
+ #
16
+ # http://www.apache.org/licenses/LICENSE-2.0
17
+ #
18
+ # Unless required by applicable law or agreed to in writing, software
19
+ # distributed under the License is distributed on an "AS IS" BASIS,
20
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ # See the License for the specific language governing permissions and
22
+ # limitations under the License.
23
+
24
+ # Extends ruby-openid to support the discovery protocol used by Google Apps. Usage is
25
+ # generally simple. Where using ruby-openid's Consumer, add the line
26
+ #
27
+ # require 'gapps_openid'
28
+ #
29
+ # Caching of discovery information is enabled when used with rails. In other environments,
30
+ # a cache can be set via:
31
+ #
32
+ # OpenID.cache = ...
33
+ #
34
+ # The cache must implement methods read(key) and write(key,value)
35
+ #
36
+ # Similarly, logging will attempt to use the default Rail's logger, but can be overriden
37
+ # by calling
38
+ #
39
+ # OpenID.logger = ...
40
+ #
41
+ # The logger must respond to warn, debug, and info methods
42
+ #
43
+ # In some cases additional setup is required, particularly to set the location of trusted
44
+ # root certificates for validating XRDS signatures. If standard locations don't work, additional
45
+ # files and directories can be added via:
46
+ #
47
+ # OpenID::SimpleSign.store.add_file(path_to_cacert_pem)
48
+ #
49
+ # or
50
+ #
51
+ # OpenID::SimpleSign.store.add_path(path_to_ca_dir)
52
+ #
53
+ # TODO:
54
+ # - Memcache support for caching host-meta and site XRDS docs
55
+ # - Better packaging (gem/rails)
56
+ module OpenID
57
+
58
+ class << self
59
+ alias_method :default_discover, :discover
60
+ attr_accessor :cache, :logger
61
+ end
62
+
63
+ def self.discover(uri)
64
+ discovery = GoogleDiscovery.new
65
+ info = discovery.perform_discovery(uri)
66
+ if not info.nil?
67
+ OpenID.logger.debug("Discovery info = #{info}") unless OpenID.logger.nil?
68
+ return info
69
+ end
70
+ return self.default_discover(uri)
71
+ end
72
+
73
+ # Handles the bulk of Google's modified discovery prototcol
74
+ # See http://groups.google.com/group/google-federated-login-api/web/openid-discovery-for-hosted-domains
75
+ class GoogleDiscovery
76
+
77
+ OpenID.cache = RAILS_CACHE rescue nil
78
+ OpenID.logger = RAILS_DEFAULT_LOGGER rescue nil
79
+
80
+ NAMESPACES = {
81
+ 'xrds' => 'xri://$xrd*($v*2.0)',
82
+ 'xrd' => 'xri://$xrds',
83
+ 'openid' => 'http://namespace.google.com/openid/xmlns'
84
+ }
85
+
86
+ # Main entry point for discovery. Attempts to detect whether or not the URI is a raw domain name ('mycompany.com')
87
+ # vs. a user's claimed ID ('http://mycompany.com/openid?id=12345') and performs the site or user discovery appropriately
88
+ def perform_discovery(uri)
89
+ OpenID.logger.debug("Performing discovery for #{uri}") unless OpenID.logger.nil?
90
+ begin
91
+ domain = uri
92
+ parsed_uri = URI::parse(uri)
93
+ domain = parsed_uri.host unless parsed_uri.host.nil?
94
+ if site_identifier?(parsed_uri)
95
+ return discover_site(domain)
96
+ end
97
+ return discover_user(domain, uri)
98
+ rescue Exception => e
99
+ # If we fail, just return nothing and fallback on default discovery mechanisms
100
+ OpenID.logger.warn("Unexpected exception performing discovery for id #{uri}: #{e}") unless OpenID.logger.nil?
101
+ return nil
102
+ end
103
+ end
104
+
105
+ def site_identifier?(parsed_uri)
106
+ return parsed_uri.scheme.nil? || parsed_uri.path.nil? || parsed_uri.path.strip.empty?
107
+ end
108
+
109
+ # Handles discovery for a user's claimed ID.
110
+ def discover_user(domain, claimed_id)
111
+ OpenID.logger.debug("Discovering user identity #{claimed_id} for domain #{domain}") unless OpenID.logger.nil?
112
+ url = fetch_host_meta(domain)
113
+ if url.nil?
114
+ OpenID.logger.debug("#{domain} is not a Google Apps domain, aborting") unless OpenID.logger.nil?
115
+ return nil # Not a Google Apps domain
116
+ end
117
+
118
+ xrds, signed = fetch_secure_xrds(domain, url)
119
+
120
+ unless xrds.nil?
121
+ # TODO - Need to propogate secure discovery info up through stack
122
+ user_url, authority = get_user_xrds_url(xrds, claimed_id)
123
+ user_xrds, signed = fetch_secure_xrds(domain, user_url, false)
124
+
125
+ # No user xrds -- likely that identifier was just OP identifier
126
+ if user_xrds.nil?
127
+ endpoints = OpenID::OpenIDServiceEndpoint.from_xrds(domain, xrds)
128
+ return [claimed_id, OpenID.get_op_or_user_services(endpoints)]
129
+ end
130
+
131
+ endpoints = OpenID::OpenIDServiceEndpoint.from_xrds(claimed_id, user_xrds)
132
+ return [claimed_id, OpenID.get_op_or_user_services(endpoints)]
133
+ end
134
+ end
135
+
136
+ # Handles discovery for a domain
137
+ def discover_site(domain)
138
+ OpenID.logger.debug("Discovering domain #{domain}") unless OpenID.logger.nil?
139
+ url = fetch_host_meta(domain)
140
+ if url.nil?
141
+ OpenID.logger.debug("#{domain} is not a Google Apps domain, aborting") unless OpenID.logger.nil?
142
+ return nil # Not a Google Apps domain
143
+ end
144
+ xrds, secure = fetch_secure_xrds(domain, url)
145
+
146
+ unless xrds.nil?
147
+ # TODO - Need to propogate secure discovery info up through stack
148
+ endpoints = OpenID::OpenIDServiceEndpoint.from_xrds(domain, xrds)
149
+ return [domain, OpenID.get_op_or_user_services(endpoints)]
150
+ end
151
+ return nil
152
+ end
153
+
154
+ # Kickstart the discovery process by checking against Google's well-known location for hosted domains.
155
+ # This gives us the location of the site's XRDS doc
156
+ def fetch_host_meta(domain)
157
+ cached_value = get_cache(domain)
158
+ return cached_value unless cached_value.nil?
159
+
160
+ host_meta_url = "https://www.google.com/accounts/o8/.well-known/host-meta?hd=#{CGI::escape(domain)}"
161
+ http_resp = fetch_url(host_meta_url)
162
+ return nil if http_resp.nil?
163
+
164
+ matches = /Link: <(.*)>/.match( http_resp.body )
165
+ if matches.nil?
166
+ OpenID.logger.debug("No link tag found at #{host_meta_url}") unless OpenID.logger.nil?
167
+ return nil
168
+ end
169
+ put_cache(domain, matches[1])
170
+ return matches[1]
171
+ end
172
+
173
+ def fetch_url(url)
174
+ http_resp = OpenID.fetch(url)
175
+ if http_resp.code != "200" and http_resp.code != "206"
176
+ OpenID.logger.debug("Received #{http_resp.code} when fetching #{url}") unless OpenID.logger.nil?
177
+ return nil
178
+ end
179
+ return http_resp
180
+ end
181
+
182
+ # Fetches the XRDS and verifies the signature and authority for the doc
183
+ def fetch_secure_xrds(authority, url, cache=true)
184
+ return if url.nil?
185
+
186
+ OpenID.logger.debug("Retrieving XRDS from #{url}") unless OpenID.logger.nil?
187
+
188
+ cached_xrds = get_cache("XRDS_#{url}")
189
+ return cached_xrds unless cached_xrds.nil?
190
+
191
+ http_resp = fetch_url(url)
192
+ return nil if http_resp.nil?
193
+
194
+ body = http_resp.body
195
+ put_cache("XRDS_#{url}", body)
196
+
197
+ signature = http_resp["Signature"]
198
+ signed_by = SimpleSign.verify(body, signature)
199
+
200
+ if signed_by.nil?
201
+ put_cache("XRDS_#{url}", body) if cache
202
+ return [body, false]
203
+ elsif signed_by.casecmp(authority) || signed_by.casecmp('hosted-id.google.com')
204
+ put_cache("XRDS_#{url}", body) if cache
205
+ return [body, true]
206
+ else
207
+ OpenID.logger.warn("Expected signature from #{authority} but found #{signed_by}") unless OpenID.logger.nil?
208
+ return nil # Signed, but not by the right domain.
209
+ end
210
+ end
211
+
212
+ # Process the URITemplate in the XRDS to derive the location of the claimed id's XRDS
213
+ def get_user_xrds_url(xrds, claimed_id)
214
+ types_to_match = ['http://www.iana.org/assignments/relation/describedby']
215
+ services = OpenID::Yadis::apply_filter(claimed_id, xrds)
216
+ services.each do | service |
217
+ if service.match_types(types_to_match)
218
+ template = REXML::XPath.first(service.service_element, '//openid:URITemplate', NAMESPACES)
219
+ authority = REXML::XPath.first(service.service_element, '//openid:NextAuthority', NAMESPACES)
220
+ url = template.text.gsub('{%uri}', CGI::escape(claimed_id))
221
+ return [url, authority.text]
222
+ end
223
+ end
224
+ end
225
+
226
+ def put_cache(key, item)
227
+ return if OpenID.cache.nil?
228
+ OpenID.cache.write("__GAPPS_OPENID__#{key}", item)
229
+ end
230
+
231
+ def get_cache(key)
232
+ return nil if OpenID.cache.nil?
233
+ return OpenID.cache.read("__GAPPS_OPENID__#{key}")
234
+ end
235
+ end
236
+
237
+ # Basic implementation of the XML Simple Sign algorithm. Currently only supports
238
+ # RSA-SHA1
239
+ class SimpleSign
240
+
241
+ @@store = nil
242
+
243
+ C14N_RAW_OCTETS = 'http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets'
244
+ SIGN_RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
245
+
246
+ NAMESPACES = {
247
+ 'ds' => 'http://www.w3.org/2000/09/xmldsig#',
248
+ 'xrds' => 'xri://xrds'
249
+ }
250
+
251
+ # Initialize the store
252
+ def self.store
253
+ if @@store.nil?
254
+ OpenID.logger.info("Initializing CA bundle") unless OpenID.logger.nil?
255
+ ca_bundle_path = File.join(File.dirname(__FILE__), 'ca-bundle.crt')
256
+ @@store = OpenSSL::X509::Store.new
257
+ @@store.set_default_paths
258
+ @@store.add_file(ca_bundle_path)
259
+ end
260
+ return @@store
261
+ end
262
+
263
+ # Extracts the signer's certificates from the XML
264
+ def self.parse_certificates(doc)
265
+ certs = []
266
+ REXML::XPath.each(doc, "//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate", NAMESPACES ) { | encoded |
267
+ encoded = encoded.text.strip.scan(/.{1,64}/).join("\n")
268
+ encoded = "-----BEGIN CERTIFICATE-----\n#{encoded}\n-----END CERTIFICATE-----\n"
269
+ cert = OpenSSL::X509::Certificate.new(encoded)
270
+ certs << cert
271
+ }
272
+ return certs
273
+ end
274
+
275
+ # Verifies the chain of trust for the signing certificates
276
+ def self.valid_chain?(chain)
277
+ if chain.nil? or chain.empty?
278
+ return false
279
+ end
280
+ cert = chain.shift
281
+ if self.store.verify(cert)
282
+ return true
283
+ end
284
+ if chain.empty? or not cert.verify(chain.first.public_key)
285
+ return false
286
+ end
287
+ return self.valid_chain?(chain)
288
+ end
289
+
290
+ # Verifies the signature of the doc, returning the CN of the signer if valid
291
+ def self.verify(xml, signature_value)
292
+ doc = REXML::Document.new(xml)
293
+
294
+ return nil if REXML::XPath.first(doc, "//ds:Signature").nil? and signature_value.nil?
295
+
296
+ decoded_sig = Base64.decode64(signature_value)
297
+ certs = self.parse_certificates(doc)
298
+ raise "No signature in document" if certs.nil? or certs.empty?
299
+ raise "Missing signature value" if signature_value.nil?
300
+
301
+
302
+ signing_certificate = certs.first
303
+ raise "Invalid signature" if !signing_certificate.public_key.verify(OpenSSL::Digest::SHA1.new, decoded_sig, xml)
304
+ raise "Certificate chain not valid" if !self.valid_chain?(certs)
305
+
306
+ # Signature is valid, return CN of the subject
307
+ subject = signing_certificate.subject.to_a
308
+ signed_by = subject.last[1]
309
+ return signed_by
310
+ end
311
+ end
312
+
313
+ end
314
+
315
+
@@ -0,0 +1,40 @@
1
+ require 'gapps_openid'
2
+ module Gappster
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def gappster_authentication(domain, more_options={})
9
+ options = {
10
+ :openid_url => "https://www.google.com/accounts/o8/site-xrds?hd=#{domain}",
11
+ :session_var => 'gapps_email',
12
+ :success_path => :root,
13
+ :failure_path => :fail
14
+ }
15
+ options.merge(more_options)
16
+
17
+ define_method("authenticate_with_gapps") do
18
+ open_id_authentication(options[:openid_url], options[:success_path], options[:failure_path], options[:session_var])
19
+ end
20
+ end
21
+ end
22
+
23
+ module InstanceMethods
24
+ def open_id_authentication(openid_url, gapps_success_path, gapps_failure_path, session_var)
25
+ authenticate_with_open_id(openid_url, :required => ['http://axschema.org/contact/email']) do |result, identity_url, sreg, ax|
26
+ if result.successful?
27
+ session[session_var] = ax['http://axschema.org/contact/email']
28
+ redirect_to gapps_success_path
29
+ else
30
+ redirect_to gapps_failure_path
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ class ActionController::Base
38
+ include Gappster
39
+ include Gappster::InstanceMethods
40
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gappster
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - David Padilla
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-19 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Google Apps authentication
23
+ email: david@crowdint.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README.markdown
30
+ - lib/ca-bundle.crt
31
+ - lib/gapps_openid.rb
32
+ - lib/gappster.rb
33
+ files:
34
+ - Gemfile
35
+ - README.markdown
36
+ - Rakefile
37
+ - gappster.gemspec
38
+ - init.rb
39
+ - lib/ca-bundle.crt
40
+ - lib/gapps_openid.rb
41
+ - lib/gappster.rb
42
+ - Manifest
43
+ has_rdoc: true
44
+ homepage: http://github.com/crowdint/gappster
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options:
49
+ - --line-numbers
50
+ - --inline-source
51
+ - --title
52
+ - Gappster
53
+ - --main
54
+ - README.markdown
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 11
72
+ segments:
73
+ - 1
74
+ - 2
75
+ version: "1.2"
76
+ requirements: []
77
+
78
+ rubyforge_project: gappster
79
+ rubygems_version: 1.3.7
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Google Apps authentication
83
+ test_files: []
84
+