openid 0.0.1
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/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: []
|