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 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: []