ruby-openid-apps-discovery 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/lib/ca-bundle.crt +2794 -0
- data/lib/gapps_openid.rb +315 -0
- metadata +67 -0
data/lib/gapps_openid.rb
ADDED
@@ -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
|
+
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-openid-apps-discovery
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors: []
|
7
|
+
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-05-13 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: ruby-openid
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.1.7
|
24
|
+
version:
|
25
|
+
description: |
|
26
|
+
Extension to ruby-openid that enables discovery for Google Apps domains
|
27
|
+
|
28
|
+
email:
|
29
|
+
executables: []
|
30
|
+
|
31
|
+
extensions: []
|
32
|
+
|
33
|
+
extra_rdoc_files: []
|
34
|
+
|
35
|
+
files:
|
36
|
+
- lib/gapps_openid.rb
|
37
|
+
- lib/ca-bundle.crt
|
38
|
+
has_rdoc: true
|
39
|
+
homepage:
|
40
|
+
licenses: []
|
41
|
+
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.3.5
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: Google Apps support for ruby-openid
|
66
|
+
test_files: []
|
67
|
+
|