dominiquebrezinski-rack-openid 1.0.2

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