dominiquebrezinski-rack-openid 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +70 -0
  3. data/lib/rack/openid.rb +269 -0
  4. metadata +77 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Joshua Peek
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,70 @@
1
+ = Rack::OpenID
2
+
3
+ Provides a more HTTPish API around the ruby-openid library.
4
+
5
+ === Usage
6
+
7
+ You trigger an OpenID request similar to HTTP authentication. From your app, return a "401 Unauthorized" and a "WWW-Authenticate" header with the identifier you would like to validate.
8
+
9
+ On competition, the OpenID response is automatically verified and assigned to
10
+ <tt>env["rack.openid.response"]</tt>.
11
+
12
+ === Rack Example
13
+
14
+ MyApp = lambda { |env|
15
+ if resp = env["rack.openid.response"]
16
+ case resp.status
17
+ when :success
18
+ ...
19
+ when :failure
20
+ ...
21
+ else
22
+ [401, {"WWW-Authenticate" => 'OpenID identifier="http://example.com/"'}, []]
23
+ end
24
+ }
25
+
26
+ use Rack::OpenID
27
+ run MyApp
28
+
29
+ === Sinatra Example
30
+
31
+ # Session needs to be before Rack::OpenID
32
+ use Rack::Session::Cookie
33
+
34
+ require 'rack/openid'
35
+ use Rack::OpenID
36
+
37
+ get '/login' do
38
+ erb :login
39
+ end
40
+
41
+ post '/login' do
42
+ if resp = request.env["rack.openid.response"]
43
+ if resp.status == :success
44
+ "Welcome: #{resp.display_identifier}"
45
+ else
46
+ "Error: #{resp.status}"
47
+ end
48
+ else
49
+ headers 'WWW-Authenticate' => Rack::OpenID.build_header(
50
+ :identifier => params["openid_identifier"]
51
+ )
52
+ throw :halt, [401, 'got openid?']
53
+ end
54
+ end
55
+
56
+ use_in_file_templates!
57
+
58
+ __END__
59
+
60
+ @@ login
61
+ <form action="/login" method="post">
62
+ <p>
63
+ <label for="openid_identifier">OpenID:</label>
64
+ <input id="openid_identifier" name="openid_identifier" type="text" />
65
+ </p>
66
+
67
+ <p>
68
+ <input name="commit" type="submit" value="Sign in" />
69
+ </p>
70
+ </form>
@@ -0,0 +1,269 @@
1
+ require 'rack/request'
2
+ require 'rack/utils'
3
+
4
+ require 'openid'
5
+ require 'openid/consumer'
6
+ require 'openid/extensions/sreg'
7
+ require 'openid/extensions/ax'
8
+ begin
9
+ require 'openid/extensions/oauth'
10
+ OPENID_GEM_WITH_OAUTH_SUPPORT=true
11
+ rescue LoadError
12
+ OPENID_GEM_WITH_OAUTH_SUPPORT=false
13
+ end
14
+
15
+ module Rack #:nodoc:
16
+ # A Rack middleware that provides a more HTTPish API around the
17
+ # ruby-openid library.
18
+ #
19
+ # You trigger an OpenID request similar to HTTP authentication.
20
+ # From your app, return a "401 Unauthorized" and a "WWW-Authenticate"
21
+ # header with the identifier you would like to validate.
22
+ #
23
+ # On competition, the OpenID response is automatically verified and
24
+ # assigned to <tt>env["rack.openid.response"]</tt>.
25
+ class OpenID
26
+ # Helper method for building the "WWW-Authenticate" header value.
27
+ #
28
+ # Rack::OpenID.build_header(:identifier => "http://josh.openid.com/")
29
+ # #=> OpenID identifier="http://josh.openid.com/"
30
+ def self.build_header(params = {})
31
+ 'OpenID ' + params.map { |key, value|
32
+ if value.is_a?(Array)
33
+ "#{key}=\"#{value.join(',')}\""
34
+ else
35
+ "#{key}=\"#{value}\""
36
+ end
37
+ }.join(', ')
38
+ end
39
+
40
+ # Helper method for parsing "WWW-Authenticate" header values into
41
+ # a hash.
42
+ #
43
+ # Rack::OpenID.parse_header("OpenID identifier='http://josh.openid.com/'")
44
+ # #=> {:identifier => "http://josh.openid.com/"}
45
+ def self.parse_header(str)
46
+ params = {}
47
+ if str =~ AUTHENTICATE_REGEXP
48
+ str = str.gsub(/#{AUTHENTICATE_REGEXP}\s+/, '')
49
+ str.split(', ').each { |pair|
50
+ key, *value = pair.split('=')
51
+ value = value.join('=')
52
+ value.gsub!(/^\"/, '').gsub!(/\"$/, "")
53
+ value = value.split(',')
54
+ params[key] = value.length > 1 ? value : value.first
55
+ }
56
+ end
57
+ params
58
+ end
59
+
60
+ class TimeoutResponse #:nodoc:
61
+ include ::OpenID::Consumer::Response
62
+ STATUS = :failure
63
+ end
64
+
65
+ class MissingResponse #:nodoc:
66
+ include ::OpenID::Consumer::Response
67
+ STATUS = :missing
68
+ end
69
+
70
+ # :stopdoc:
71
+
72
+ HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS)
73
+
74
+ RESPONSE = "rack.openid.response".freeze
75
+ AUTHENTICATE_HEADER = "WWW-Authenticate".freeze
76
+ AUTHENTICATE_REGEXP = /^OpenID/.freeze
77
+
78
+ URL_FIELD_SELECTOR = lambda { |field| field.to_s =~ %r{^https?://} }
79
+
80
+ # :startdoc:
81
+
82
+ # Initialize middleware with application and optional OpenID::Store.
83
+ # If no store is given, OpenID::Store::Memory is used.
84
+ #
85
+ # use Rack::OpenID
86
+ #
87
+ # or
88
+ #
89
+ # use Rack::OpenID, OpenID::Store::Memcache.new
90
+ def initialize(app, store = nil)
91
+ @app = app
92
+ @store = store || default_store
93
+ freeze
94
+ end
95
+
96
+ # Standard Rack +call+ dispatch that accepts an +env+ and
97
+ # returns a <tt>[status, header, body]</tt> response.
98
+ def call(env)
99
+ req = Rack::Request.new(env)
100
+ if req.params["openid.mode"]
101
+ complete_authentication(env)
102
+ end
103
+
104
+ status, headers, body = @app.call(env)
105
+
106
+ qs = headers[AUTHENTICATE_HEADER]
107
+ if status.to_i == 401 && qs && qs.match(AUTHENTICATE_REGEXP)
108
+ begin_authentication(env, qs)
109
+ else
110
+ [status, headers, body]
111
+ end
112
+ end
113
+
114
+ private
115
+ def begin_authentication(env, qs)
116
+ req = Rack::Request.new(env)
117
+ params = self.class.parse_header(qs)
118
+ session = env["rack.session"]
119
+
120
+ unless session
121
+ raise RuntimeError, "Rack::OpenID requires a session"
122
+ end
123
+
124
+ consumer = ::OpenID::Consumer.new(session, @store)
125
+ identifier = params['identifier'] || params['identity']
126
+
127
+ begin
128
+ oidreq = consumer.begin(identifier)
129
+ add_simple_registration_fields(oidreq, params)
130
+ add_attribute_exchange_fields(oidreq, params)
131
+ add_oauth_fields(oidreq, params) if OPENID_GEM_WITH_OAUTH_SUPPORT
132
+ url = open_id_redirect_url(req, oidreq, params["trust_root"], params["return_to"], params["method"])
133
+ return redirect_to(url)
134
+ rescue ::OpenID::OpenIDError, Timeout::Error => e
135
+ env[RESPONSE] = MissingResponse.new
136
+ return @app.call(env)
137
+ end
138
+ end
139
+
140
+ def complete_authentication(env)
141
+ req = Rack::Request.new(env)
142
+ session = env["rack.session"]
143
+
144
+ unless session
145
+ raise RuntimeError, "Rack::OpenID requires a session"
146
+ end
147
+
148
+ oidresp = timeout_protection_from_identity_server {
149
+ consumer = ::OpenID::Consumer.new(session, @store)
150
+ consumer.complete(req.params, req.url)
151
+ }
152
+
153
+ env[RESPONSE] = oidresp
154
+
155
+ method = req.GET["_method"]
156
+ override_request_method(env, method)
157
+
158
+ sanitize_query_string(env)
159
+ end
160
+
161
+ def override_request_method(env, method)
162
+ return unless method
163
+ method = method.upcase
164
+ if HTTP_METHODS.include?(method)
165
+ env["REQUEST_METHOD"] = method
166
+ end
167
+ end
168
+
169
+ def sanitize_query_string(env)
170
+ query_hash = env["rack.request.query_hash"]
171
+ query_hash.delete("_method")
172
+ query_hash.delete_if do |key, value|
173
+ key =~ /^openid\./
174
+ end
175
+
176
+ env["QUERY_STRING"] = env["rack.request.query_string"] =
177
+ Rack::Utils.build_query(env["rack.request.query_hash"])
178
+
179
+ qs = env["QUERY_STRING"]
180
+ request_uri = env["PATH_INFO"].dup
181
+ request_uri << "?" + qs unless qs == ""
182
+ env["REQUEST_URI"] = request_uri
183
+ end
184
+
185
+ def realm_url(req)
186
+ url = req.scheme + "://"
187
+ url << req.host
188
+
189
+ scheme, port = req.scheme, req.port
190
+ if scheme == "https" && port != 443 ||
191
+ scheme == "http" && port != 80
192
+ url << ":#{port}"
193
+ end
194
+
195
+ url
196
+ end
197
+
198
+ def request_url(req)
199
+ url = realm_url(req)
200
+ url << req.script_name
201
+ url << req.path_info
202
+ url
203
+ end
204
+
205
+ def redirect_to(url)
206
+ [303, {"Content-Type" => "text/html", "Location" => url}, []]
207
+ end
208
+
209
+ def open_id_redirect_url(req, oidreq, trust_root = nil, return_to = nil, method = nil)
210
+ request_url = request_url(req)
211
+
212
+ if return_to
213
+ method ||= "get"
214
+ else
215
+ return_to = request_url
216
+ method ||= req.request_method
217
+ end
218
+
219
+ method = method.to_s.downcase
220
+ oidreq.return_to_args['_method'] = method unless method == "get"
221
+ oidreq.redirect_url(trust_root || realm_url(req), return_to || request_url)
222
+ end
223
+
224
+ def add_simple_registration_fields(oidreq, fields)
225
+ sregreq = ::OpenID::SReg::Request.new
226
+
227
+ required = Array(fields['required']).reject(&URL_FIELD_SELECTOR)
228
+ sregreq.request_fields(required, true) if required.any?
229
+
230
+ optional = Array(fields['optional']).reject(&URL_FIELD_SELECTOR)
231
+ sregreq.request_fields(optional, false) if optional.any?
232
+
233
+ policy_url = fields['policy_url']
234
+ sregreq.policy_url = policy_url if policy_url
235
+
236
+ oidreq.add_extension(sregreq)
237
+ end
238
+
239
+ def add_attribute_exchange_fields(oidreq, fields)
240
+ axreq = ::OpenID::AX::FetchRequest.new
241
+
242
+ required = Array(fields['required']).select(&URL_FIELD_SELECTOR)
243
+ required.each { |field| axreq.add(::OpenID::AX::AttrInfo.new(field, nil, true)) }
244
+
245
+ optional = Array(fields['optional']).select(&URL_FIELD_SELECTOR)
246
+ optional.each { |field| axreq.add(::OpenID::AX::AttrInfo.new(field, nil, false)) }
247
+
248
+ oidreq.add_extension(axreq)
249
+ end
250
+
251
+ def add_oauth_fields(oidreq, options={})
252
+ return unless options[:oauth] && options[:oauth][:consumer]
253
+ oauth_request = OpenID::OAuth::Request.new options[:oauth][:consumer], options[:oauth][:scope]
254
+
255
+ oidreq.add_extension(oauth_request)
256
+ end
257
+
258
+ def default_store
259
+ require 'openid/store/memory'
260
+ ::OpenID::Store::Memory.new
261
+ end
262
+
263
+ def timeout_protection_from_identity_server
264
+ yield
265
+ rescue Timeout::Error
266
+ TimeoutResponse.new
267
+ end
268
+ end
269
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dominiquebrezinski-rack-openid
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Dominique
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-03-19 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.4"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: pelle-ruby-openid
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.1.9
34
+ version:
35
+ description: " Rack::OpenID provides a more HTTPish API around the ruby-openid library.\n"
36
+ email: dominique.brezinski@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.rdoc
43
+ - LICENSE
44
+ files:
45
+ - lib/rack/openid.rb
46
+ - README.rdoc
47
+ - LICENSE
48
+ has_rdoc: true
49
+ homepage: http://github.com/dominiquebrezinski/rack-openid
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.3.5
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Provides a more HTTPish API around the ruby-openid library
76
+ test_files: []
77
+