openid 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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: []