openid 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +26 -0
- data/README +56 -0
- data/examples/httpconsumer.rb +126 -0
- data/lib/openid/association.rb +222 -0
- data/lib/openid/constants.rb +5 -0
- data/lib/openid/consumer.rb +207 -0
- data/lib/openid/errors.rb +25 -0
- data/lib/openid/interface.rb +65 -0
- data/lib/openid/util.rb +145 -0
- metadata +48 -0
data/COPYING
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Ruby OpenID
|
2
|
+
|
3
|
+
Copyright (C) 2005 Mark Quinn
|
4
|
+
|
5
|
+
This library is free software; you can redistribute it and/or
|
6
|
+
modify it under the terms of the GNU Lesser General Public
|
7
|
+
License as published by the Free Software Foundation; either
|
8
|
+
version 2.1 of the License, or (at your option) any later version.
|
9
|
+
|
10
|
+
This library is distributed in the hope that it will be useful,
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
Lesser General Public License for more details.
|
14
|
+
|
15
|
+
You should have received a copy of the GNU Lesser General Public
|
16
|
+
License along with this library; if not, write to the Free Software
|
17
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
|
+
|
19
|
+
You also have the option of using this library under the terms of
|
20
|
+
Ruby's license.
|
21
|
+
|
22
|
+
More info about Ruby OpenID:
|
23
|
+
http://openid.rubyforge.org/
|
24
|
+
|
25
|
+
More info about OpenID:
|
26
|
+
http://www.openid.net/
|
data/README
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
OpenID for Ruby
|
2
|
+
===============
|
3
|
+
[Mark Quinn](mailto:mmq@dekinai.com)
|
4
|
+
|
5
|
+
Version: 0.0.1
|
6
|
+
July 10, 2005
|
7
|
+
|
8
|
+
License: LGPL or Ruby's license
|
9
|
+
|
10
|
+
|
11
|
+
- Homepage: http://openid.rubyforge.org/
|
12
|
+
- OpenID Homepage: http://openid.net/
|
13
|
+
|
14
|
+
|
15
|
+
Currently this only implements a consumer module. See examples/httpconsumer.rb for an example.
|
16
|
+
|
17
|
+
This is the first release, so there may be some API changes (and, notably, it will need to be modified for the coming/recent OpenID spec changes regarding times)
|
18
|
+
|
19
|
+
ACKNOWLEDGMENTS
|
20
|
+
---------------
|
21
|
+
|
22
|
+
This project owes a great deal to the work done over at schtuff.com on [python modules](http://openid.schtuff.com/). The structure and code of this module and examples/httpconsumer.rb borrows heavily from them. Thanks guys!
|
23
|
+
|
24
|
+
Big thanks to the creators of OpenID, and everybody on the mailing list (I'm watching you!)
|
25
|
+
|
26
|
+
Thanks to Google for funding open source, this is part of the Google Summer of Code 2005.
|
27
|
+
|
28
|
+
|
29
|
+
PREREQUISITES
|
30
|
+
-------------
|
31
|
+
|
32
|
+
+ Ruby 1.8
|
33
|
+
+ rake (if installing the gem)
|
34
|
+
+ [htmltokenizer](http://rubyforge.org/projects/htmltokenizer/)
|
35
|
+
+ [ruby-hmac](http://deisui.org/~ueno/ruby/hmac.html)
|
36
|
+
|
37
|
+
INSTALLATION
|
38
|
+
------------
|
39
|
+
|
40
|
+
The easiest way to install is by using the gem, available at rubyforge.
|
41
|
+
|
42
|
+
Get rubygems and rake, if you don't have them (you can install rake with `gem install rake`).
|
43
|
+
|
44
|
+
Unfortunately, there are no gems for htmltokenizer and ruby-hmac yet, so you must install them manually.
|
45
|
+
|
46
|
+
Once that is done, `gem install openid` should do the trick.
|
47
|
+
|
48
|
+
|
49
|
+
CHANGES
|
50
|
+
-------
|
51
|
+
|
52
|
+
### Changes from 0.0.0 ###
|
53
|
+
|
54
|
+
* Brand new
|
55
|
+
|
56
|
+
(This document is formatted with Markdown)
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'webrick'
|
2
|
+
include WEBrick
|
3
|
+
|
4
|
+
require 'openid/consumer'
|
5
|
+
require 'openid/association'
|
6
|
+
require 'openid/interface'
|
7
|
+
|
8
|
+
module OpenID
|
9
|
+
# First we need to define an AssociationManager to keep track of all
|
10
|
+
# associations. This one is basically a copy of the one from httpconsumer.py
|
11
|
+
# in the python module examples. It is not well designed, but works for an
|
12
|
+
# example. I placed this in the OpenID namespace so it has easy access to
|
13
|
+
# everything from the modules, without having to pull everything in.
|
14
|
+
# Maybe that is bad form.
|
15
|
+
class DictionaryAssociationManager < BaseAssociationManager
|
16
|
+
def initialize()
|
17
|
+
associator = DiffieHelmanAssociator.new(OpenID::SimpleHTTPClient.new)
|
18
|
+
super(associator)
|
19
|
+
@associations = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def update(new_assoc, expired)
|
23
|
+
@associations.push(new_assoc) if new_assoc
|
24
|
+
|
25
|
+
if expired
|
26
|
+
expired.each { |assoc1|
|
27
|
+
@associations.each_index { |i|
|
28
|
+
if assoc1 == @associations[i]
|
29
|
+
@associations.delete_at(i)
|
30
|
+
# break
|
31
|
+
end
|
32
|
+
}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_all(server_url)
|
38
|
+
results = []
|
39
|
+
@associations.each { |assoc|
|
40
|
+
results.push(assoc) if assoc.server_url == server_url
|
41
|
+
}
|
42
|
+
return results
|
43
|
+
end
|
44
|
+
|
45
|
+
def invalidate(server_url, assoc_handle)
|
46
|
+
@associations.each_index { |i|
|
47
|
+
if @associations[i].server_url == server_url and @associations[i].handle == assoc_handle
|
48
|
+
@associations.delete_at(i)
|
49
|
+
# break
|
50
|
+
end
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end #module OpenID
|
55
|
+
|
56
|
+
class OpenIDServlet < HTTPServlet::AbstractServlet
|
57
|
+
|
58
|
+
def initialize(server, *options)
|
59
|
+
@http_client = OpenID::SimpleHTTPClient.new
|
60
|
+
@association_manager = OpenID::DictionaryAssociationManager.new
|
61
|
+
@consumer = OpenID::Consumer.new(@http_client, @association_manager)
|
62
|
+
super(server, *options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def do_GET(req, res)
|
66
|
+
res['Content-Type'] = 'text/html'
|
67
|
+
if req.query['identity_url']
|
68
|
+
# This executes after the form has been submitted.
|
69
|
+
identity_url = req.query['identity_url'].to_s
|
70
|
+
redirect_url = @consumer.handle_request(identity_url, req.header['referer'][0].to_s)
|
71
|
+
if redirect_url
|
72
|
+
# Redirect to the authentication page (The openid server)
|
73
|
+
res.set_redirect(HTTPStatus::Found, redirect_url)
|
74
|
+
else
|
75
|
+
res.status = 412
|
76
|
+
res.body = "no identity url!"
|
77
|
+
end
|
78
|
+
elsif req.query['openid.mode']
|
79
|
+
# This is where we handle the user after they are sent back by the server
|
80
|
+
begin
|
81
|
+
valid_to = @consumer.handle_response(OpenID::Request.new(req.query, 'GET'))
|
82
|
+
rescue
|
83
|
+
# You can rescue specific errors here to deal with the user cancelling and such.
|
84
|
+
res.body = get_output_page("Exception raised by consumer.handle_response:\n<br />" + $!)
|
85
|
+
else
|
86
|
+
res.body = get_output_page("Logged in! Valid-to: #{valid_to}")
|
87
|
+
end
|
88
|
+
else
|
89
|
+
# Display the login form
|
90
|
+
res.body = get_input_form
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_input_form()
|
95
|
+
return <<EOF
|
96
|
+
<html>
|
97
|
+
<head><title>OpenID for Ruby</title></head>
|
98
|
+
<body style="background-color: #FFFFCC;">
|
99
|
+
<h2>OpenID for Ruby httpconsumer.rb example</h2>
|
100
|
+
<form method="GET" action="/">
|
101
|
+
Your Identity URL: <input type="text" name="identity_url" size="60" />
|
102
|
+
<br /><input type="submit" value="Log in" />
|
103
|
+
</form>
|
104
|
+
</body>
|
105
|
+
</html>
|
106
|
+
EOF
|
107
|
+
end
|
108
|
+
|
109
|
+
def get_output_page(text, bgcolor = '#FFFFCC')
|
110
|
+
return <<EOF
|
111
|
+
<html>
|
112
|
+
<head><title>OpenID for Ruby</title></head>
|
113
|
+
<body style="background-color: #{bgcolor};">
|
114
|
+
<h2>OpenID for Ruby httpconsumer.rb example</h2>
|
115
|
+
#{text}
|
116
|
+
</body>
|
117
|
+
</html>
|
118
|
+
EOF
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
server = HTTPServer.new( :Port => 8081 )
|
124
|
+
server.mount('/', OpenIDServlet)
|
125
|
+
trap("INT") { server.shutdown }
|
126
|
+
server.start
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'openid/constants'
|
2
|
+
require 'openid/errors'
|
3
|
+
require 'openid/util'
|
4
|
+
|
5
|
+
require 'base64'
|
6
|
+
require 'digest/sha1'
|
7
|
+
|
8
|
+
module OpenID
|
9
|
+
class Association
|
10
|
+
attr_reader :handle, :secret, :expiry
|
11
|
+
attr_writer :handle, :secret, :expiry
|
12
|
+
def initialize(handle, secret, expiry, replace_after)
|
13
|
+
@handle = handle.to_s
|
14
|
+
@secret = secret.to_s
|
15
|
+
@replace_after = replace_after
|
16
|
+
@expiry = expiry
|
17
|
+
end
|
18
|
+
#TODO: override ==?
|
19
|
+
end #class Association
|
20
|
+
|
21
|
+
# Represents an association established for a consumer.
|
22
|
+
class ConsumerAssociation < Association
|
23
|
+
attr_reader :server_url
|
24
|
+
attr_writer :server_url
|
25
|
+
def initialize(server_url, handle, secret, expiry, replace_after)
|
26
|
+
super(handle, secret, expiry, replace_after)
|
27
|
+
@server_url = server_url.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_replace_after
|
31
|
+
if @replace_after
|
32
|
+
return @replace_after
|
33
|
+
else
|
34
|
+
return @expiry
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end #class ConsumerAssociation
|
38
|
+
|
39
|
+
class ServerAssociation < Association
|
40
|
+
def initialize(handle, secret, expiry_off, replace_after_off)
|
41
|
+
now = Time.now
|
42
|
+
expiry = now + expiry_off
|
43
|
+
replace_after = now + replace_after_off
|
44
|
+
super(handle, secret, expiry, replace_after)
|
45
|
+
@issued = now
|
46
|
+
end
|
47
|
+
end #class ServerAssociation
|
48
|
+
|
49
|
+
# Abstract class which is the parent of both DumbAssociationManager and
|
50
|
+
# BaseAssociationManager.
|
51
|
+
class AssociationManager
|
52
|
+
# Return a ConsumerAssociation based on server_url and assoc_handle
|
53
|
+
def get_association(server_url, assoc_handle)
|
54
|
+
raise NotImplementedError
|
55
|
+
end
|
56
|
+
# Create an association with server_url. Return an assoc_handle
|
57
|
+
def associate(server_url)
|
58
|
+
raise NotImplementedError
|
59
|
+
end
|
60
|
+
# Invalidate an Association
|
61
|
+
def invalidate(server_url, assoc_handle)
|
62
|
+
raise NotImplementedError
|
63
|
+
end
|
64
|
+
end # class AssociactionManager
|
65
|
+
|
66
|
+
class DumbAssociationManager < AssociationManager
|
67
|
+
def get_association(server_url, assoc_handle)
|
68
|
+
return nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def associate(server_url)
|
72
|
+
return nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def invalidate(server_url, assoc_handle)
|
76
|
+
# pass
|
77
|
+
end
|
78
|
+
end #class DumbAssociationManager
|
79
|
+
|
80
|
+
class BaseAssociationManager < AssociationManager
|
81
|
+
def initialize(associator)
|
82
|
+
@associator = associator
|
83
|
+
end
|
84
|
+
# Returns assoc_handle associated with server_url
|
85
|
+
def associate(server_url)
|
86
|
+
now = Time.now
|
87
|
+
expired = []
|
88
|
+
assoc = nil
|
89
|
+
get_all(server_url).each { |current|
|
90
|
+
replace_after = current.get_replace_after
|
91
|
+
if current.expiry < now
|
92
|
+
expired.push(current)
|
93
|
+
elsif assoc == nil
|
94
|
+
assoc = current if replace_after > now
|
95
|
+
elsif replace_after > assoc.replace_after
|
96
|
+
assoc = current
|
97
|
+
end
|
98
|
+
}
|
99
|
+
new_assoc = nil
|
100
|
+
if assoc == nil
|
101
|
+
assoc = new_assoc = @associator.associate(server_url)
|
102
|
+
end
|
103
|
+
if new_assoc or expired
|
104
|
+
update(new_assoc, expired)
|
105
|
+
end
|
106
|
+
|
107
|
+
return assoc.handle
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_association(server_url, assoc_handle)
|
111
|
+
get_all(server_url).each { |assoc|
|
112
|
+
return assoc if assoc.handle == assoc_handle
|
113
|
+
}
|
114
|
+
return nil
|
115
|
+
end
|
116
|
+
|
117
|
+
# This must be implemented by subclasses.
|
118
|
+
#
|
119
|
+
# new_assoc is either a new association object or nil. Expired is a possibly
|
120
|
+
# empty list of expired associations.
|
121
|
+
# Subclass should add new_assoc if it is not nil, and expire each association
|
122
|
+
# in the expired list.
|
123
|
+
def update(new_assoc, expired)
|
124
|
+
raise NotImplementedError
|
125
|
+
end
|
126
|
+
|
127
|
+
# This must be implemented by subclasses.
|
128
|
+
#
|
129
|
+
# Should return a list of Association objects matching server_url
|
130
|
+
def get_all(server_url)
|
131
|
+
raise NotImplementedError
|
132
|
+
end
|
133
|
+
|
134
|
+
# This must be implemented by subclasses.
|
135
|
+
#
|
136
|
+
# Subclass should remove the association for the given
|
137
|
+
# server_url and assoc_handle.
|
138
|
+
def invalidate(server_url, assoc_handle)
|
139
|
+
raise NotImplementedError
|
140
|
+
end
|
141
|
+
end #class BaseAssociationManager
|
142
|
+
|
143
|
+
# A class for establishing associations with OpenID servers.
|
144
|
+
class DiffieHelmanAssociator
|
145
|
+
include OpenID
|
146
|
+
def initialize(http_client)
|
147
|
+
@http_client = http_client
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns modulus and generator for Diffie-Helman.
|
151
|
+
# Override this for non-default values.
|
152
|
+
def get_mod_gen()
|
153
|
+
return DEFAULT_DH_MODULUS, DEFAULT_DH_GEN
|
154
|
+
end
|
155
|
+
|
156
|
+
# Establishes an association with an OpenID server, indicated by server_url.
|
157
|
+
# Returns a ConsumerAssociation.
|
158
|
+
def associate(server_url)
|
159
|
+
p, g = get_mod_gen()
|
160
|
+
private_key = (1 + rand(p-2))
|
161
|
+
dh_public = g.mod_exp(private_key, p)
|
162
|
+
args = {
|
163
|
+
'openid.mode' => 'associate',
|
164
|
+
'openid.assoc_type' => 'HMAC-SHA1',
|
165
|
+
'openid.session_type' => 'DH-SHA1',
|
166
|
+
'openid.dh_modulus' => Base64.encode64(p.to_btwoc).delete("\n"),
|
167
|
+
'openid.dh_gen' => Base64.encode64(g.to_btwoc).delete("\n"),
|
168
|
+
'openid.dh_consumer_public' => Base64.encode64(dh_public.to_btwoc).delete("\n")
|
169
|
+
}
|
170
|
+
body = url_encode(args)
|
171
|
+
url, data = @http_client.post(server_url, body)
|
172
|
+
now = Time.now.utc
|
173
|
+
# results are temporarily stored in an instance variable.
|
174
|
+
# I'd like to restructure to avoid this.
|
175
|
+
@results = parse_kv(data)
|
176
|
+
|
177
|
+
assoc_type = get_result('assoc_type')
|
178
|
+
if assoc_type != 'HMAC-SHA1'
|
179
|
+
raise RuntimeError, "Unknown association type: #{assoc_type}", caller
|
180
|
+
end
|
181
|
+
|
182
|
+
assoc_handle = get_result('assoc_handle')
|
183
|
+
issued = DateTime.strptime(get_result('issued')).to_time
|
184
|
+
expiry = DateTime.strptime(get_result('expiry')).to_time
|
185
|
+
|
186
|
+
delta = now - issued
|
187
|
+
expiry = expiry + delta
|
188
|
+
|
189
|
+
replace_after_s = @results['replace_after']
|
190
|
+
if replace_after_s
|
191
|
+
replace_after = DateTime.strptime(replace_after_s).to_time + delta
|
192
|
+
else
|
193
|
+
replace_after = nil
|
194
|
+
end
|
195
|
+
|
196
|
+
session_type = @results['session_type']
|
197
|
+
if session_type
|
198
|
+
if session_type != 'DH-SHA1'
|
199
|
+
raise RuntimeError, "Unknown Session Type: #{session_type}", caller
|
200
|
+
end
|
201
|
+
dh_server_pub = from_btwoc(Base64.decode64(get_result('dh_server_public')))
|
202
|
+
enc_mac_key = get_result('enc_mac_key')
|
203
|
+
dh_shared = dh_server_pub.mod_exp(private_key, p)
|
204
|
+
|
205
|
+
secret = Base64.decode64(enc_mac_key) ^ Digest::SHA1.digest(dh_shared.to_btwoc)
|
206
|
+
else
|
207
|
+
secret = get_result('mac_key')
|
208
|
+
end
|
209
|
+
@results = nil
|
210
|
+
return ConsumerAssociation.new(server_url, assoc_handle, secret, expiry, replace_after)
|
211
|
+
end
|
212
|
+
private
|
213
|
+
def get_result(key)
|
214
|
+
begin
|
215
|
+
return @results.fetch(key)
|
216
|
+
rescue IndexError
|
217
|
+
raise ProtocolError, "Association server response missing argument #{key}", caller
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end #class DiffieHelmanAssociator
|
221
|
+
|
222
|
+
end #module OpenID
|
@@ -0,0 +1,5 @@
|
|
1
|
+
module OpenID
|
2
|
+
SECRET_SIZES = {'HMAC-SHA1' => 20 }
|
3
|
+
DEFAULT_DH_MODULUS = 155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443
|
4
|
+
DEFAULT_DH_GEN = 2
|
5
|
+
end #module OpenID
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'openid/errors'
|
2
|
+
require 'openid/constants'
|
3
|
+
require 'openid/util'
|
4
|
+
|
5
|
+
require 'net/http'
|
6
|
+
require 'uri'
|
7
|
+
require 'open-uri'
|
8
|
+
|
9
|
+
module OpenID
|
10
|
+
|
11
|
+
def quote_minimal(s)
|
12
|
+
# TODO: implement? (Used by normalize_url's unicode handling in the python modules)
|
13
|
+
return s
|
14
|
+
end
|
15
|
+
# Strip whitespace off, and add http:// if http:// or https:// is not already
|
16
|
+
# present.
|
17
|
+
def normalize_url(url)
|
18
|
+
url.strip!
|
19
|
+
if (!url.index(/^http:\/\/|^https:\/\//))
|
20
|
+
url = 'http://' + url
|
21
|
+
end
|
22
|
+
|
23
|
+
# TODO: Some unicode handling
|
24
|
+
# (Keeping in mind that ruby's unicode/string distinction is kinda nil so far)
|
25
|
+
|
26
|
+
return url
|
27
|
+
end
|
28
|
+
|
29
|
+
# Provides a very simple interface to get from and post to http servers
|
30
|
+
class SimpleHTTPClient
|
31
|
+
# Returns the the url which was retrieved, and the retrived data
|
32
|
+
# (data.base_uri, data)
|
33
|
+
def get(url)
|
34
|
+
uri = URI.parse(url)
|
35
|
+
begin
|
36
|
+
data = uri.read
|
37
|
+
ensure
|
38
|
+
if data
|
39
|
+
return data.base_uri, data
|
40
|
+
else
|
41
|
+
return nil, nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
# Takes the url to post body to.
|
46
|
+
# Returns the url retrieved, and the body of the response received.
|
47
|
+
def post(url, body)
|
48
|
+
uri = URI.parse(url)
|
49
|
+
response = nil
|
50
|
+
Net::HTTP.start(uri.host, uri.port) { |http|
|
51
|
+
response = http.post(uri.request_uri(), body)
|
52
|
+
}
|
53
|
+
# TODO: some error checking here
|
54
|
+
# TODO: return actually retrieved url
|
55
|
+
return url, response.body
|
56
|
+
end
|
57
|
+
|
58
|
+
end #class SimpleHTTPClient
|
59
|
+
|
60
|
+
class Consumer
|
61
|
+
include OpenID
|
62
|
+
# Takes an http_client and an association_manager. Will create some automatically if none are passed in.
|
63
|
+
def initialize(http_client = SimpleHTTPClient.new(), association_manager = DumbAssociationManager.new())
|
64
|
+
@http_client = http_client
|
65
|
+
@association_manager = association_manager
|
66
|
+
end
|
67
|
+
# Returns the url to redirect to or nil if no identity is found
|
68
|
+
def handle_request(url, return_to, trust_root = nil, immediate = false)
|
69
|
+
url = normalize_url(url)
|
70
|
+
|
71
|
+
server_info = find_server(url)
|
72
|
+
return nil if server_info == nil
|
73
|
+
identity, server_url = server_info
|
74
|
+
redir_args = { 'openid.identity' => identity, 'openid.return_to' => return_to}
|
75
|
+
|
76
|
+
redir_args['openid.trust_root'] = trust_root if trust_root
|
77
|
+
|
78
|
+
if immediate
|
79
|
+
mode = 'checkid_immediate'
|
80
|
+
else
|
81
|
+
mode = 'checkid_setup'
|
82
|
+
end
|
83
|
+
|
84
|
+
redir_args['openid.mode'] = mode
|
85
|
+
assoc_handle = @association_manager.associate(server_url)
|
86
|
+
if assoc_handle
|
87
|
+
redir_args['openid.assoc_handle'] = assoc_handle
|
88
|
+
end
|
89
|
+
|
90
|
+
return append_args(server_url, redir_args).to_s
|
91
|
+
end
|
92
|
+
# Handles an OpenID GET request with openid.mode in the arguments. req should
|
93
|
+
# be a Request instance, properly initialized with the http arguments given,
|
94
|
+
# and the http method used to make the request. Returns the expiry time of
|
95
|
+
# the session as a Time.
|
96
|
+
#
|
97
|
+
# Will raise a ProtocolError if the http_method is not GET, or the request
|
98
|
+
# mode is unknown.
|
99
|
+
def handle_response(req)
|
100
|
+
if req.http_method != 'GET'
|
101
|
+
raise ProtocolError, "Expected HTTP Method 'GET', got #{req.http_method}", caller
|
102
|
+
end
|
103
|
+
begin
|
104
|
+
return __send__('do_' + req['mode'], req)
|
105
|
+
rescue NoMethodError
|
106
|
+
raise ProtocolError, "Unknown Mode: #{req['mode']}", caller
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def determine_server_url(req)
|
111
|
+
identity, server_url = find_server(req['identity'])
|
112
|
+
if req['identity'] != identity
|
113
|
+
raise ValueMismatchError, "ID URL #{req['identity']} seems to have moved: #{identity}", caller
|
114
|
+
end
|
115
|
+
return server_url
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns identity_url, server_url or nil if no server is found.
|
119
|
+
def find_server(url)
|
120
|
+
identity, data = @http_client.get(url)
|
121
|
+
identity = identity.to_s
|
122
|
+
server = nil
|
123
|
+
delegate = nil
|
124
|
+
parse_link_attrs(data) { |link|
|
125
|
+
rel = link['rel']
|
126
|
+
if rel == 'openid.server' and server == nil
|
127
|
+
href = link['href']
|
128
|
+
server = href if href
|
129
|
+
end
|
130
|
+
if rel == 'openid.delegate' and delegate == nil
|
131
|
+
href = link['href']
|
132
|
+
delegate = href if href
|
133
|
+
end
|
134
|
+
}
|
135
|
+
return nil if !server
|
136
|
+
identity = delegate if delegate
|
137
|
+
return normalize_url(identity), normalize_url(server)
|
138
|
+
end
|
139
|
+
|
140
|
+
def _dumb_auth(server_url, now, req)
|
141
|
+
if !verify_return_to(req)
|
142
|
+
raise ValueMismatchError, "return_to is not valid", caller
|
143
|
+
end
|
144
|
+
check_args = {}
|
145
|
+
req.args.each { |k, v| check_args[k] = v if k.index('openid.') == 0 }
|
146
|
+
check_args['openid.mode'] = 'check_authentication'
|
147
|
+
body = url_encode(check_args)
|
148
|
+
url, data = @http_client.post(server_url, body)
|
149
|
+
results = parse_kv(data)
|
150
|
+
lifetime = results['lifetime'].to_i
|
151
|
+
if lifetime
|
152
|
+
invalidate_handle = results['invalidate_handle']
|
153
|
+
if invalidate_handle
|
154
|
+
@association_manager.invalidate(server_url, invalidate_handle)
|
155
|
+
end
|
156
|
+
return now + lifetime
|
157
|
+
else
|
158
|
+
raise ValueMismatchError, 'Server failed to validate signature', caller
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def do_id_res(req)
|
163
|
+
now = Time.now
|
164
|
+
user_setup_url = req.get('user_setup_url')
|
165
|
+
raise UserSetupNeeded, user_setup_url, caller if user_setup_url
|
166
|
+
server_url = determine_server_url(req)
|
167
|
+
assoc = @association_manager.get_association(server_url, req['assoc_handle'])
|
168
|
+
if assoc == nil
|
169
|
+
return _dumb_auth(server_url, now, req)
|
170
|
+
end
|
171
|
+
sig = req.get('sig')
|
172
|
+
signed_fields = req.get('signed').strip.split(',')
|
173
|
+
signed, v_sig = sign_reply(req.args, assoc.secret, signed_fields)
|
174
|
+
if v_sig != sig
|
175
|
+
raise ValueMismatchError, "Signatures did not match: #{req.args}, #{v_sig}, #{assoc.secret}", caller
|
176
|
+
end
|
177
|
+
issued = DateTime.strptime(req.get('issued')).to_time
|
178
|
+
valid_to = [assoc.expiry, DateTime.strptime(req.get('valid_to')).to_time].min
|
179
|
+
return now + (valid_to - issued)
|
180
|
+
end
|
181
|
+
# Handle an error from the server
|
182
|
+
def do_error(req)
|
183
|
+
error = req.get('error')
|
184
|
+
if error
|
185
|
+
raise ProtocolError, "Server Response: #{error}", caller
|
186
|
+
else
|
187
|
+
raise ProtocolError, "Unspecified Server Error: #{req.args}", caller
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def do_cancel(req)
|
192
|
+
raise UserCancelled
|
193
|
+
end
|
194
|
+
|
195
|
+
# This is called before the consumer makes a check_authentication call to the
|
196
|
+
# server. It can be used to verify that the request being authenticated
|
197
|
+
# is valid by confirming that the openid.return_to value signed by the server
|
198
|
+
# corresponds to this consumer. The full OpenID::Request object is passed in.
|
199
|
+
# Should return true if the return_to field corresponds to this consumer,
|
200
|
+
# false otherwise. The default function performs no check and returns true.
|
201
|
+
def verify_return_to(req)
|
202
|
+
return true
|
203
|
+
end
|
204
|
+
end #class Consumer
|
205
|
+
|
206
|
+
|
207
|
+
end #module OpenID
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
module OpenID
|
3
|
+
|
4
|
+
class ProtocolError < RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
class AuthenticationError < RuntimeError
|
8
|
+
end
|
9
|
+
|
10
|
+
class ValueMismatchError < RuntimeError
|
11
|
+
end
|
12
|
+
|
13
|
+
class NoArgumentsError < RuntimeError
|
14
|
+
end
|
15
|
+
|
16
|
+
class UserCancelled < RuntimeError
|
17
|
+
end
|
18
|
+
|
19
|
+
class UserSetupNeeded < RuntimeError
|
20
|
+
end
|
21
|
+
|
22
|
+
class NoOpenIDArgs < RuntimeError
|
23
|
+
end
|
24
|
+
|
25
|
+
end #module OpenID
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'openid/errors'
|
2
|
+
|
3
|
+
module OpenID
|
4
|
+
# Response is a hash with a modified constructor to pretty things a little
|
5
|
+
class Response < Hash
|
6
|
+
# Takes a hash, which becomes the contents of the Response object
|
7
|
+
def initialize(attr_hash)
|
8
|
+
update(attr_hash)
|
9
|
+
end
|
10
|
+
end #class Response
|
11
|
+
# Creates a Response object signaling a redirect to url.
|
12
|
+
def redirect(url)
|
13
|
+
return Response.new('code'=>302, 'redirect_url'=>url.to_s)
|
14
|
+
end
|
15
|
+
# Creates a response object signaling a plaintext response.
|
16
|
+
def response_page(body)
|
17
|
+
return Response.new('code'=>200, 'content_type'=>'text/plain', 'body'=>body)
|
18
|
+
end
|
19
|
+
# Creates a response object signaling a plaintext error.
|
20
|
+
def error_page(body)
|
21
|
+
return Response.new('code'=>400, 'content_type'=>'text/plain', 'body'=>body)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Request objects are used by both the consumer and server APIs to signal
|
25
|
+
# and represent various HTTP requests.
|
26
|
+
|
27
|
+
class Request
|
28
|
+
attr_reader :args, :http_method, :authentication
|
29
|
+
attr_writer :args, :http_method
|
30
|
+
# args is a hash of HTTP arguments.
|
31
|
+
# http_method should be set to "POST" or "GET"
|
32
|
+
# authentication is an unused passthrough field that will remain attatched
|
33
|
+
# to the request.
|
34
|
+
# A NoOpenIDArgs exception will be raised if args contains no openid.*
|
35
|
+
# arguments.
|
36
|
+
def initialize(args, http_method, authentication=nil)
|
37
|
+
@args = args
|
38
|
+
@http_method = http_method.upcase
|
39
|
+
@authentication = authentication
|
40
|
+
|
41
|
+
@args.each_key { |key|
|
42
|
+
return if key.index('openid.') == 0
|
43
|
+
}
|
44
|
+
raise NoOpenIDArgs
|
45
|
+
end
|
46
|
+
# The preferred method for getting OpenID args out of the request.
|
47
|
+
# Raises a ProtoclError if the argument does not exist.
|
48
|
+
def [](key)
|
49
|
+
result = get(key)
|
50
|
+
if result == nil
|
51
|
+
if key == 'trust_root'
|
52
|
+
return self['return_to']
|
53
|
+
else
|
54
|
+
raise ProtocolError, "Query argument #{key} not found", caller
|
55
|
+
end
|
56
|
+
end
|
57
|
+
return result
|
58
|
+
end
|
59
|
+
|
60
|
+
def get(key, default=nil)
|
61
|
+
return @args.fetch('openid.' + key, default)
|
62
|
+
end
|
63
|
+
|
64
|
+
end #class Request
|
65
|
+
end #module OpenID
|
data/lib/openid/util.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'html/htmltokenizer'
|
3
|
+
require 'base64'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'hmac-sha1'
|
6
|
+
|
7
|
+
# Functions convenient for cryptography
|
8
|
+
|
9
|
+
module Crypto_math
|
10
|
+
# This code is taken from this post[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/19098]
|
11
|
+
# by Eric Lee Green. x.mod_exp(n,q) returns x ** n % q
|
12
|
+
def mod_exp(n,q)
|
13
|
+
counter=0
|
14
|
+
n_p=n # N
|
15
|
+
y_p=1 # Y
|
16
|
+
z_p=self # Z
|
17
|
+
while n_p != 0
|
18
|
+
if n_p[0]==1
|
19
|
+
y_p=(y_p*z_p) % q
|
20
|
+
end
|
21
|
+
n_p = n_p >> 1
|
22
|
+
z_p = (z_p * z_p) % q
|
23
|
+
counter += 1
|
24
|
+
end
|
25
|
+
return y_p
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
class Fixnum
|
31
|
+
include Crypto_math
|
32
|
+
# Returns a number in big endian two's complement notation, stored
|
33
|
+
# as a raw string.
|
34
|
+
def to_btwoc
|
35
|
+
bits = self.to_s(2)
|
36
|
+
prepend = (8 - bits.length % 8) || (bits.index(/^1/) ? 8 : 0)
|
37
|
+
bits = ('0' * prepend) + bits if prepend
|
38
|
+
return [bits].pack('B*')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Bignum
|
43
|
+
include Crypto_math
|
44
|
+
# Returns a number in big endian two's complement notation, stored
|
45
|
+
# as a raw string.
|
46
|
+
def to_btwoc
|
47
|
+
bits = self.to_s(2)
|
48
|
+
prepend = (8 - bits.length % 8) || (bits.index(/^1/) ? 8 : 0)
|
49
|
+
bits = ('0' * prepend) + bits if prepend
|
50
|
+
return [bits].pack('B*')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class DateTime
|
55
|
+
# Converts to a UTC Time.
|
56
|
+
def to_time()
|
57
|
+
if offset == 0
|
58
|
+
return Time.gm(year, month, day, hour, min, sec)
|
59
|
+
else
|
60
|
+
utc = new_offset
|
61
|
+
return Time.gm(utc.year, utc.month, utc.day, utc.hour, utc.min, utc.sec)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end #class DateTime
|
65
|
+
|
66
|
+
class String
|
67
|
+
# Bitwise-XOR two equal length strings.
|
68
|
+
# Raises an ArgumentError if strings are different length.
|
69
|
+
def ^(other)
|
70
|
+
raise ArgumentError, "Can't bitwise-XOR a String with a non-String" \
|
71
|
+
unless other.kind_of? String
|
72
|
+
raise ArgumentError, "Can't bitwise-XOR strings of different length" \
|
73
|
+
unless self.length == other.length
|
74
|
+
result = (0..self.length-1).collect { |i| self[i] ^ other[i] }
|
75
|
+
result.pack("C*")\
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module OpenID
|
80
|
+
# Takes all entries in a hash and combines and escapes them (using CGI::escape) into a single string.
|
81
|
+
def url_encode(query)
|
82
|
+
output = ''
|
83
|
+
query.each { |key, val|
|
84
|
+
output += CGI::escape(key.to_s) + '=' + CGI::escape(val.to_s) + '&'
|
85
|
+
}
|
86
|
+
output.chop!
|
87
|
+
return output
|
88
|
+
end
|
89
|
+
# Appends arguments in hash args to the existing url string
|
90
|
+
def append_args(url, args)
|
91
|
+
return url if args.empty?
|
92
|
+
return url + (url.include? '?' and '&' or '?') + url_encode(args)
|
93
|
+
end
|
94
|
+
# Converts a raw string containing a big endian two's complement number into a Fixnum or Bignum
|
95
|
+
def from_btwoc(str)
|
96
|
+
# Maybe this should be part of a btwoc class or something
|
97
|
+
str = "\000" * (4 - (str.length % 4)) + str
|
98
|
+
num = 0
|
99
|
+
str.unpack('N*').each { |x|
|
100
|
+
num <<= 32
|
101
|
+
num |= x
|
102
|
+
}
|
103
|
+
return num
|
104
|
+
end
|
105
|
+
# Parses an OpenID Key Value string(A new-line separated list containing key:value pairs).
|
106
|
+
# Returns a hash.
|
107
|
+
def parse_kv(d)
|
108
|
+
d.strip!
|
109
|
+
args = {}
|
110
|
+
d.split("\n").each { |line|
|
111
|
+
pair = line.split(':',2)
|
112
|
+
if pair.length == 2
|
113
|
+
k, v = pair
|
114
|
+
args[k.strip] = v.strip
|
115
|
+
end
|
116
|
+
}
|
117
|
+
return args
|
118
|
+
end
|
119
|
+
# Takes a string containing html, and yields the attributes of each link tags until a body tag is found.
|
120
|
+
def parse_link_attrs(data)
|
121
|
+
parser = HTMLTokenizer.new(data)
|
122
|
+
while el = parser.getTag('link', 'body')
|
123
|
+
if el.tag_name == 'link'
|
124
|
+
yield el.attr_hash
|
125
|
+
elsif el.tag_name == 'body'
|
126
|
+
return
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
# Generates a signature for a set of fields. reply is a hash containing the
|
133
|
+
# data that was signed, key is the secret key, and signed_fields is an array
|
134
|
+
# containing the names (in order) of the fields to be signed.
|
135
|
+
# Returns the list of signed fields (comma separated) and the base64 encoded signature
|
136
|
+
def sign_reply(reply, key, signed_fields)
|
137
|
+
token = ''
|
138
|
+
signed_fields.each { |x|
|
139
|
+
token += x + ':' + reply['openid.' + x] + "\n"
|
140
|
+
}
|
141
|
+
d = Base64.encode64(HMAC::SHA1.digest(key,token)).delete("\n")
|
142
|
+
return signed_fields.join(','), d
|
143
|
+
end
|
144
|
+
|
145
|
+
end #module OpenID
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.10
|
3
|
+
specification_version: 1
|
4
|
+
name: openid
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2005-07-10
|
8
|
+
summary: OpenID support for Ruby
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: mmq@dekinai.com
|
12
|
+
homepage: http://openid.rubyforge.org/
|
13
|
+
rubyforge_project: openid
|
14
|
+
description: "OpenID support for Ruby -- OpenID (http://openid.net) is a decentralized
|
15
|
+
identification system that allows users to prove they own a url. OpenID for Ruby
|
16
|
+
currently includes only consumer modules."
|
17
|
+
autorequire:
|
18
|
+
default_executable:
|
19
|
+
bindir: bin
|
20
|
+
has_rdoc: true
|
21
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
22
|
+
requirements:
|
23
|
+
-
|
24
|
+
- ">"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.0.0
|
27
|
+
version:
|
28
|
+
platform: ruby
|
29
|
+
authors:
|
30
|
+
- Mark Quinn
|
31
|
+
files:
|
32
|
+
- lib/openid/consumer.rb
|
33
|
+
- lib/openid/util.rb
|
34
|
+
- lib/openid/association.rb
|
35
|
+
- lib/openid/errors.rb
|
36
|
+
- lib/openid/constants.rb
|
37
|
+
- lib/openid/interface.rb
|
38
|
+
- examples/httpconsumer.rb
|
39
|
+
- README
|
40
|
+
- COPYING
|
41
|
+
test_files: []
|
42
|
+
rdoc_options: []
|
43
|
+
extra_rdoc_files: []
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
requirements:
|
47
|
+
- "htmltokenizer, ruby-hmac"
|
48
|
+
dependencies: []
|