nov-rack-openid 1.2.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/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.
@@ -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,65 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.warning = true
7
+ end
8
+
9
+ begin
10
+ require 'jeweler'
11
+
12
+ class Jeweler
13
+ module Commands
14
+ class ReleaseGemspec
15
+ def run
16
+ unless clean_staging_area?
17
+ system "git status"
18
+ raise "Unclean staging area! Be sure to commit or .gitignore everything first. See `git status` above."
19
+ end
20
+ repo.checkout('releasable')
21
+ regenerate_gemspec!
22
+ commit_gemspec! if gemspec_changed?
23
+ output.puts "Pushing releasable to origin"
24
+ repo.push
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ class Jeweler
31
+ module Commands
32
+ class ReleaseToGit
33
+ def run
34
+ unless clean_staging_area?
35
+ system "git status"
36
+ raise "Unclean staging area! Be sure to commit or .gitignore everything first. See `git status` above."
37
+ end
38
+ repo.checkout('releasable')
39
+ repo.push
40
+ if release_not_tagged?
41
+ output.puts "Tagging #{release_tag}"
42
+ repo.add_tag(release_tag)
43
+ output.puts "Pushing #{release_tag} to origin"
44
+ repo.push('origin', release_tag)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ Jeweler::Tasks.new do |gem|
52
+ gem.name = 'nov-rack-openid'
53
+ gem.summary = %Q{Provides a more HTTPish API around the ruby-openid library}
54
+ gem.description = %Q{Provides a more HTTPish API around the ruby-openid library}
55
+ gem.email = 'nov@matake.jp'
56
+ gem.homepage = 'http://github.com/nov/rack-openid'
57
+ gem.authors = ['nov matake']
58
+ gem.add_dependency 'rack', '>= 1.1'
59
+ gem.add_dependency 'nov-ruby-openid', '>= 2.1.9'
60
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
61
+ end
62
+ Jeweler::GemcutterTasks.new
63
+ rescue LoadError
64
+ puts 'Jeweler not available. Install it with: gem install jeweler'
65
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.2.1
@@ -0,0 +1,2 @@
1
+ rack >=1.1.0
2
+ ruby-openid >=2.1.8
@@ -0,0 +1 @@
1
+ git://github.com/roman/rots.git
@@ -0,0 +1,283 @@
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
+ require 'openid/extensions/oauth'
9
+ require 'openid/extensions/ui'
10
+
11
+ module Rack #:nodoc:
12
+ # A Rack middleware that provides a more HTTPish API around the
13
+ # ruby-openid library.
14
+ #
15
+ # You trigger an OpenID request similar to HTTP authentication.
16
+ # From your app, return a "401 Unauthorized" and a "WWW-Authenticate"
17
+ # header with the identifier you would like to validate.
18
+ #
19
+ # On competition, the OpenID response is automatically verified and
20
+ # assigned to <tt>env["rack.openid.response"]</tt>.
21
+ class OpenID
22
+ # Helper method for building the "WWW-Authenticate" header value.
23
+ #
24
+ # Rack::OpenID.build_header(:identifier => "http://josh.openid.com/")
25
+ # #=> OpenID identifier="http://josh.openid.com/"
26
+ def self.build_header(params = {})
27
+ 'OpenID ' + params.map { |key, value|
28
+ if value.is_a?(Array)
29
+ "#{key}=\"#{value.join(',')}\""
30
+ else
31
+ "#{key}=\"#{value}\""
32
+ end
33
+ }.join(', ')
34
+ end
35
+
36
+ # Helper method for parsing "WWW-Authenticate" header values into
37
+ # a hash.
38
+ #
39
+ # Rack::OpenID.parse_header("OpenID identifier='http://josh.openid.com/'")
40
+ # #=> {:identifier => "http://josh.openid.com/"}
41
+ def self.parse_header(str)
42
+ params = {}
43
+ if str =~ AUTHENTICATE_REGEXP
44
+ str = str.gsub(/#{AUTHENTICATE_REGEXP}\s+/, '')
45
+ str.split(', ').each { |pair|
46
+ key, *value = pair.split('=')
47
+ value = value.join('=')
48
+ value.gsub!(/^\"/, '').gsub!(/\"$/, "")
49
+ value = value.split(',')
50
+ params[key] = value.length > 1 ? value : value.first
51
+ }
52
+ end
53
+ params
54
+ end
55
+
56
+ class TimeoutResponse #:nodoc:
57
+ include ::OpenID::Consumer::Response
58
+ STATUS = :failure
59
+ end
60
+
61
+ class MissingResponse #:nodoc:
62
+ include ::OpenID::Consumer::Response
63
+ STATUS = :missing
64
+ end
65
+
66
+ # :stopdoc:
67
+
68
+ HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS)
69
+
70
+ RESPONSE = "rack.openid.response"
71
+ AUTHENTICATE_HEADER = "WWW-Authenticate"
72
+ AUTHENTICATE_REGEXP = /^OpenID/
73
+
74
+ URL_FIELD_SELECTOR = lambda { |field| field.to_s =~ %r{^https?://} }
75
+
76
+ # :startdoc:
77
+
78
+ # Initialize middleware with application and optional OpenID::Store.
79
+ # If no store is given, OpenID::Store::Memory is used.
80
+ #
81
+ # use Rack::OpenID
82
+ #
83
+ # or
84
+ #
85
+ # use Rack::OpenID, OpenID::Store::Memcache.new
86
+ def initialize(app, store = nil)
87
+ @app = app
88
+ @store = store || default_store
89
+ end
90
+
91
+ # Standard Rack +call+ dispatch that accepts an +env+ and
92
+ # returns a <tt>[status, header, body]</tt> response.
93
+ def call(env)
94
+ req = Rack::Request.new(env)
95
+ if req.params["openid.mode"]
96
+ complete_authentication(env)
97
+ end
98
+
99
+ status, headers, body = @app.call(env)
100
+
101
+ qs = headers[AUTHENTICATE_HEADER]
102
+ if status.to_i == 401 && qs && qs.match(AUTHENTICATE_REGEXP)
103
+ begin_authentication(env, qs)
104
+ else
105
+ [status, headers, body]
106
+ end
107
+ end
108
+
109
+ private
110
+ def begin_authentication(env, qs)
111
+ req = Rack::Request.new(env)
112
+ params = self.class.parse_header(qs)
113
+ session = env["rack.session"]
114
+
115
+ unless session
116
+ raise RuntimeError, "Rack::OpenID requires a session"
117
+ end
118
+
119
+ consumer = ::OpenID::Consumer.new(session, @store)
120
+ identifier = params['identifier'] || params['identity']
121
+
122
+ begin
123
+ oidreq = consumer.begin(identifier)
124
+ add_simple_registration_fields(oidreq, params)
125
+ add_attribute_exchange_fields(oidreq, params)
126
+ add_oauth_fields(oidreq, params)
127
+ add_ui_fields(oidreq, params)
128
+ url = open_id_redirect_url(req, oidreq, params["trust_root"], params["return_to"], params["method"])
129
+ return redirect_to(url)
130
+ rescue ::OpenID::OpenIDError, Timeout::Error => e
131
+ env[RESPONSE] = MissingResponse.new
132
+ return @app.call(env)
133
+ end
134
+ end
135
+
136
+ def complete_authentication(env)
137
+ req = Rack::Request.new(env)
138
+ session = env["rack.session"]
139
+
140
+ unless session
141
+ raise RuntimeError, "Rack::OpenID requires a session"
142
+ end
143
+
144
+ oidresp = timeout_protection_from_identity_server {
145
+ consumer = ::OpenID::Consumer.new(session, @store)
146
+ consumer.complete(flatten_params(req.params), req.url)
147
+ }
148
+
149
+ env[RESPONSE] = oidresp
150
+
151
+ method = req.GET["_method"]
152
+ override_request_method(env, method)
153
+
154
+ sanitize_query_string(env)
155
+ end
156
+
157
+ def flatten_params(params)
158
+ Rack::Utils.parse_query(Rack::Utils.build_nested_query(params))
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
+ optional = Array(fields['optional']).select(&URL_FIELD_SELECTOR)
244
+
245
+ if required.any? || optional.any?
246
+ required.each do |field|
247
+ axreq.add(::OpenID::AX::AttrInfo.new(field, nil, true))
248
+ end
249
+
250
+ optional.each do |field|
251
+ axreq.add(::OpenID::AX::AttrInfo.new(field, nil, false))
252
+ end
253
+
254
+ oidreq.add_extension(axreq)
255
+ end
256
+ end
257
+
258
+ def add_oauth_fields(oidreq, fields)
259
+ if fields['oauth[consumer]']
260
+ oauthreq = ::OpenID::OAuth::Request.new(fields['oauth[consumer]'], Array(fields['oauth[scope]']).join(' '))
261
+ oidreq.add_extension(oauthreq)
262
+ end
263
+ end
264
+
265
+ def add_ui_fields(oidreq, fields)
266
+ if fields['ui[mode]'] || fields['ui[icon]'] || fields['ui[lang]']
267
+ uireq = ::OpenID::UI::Request.new(fields['ui[mode]'], fields['ui[icon]'], fields['ui[lang]'])
268
+ oidreq.add_extension(uireq)
269
+ end
270
+ end
271
+
272
+ def default_store
273
+ require 'openid/store/memory'
274
+ ::OpenID::Store::Memory.new
275
+ end
276
+
277
+ def timeout_protection_from_identity_server
278
+ yield
279
+ rescue Timeout::Error
280
+ TimeoutResponse.new
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,77 @@
1
+ require 'rack/openid'
2
+ require 'rack/request'
3
+
4
+ module Rack #:nodoc:
5
+ class OpenID
6
+ # A simple OpenID middleware that restricts access to
7
+ # a single identifier.
8
+ #
9
+ # use Rack::OpenID::SimpleAuth, "http://example.org"
10
+ #
11
+ # SimpleAuth will automatically insert the required Rack::OpenID
12
+ # middleware, so <tt>use Rack::OpenID</tt> is unnecessary.
13
+ class SimpleAuth
14
+ def self.new(*args)
15
+ Rack::OpenID.new(super)
16
+ end
17
+
18
+ attr_reader :app, :identifier
19
+
20
+ def initialize(app, identifier)
21
+ @app = app
22
+ @identifier = identifier
23
+ end
24
+
25
+ def call(env)
26
+ if session_authenticated?(env)
27
+ app.call(env)
28
+ elsif successful_response?(env)
29
+ authenticate_session(env)
30
+ redirect_to requested_url(env)
31
+ else
32
+ authentication_request
33
+ end
34
+ end
35
+
36
+ private
37
+ def session(env)
38
+ env['rack.session'] || raise_session_error
39
+ end
40
+
41
+ def raise_session_error
42
+ raise RuntimeError, 'Rack::OpenID::SimpleAuth requires a session'
43
+ end
44
+
45
+ def session_authenticated?(env)
46
+ session(env)['authenticated'] == true
47
+ end
48
+
49
+ def authenticate_session(env)
50
+ session(env)['authenticated'] = true
51
+ end
52
+
53
+ def successful_response?(env)
54
+ if resp = env[OpenID::RESPONSE]
55
+ resp.status == :success && resp.display_identifier == identifier
56
+ end
57
+ end
58
+
59
+ def requested_url(env)
60
+ req = Rack::Request.new(env)
61
+ req.url
62
+ end
63
+
64
+ def redirect_to(url)
65
+ [303, {'Content-Type' => 'text/html', 'Location' => url}, []]
66
+ end
67
+
68
+ def authentication_request
69
+ [401, { OpenID::AUTHENTICATE_HEADER => www_authenticate_header }, []]
70
+ end
71
+
72
+ def www_authenticate_header
73
+ OpenID.build_header(:identifier => identifier)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,54 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{nov-rack-openid}
8
+ s.version = "1.2.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["nov matake"]
12
+ s.date = %q{2011-01-18}
13
+ s.description = %q{Provides a more HTTPish API around the ruby-openid library}
14
+ s.email = %q{nov@matake.jp}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ "LICENSE",
21
+ "README.rdoc",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "deps.rip",
25
+ "dev-deps.rip",
26
+ "lib/rack/openid.rb",
27
+ "lib/rack/openid/simple_auth.rb",
28
+ "nov-rack-openid.gemspec",
29
+ "test/test_openid.rb"
30
+ ]
31
+ s.homepage = %q{http://github.com/nov/rack-openid}
32
+ s.require_paths = ["lib"]
33
+ s.rubygems_version = %q{1.4.1}
34
+ s.summary = %q{Provides a more HTTPish API around the ruby-openid library}
35
+ s.test_files = [
36
+ "test/test_openid.rb"
37
+ ]
38
+
39
+ if s.respond_to? :specification_version then
40
+ s.specification_version = 3
41
+
42
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
43
+ s.add_runtime_dependency(%q<rack>, [">= 1.1"])
44
+ s.add_runtime_dependency(%q<nov-ruby-openid>, [">= 2.1.9"])
45
+ else
46
+ s.add_dependency(%q<rack>, [">= 1.1"])
47
+ s.add_dependency(%q<nov-ruby-openid>, [">= 2.1.9"])
48
+ end
49
+ else
50
+ s.add_dependency(%q<rack>, [">= 1.1"])
51
+ s.add_dependency(%q<nov-ruby-openid>, [">= 2.1.9"])
52
+ end
53
+ end
54
+
@@ -0,0 +1,393 @@
1
+ require 'test/unit'
2
+ require 'net/http'
3
+
4
+ require 'rack'
5
+ require 'rack/openid'
6
+ require 'rack/openid/simple_auth'
7
+
8
+ log = Logger.new(STDOUT)
9
+ log.level = Logger::WARN
10
+ OpenID::Util.logger = log
11
+
12
+ class MockFetcher
13
+ def initialize(app)
14
+ @app = app
15
+ end
16
+
17
+ def fetch(url, body = nil, headers = nil, limit = nil)
18
+ opts = (headers || {}).dup
19
+ opts[:input] = body
20
+ opts[:method] = "POST" if body
21
+ env = Rack::MockRequest.env_for(url, opts)
22
+
23
+ status, headers, body = @app.call(env)
24
+
25
+ buf = []
26
+ buf << "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}"
27
+ headers.each { |header, value| buf << "#{header}: #{value}" }
28
+ buf << ""
29
+ body.each { |part| buf << part }
30
+
31
+ io = Net::BufferedIO.new(StringIO.new(buf.join("\n")))
32
+ res = Net::HTTPResponse.read_new(io)
33
+ res.reading_body(io, true) {}
34
+ OpenID::HTTPResponse._from_net_response(res, url)
35
+ end
36
+ end
37
+
38
+ RotsServerUrl = 'http://localhost:9292'
39
+
40
+ RotsApp = Rack::Builder.new do
41
+ require 'rots'
42
+
43
+ config = {
44
+ 'identity' => 'john.doe',
45
+ 'sreg' => {
46
+ 'nickname' => 'jdoe',
47
+ 'fullname' => 'John Doe',
48
+ 'email' => 'jhon@doe.com',
49
+ 'dob' => Date.parse('1985-09-21'),
50
+ 'gender' => 'M'
51
+ }
52
+ }
53
+
54
+ map("/%s" % config['identity']) do
55
+ run Rots::IdentityPageApp.new(config, {})
56
+ end
57
+
58
+ map '/server' do
59
+ run Rots::ServerApp.new(config, :storage => Dir.tmpdir)
60
+ end
61
+ end
62
+
63
+ OpenID.fetcher = MockFetcher.new(RotsApp)
64
+
65
+
66
+ class TestHeader < Test::Unit::TestCase
67
+ def test_build_header
68
+ assert_equal 'OpenID identity="http://example.com/"',
69
+ Rack::OpenID.build_header(:identity => "http://example.com/")
70
+ assert_equal 'OpenID identity="http://example.com/?foo=bar"',
71
+ Rack::OpenID.build_header(:identity => "http://example.com/?foo=bar")
72
+
73
+ header = Rack::OpenID.build_header(:identity => "http://example.com/", :return_to => "http://example.org/")
74
+ assert_match(/OpenID /, header)
75
+ assert_match(/identity="http:\/\/example\.com\/"/, header)
76
+ assert_match(/return_to="http:\/\/example\.org\/"/, header)
77
+
78
+ header = Rack::OpenID.build_header(:identity => "http://example.com/", :required => ["nickname", "email"])
79
+ assert_match(/OpenID /, header)
80
+ assert_match(/identity="http:\/\/example\.com\/"/, header)
81
+ assert_match(/required="nickname,email"/, header)
82
+ end
83
+
84
+ def test_parse_header
85
+ assert_equal({"identity" => "http://example.com/"},
86
+ Rack::OpenID.parse_header('OpenID identity="http://example.com/"'))
87
+ assert_equal({"identity" => "http://example.com/?foo=bar"},
88
+ Rack::OpenID.parse_header('OpenID identity="http://example.com/?foo=bar"'))
89
+ assert_equal({"identity" => "http://example.com/", "return_to" => "http://example.org/"},
90
+ Rack::OpenID.parse_header('OpenID identity="http://example.com/", return_to="http://example.org/"'))
91
+ assert_equal({"identity" => "http://example.com/", "required" => ["nickname", "email"]},
92
+ Rack::OpenID.parse_header('OpenID identity="http://example.com/", required="nickname,email"'))
93
+
94
+ # ensure we don't break standard HTTP basic auth
95
+ assert_equal({},
96
+ Rack::OpenID.parse_header('Realm="Example"'))
97
+ end
98
+ end
99
+
100
+ module RackTestHelpers
101
+ private
102
+ def process(*args)
103
+ env = Rack::MockRequest.env_for(*args)
104
+ @response = Rack::MockResponse.new(*@app.call(env))
105
+ end
106
+
107
+ def follow_redirect!
108
+ assert @response
109
+ assert_equal 303, @response.status
110
+
111
+ env = Rack::MockRequest.env_for(@response.headers['Location'])
112
+ status, headers, body = RotsApp.call(env)
113
+
114
+ uri = URI(headers['Location'])
115
+ process("#{uri.path}?#{uri.query}")
116
+ end
117
+ end
118
+
119
+ class TestOpenID < Test::Unit::TestCase
120
+ include RackTestHelpers
121
+
122
+ def test_with_get
123
+ @app = app
124
+ process('/', :method => 'GET')
125
+ follow_redirect!
126
+ assert_equal 200, @response.status
127
+ assert_equal 'GET', @response.headers['X-Method']
128
+ assert_equal '/', @response.headers['X-Path']
129
+ assert_equal 'success', @response.body
130
+ end
131
+
132
+ def test_with_deprecated_identity
133
+ @app = app
134
+ process('/', :method => 'GET', :identity => "#{RotsServerUrl}/john.doe?openid.success=true")
135
+ follow_redirect!
136
+ assert_equal 200, @response.status
137
+ assert_equal 'GET', @response.headers['X-Method']
138
+ assert_equal '/', @response.headers['X-Path']
139
+ assert_equal 'success', @response.body
140
+ end
141
+
142
+ def test_with_post_method
143
+ @app = app
144
+ process('/', :method => 'POST')
145
+ follow_redirect!
146
+ assert_equal 200, @response.status
147
+ assert_equal 'POST', @response.headers['X-Method']
148
+ assert_equal '/', @response.headers['X-Path']
149
+ assert_equal 'success', @response.body
150
+ end
151
+
152
+ def test_with_custom_return_to
153
+ @app = app(:return_to => 'http://example.org/complete')
154
+ process('/', :method => 'GET')
155
+ follow_redirect!
156
+ assert_equal 200, @response.status
157
+ assert_equal 'GET', @response.headers['X-Method']
158
+ assert_equal '/complete', @response.headers['X-Path']
159
+ assert_equal 'success', @response.body
160
+ end
161
+
162
+ def test_with_get_nested_params_custom_return_to
163
+ url = 'http://example.org/complete?user[remember_me]=true'
164
+ @app = app(:return_to => url)
165
+ process('/', :method => 'GET')
166
+ follow_redirect!
167
+ assert_equal 200, @response.status
168
+ assert_equal 'GET', @response.headers['X-Method']
169
+ assert_equal '/complete', @response.headers['X-Path']
170
+ assert_equal 'success', @response.body
171
+ assert_match(/remember_me/, @response.headers['X-Query-String'])
172
+ end
173
+
174
+ def test_with_post_nested_params_custom_return_to
175
+ url = 'http://example.org/complete?user[remember_me]=true'
176
+ @app = app(:return_to => url)
177
+ process('/', :method => 'POST')
178
+
179
+ assert_equal 303, @response.status
180
+ env = Rack::MockRequest.env_for(@response.headers['Location'])
181
+ status, headers, body = RotsApp.call(env)
182
+
183
+ uri, input = headers['Location'].split('?', 2)
184
+ process("http://example.org/complete?user[remember_me]=true", :method => 'POST', :input => input)
185
+
186
+ assert_equal 200, @response.status
187
+ assert_equal 'POST', @response.headers['X-Method']
188
+ assert_equal '/complete', @response.headers['X-Path']
189
+ assert_equal 'success', @response.body
190
+ assert_match(/remember_me/, @response.headers['X-Query-String'])
191
+ end
192
+
193
+ def test_with_post_method_custom_return_to
194
+ @app = app(:return_to => 'http://example.org/complete')
195
+ process('/', :method => 'POST')
196
+ follow_redirect!
197
+ assert_equal 200, @response.status
198
+ assert_equal 'GET', @response.headers['X-Method']
199
+ assert_equal '/complete', @response.headers['X-Path']
200
+ assert_equal 'success', @response.body
201
+ end
202
+
203
+ def test_with_custom_return_method
204
+ @app = app(:method => 'put')
205
+ process('/', :method => 'GET')
206
+ follow_redirect!
207
+ assert_equal 200, @response.status
208
+ assert_equal 'PUT', @response.headers['X-Method']
209
+ assert_equal '/', @response.headers['X-Path']
210
+ assert_equal 'success', @response.body
211
+ end
212
+
213
+ def test_with_simple_registration_fields
214
+ @app = app(:required => ['nickname', 'email'], :optional => 'fullname')
215
+ process('/', :method => 'GET')
216
+ follow_redirect!
217
+ assert_equal 200, @response.status
218
+ assert_equal 'GET', @response.headers['X-Method']
219
+ assert_equal '/', @response.headers['X-Path']
220
+ assert_equal 'success', @response.body
221
+ end
222
+
223
+ def test_with_attribute_exchange
224
+ @app = app(
225
+ :required => ['http://axschema.org/namePerson/friendly', 'http://axschema.org/contact/email'],
226
+ :optional => 'http://axschema.org/namePerson')
227
+ process('/', :method => 'GET')
228
+ follow_redirect!
229
+ assert_equal 200, @response.status
230
+ assert_equal 'GET', @response.headers['X-Method']
231
+ assert_equal '/', @response.headers['X-Path']
232
+ assert_equal 'success', @response.body
233
+ end
234
+
235
+ def test_with_oauth
236
+ @app = app(
237
+ :'oauth[consumer]' => 'www.example.com',
238
+ :'oauth[scope]' => ['http://docs.google.com/feeds/', 'http://spreadsheets.google.com/feeds/']
239
+ )
240
+ process('/', :method => 'GET')
241
+
242
+ location = @response.headers['Location']
243
+ assert_match(/openid.oauth.consumer/, location)
244
+ assert_match(/openid.oauth.scope/, location)
245
+
246
+ follow_redirect!
247
+ assert_equal 200, @response.status
248
+ assert_equal 'GET', @response.headers['X-Method']
249
+ assert_equal '/', @response.headers['X-Path']
250
+ assert_equal 'success', @response.body
251
+ end
252
+
253
+ def test_with_ui_mode
254
+ @app = app(
255
+ :'ui[mode]' => 'popup'
256
+ )
257
+ process('/', :method => 'GET')
258
+
259
+ location = @response.headers['Location']
260
+ assert_match(/openid.ui.mode/, location)
261
+
262
+ follow_redirect!
263
+ assert_equal 200, @response.status
264
+ assert_equal 'GET', @response.headers['X-Method']
265
+ assert_equal '/', @response.headers['X-Path']
266
+ assert_equal 'success', @response.body
267
+ end
268
+
269
+ def test_with_ui_icon
270
+ @app = app(
271
+ :'ui[icon]' => true
272
+ )
273
+ process('/', :method => 'GET')
274
+
275
+ location = @response.headers['Location']
276
+ assert_match(/openid.ui.icon/, location)
277
+
278
+ follow_redirect!
279
+ assert_equal 200, @response.status
280
+ assert_equal 'GET', @response.headers['X-Method']
281
+ assert_equal '/', @response.headers['X-Path']
282
+ assert_equal 'success', @response.body
283
+ end
284
+
285
+ def test_with_ui_lang
286
+ @app = app(
287
+ :'ui[lang]' => 'ja-JP'
288
+ )
289
+ process('/', :method => 'GET')
290
+
291
+ location = @response.headers['Location']
292
+ assert_match(/openid.ui.lang/, location)
293
+
294
+ follow_redirect!
295
+ assert_equal 200, @response.status
296
+ assert_equal 'GET', @response.headers['X-Method']
297
+ assert_equal '/', @response.headers['X-Path']
298
+ assert_equal 'success', @response.body
299
+ end
300
+
301
+ def test_with_missing_id
302
+ @app = app(:identifier => "#{RotsServerUrl}/john.doe")
303
+ process('/', :method => 'GET')
304
+ follow_redirect!
305
+ assert_equal 400, @response.status
306
+ assert_equal 'GET', @response.headers['X-Method']
307
+ assert_equal '/', @response.headers['X-Path']
308
+ assert_equal 'cancel', @response.body
309
+ end
310
+
311
+ def test_with_timeout
312
+ @app = app(:identifier => RotsServerUrl)
313
+ process('/', :method => "GET")
314
+ assert_equal 400, @response.status
315
+ assert_equal 'GET', @response.headers['X-Method']
316
+ assert_equal '/', @response.headers['X-Path']
317
+ assert_equal 'missing', @response.body
318
+ end
319
+
320
+ def test_sanitize_query_string
321
+ @app = app
322
+ process('/', :method => 'GET')
323
+ follow_redirect!
324
+ assert_equal 200, @response.status
325
+ assert_equal '/', @response.headers['X-Path']
326
+ assert_equal '', @response.headers['X-Query-String']
327
+ end
328
+
329
+ def test_passthrough_standard_http_basic_auth
330
+ @app = app
331
+ process('/', :method => 'GET', "MOCK_HTTP_BASIC_AUTH" => '1')
332
+ assert_equal 401, @response.status
333
+ end
334
+
335
+ private
336
+ def app(options = {})
337
+ options[:identifier] ||= "#{RotsServerUrl}/john.doe?openid.success=true"
338
+
339
+ app = lambda { |env|
340
+ if resp = env[Rack::OpenID::RESPONSE]
341
+ headers = {
342
+ 'X-Path' => env['PATH_INFO'],
343
+ 'X-Method' => env['REQUEST_METHOD'],
344
+ 'X-Query-String' => env['QUERY_STRING']
345
+ }
346
+ if resp.status == :success
347
+ [200, headers, [resp.status.to_s]]
348
+ else
349
+ [400, headers, [resp.status.to_s]]
350
+ end
351
+ elsif env["MOCK_HTTP_BASIC_AUTH"]
352
+ [401, {Rack::OpenID::AUTHENTICATE_HEADER => 'Realm="Example"'}, []]
353
+ else
354
+ [401, {Rack::OpenID::AUTHENTICATE_HEADER => Rack::OpenID.build_header(options)}, []]
355
+ end
356
+ }
357
+ Rack::Session::Pool.new(Rack::OpenID.new(app))
358
+ end
359
+ end
360
+
361
+ class TestSimpleAuth < Test::Unit::TestCase
362
+ include RackTestHelpers
363
+
364
+ def test_successful_login
365
+ @app = app "#{RotsServerUrl}/john.doe?openid.success=true"
366
+
367
+ process '/dashboard'
368
+ follow_redirect!
369
+
370
+ assert_equal 303, @response.status
371
+ assert_equal 'http://example.org/dashboard', @response.headers['Location']
372
+
373
+ cookie = @response.headers['Set-Cookie'].split(';').first
374
+ process '/dashboard', 'HTTP_COOKIE' => cookie
375
+ assert_equal 200, @response.status
376
+ assert_equal 'Hello', @response.body
377
+ end
378
+
379
+ def test_failed_login
380
+ @app = app "#{RotsServerUrl}/john.doe"
381
+
382
+ process '/dashboard'
383
+ follow_redirect!
384
+ assert_match RotsServerUrl, @response.headers['Location']
385
+ end
386
+
387
+ private
388
+ def app(identifier)
389
+ app = lambda { |env| [200, {'Content-Type' => 'text/html'}, ['Hello']] }
390
+ app = Rack::OpenID::SimpleAuth.new(app, identifier)
391
+ Rack::Session::Pool.new(app)
392
+ end
393
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nov-rack-openid
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 2
9
+ - 1
10
+ version: 1.2.1
11
+ platform: ruby
12
+ authors:
13
+ - nov matake
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-18 00:00:00 +09:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rack
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 13
30
+ segments:
31
+ - 1
32
+ - 1
33
+ version: "1.1"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: nov-ruby-openid
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 25
45
+ segments:
46
+ - 2
47
+ - 1
48
+ - 9
49
+ version: 2.1.9
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ description: Provides a more HTTPish API around the ruby-openid library
53
+ email: nov@matake.jp
54
+ executables: []
55
+
56
+ extensions: []
57
+
58
+ extra_rdoc_files:
59
+ - LICENSE
60
+ - README.rdoc
61
+ files:
62
+ - LICENSE
63
+ - README.rdoc
64
+ - Rakefile
65
+ - VERSION
66
+ - deps.rip
67
+ - dev-deps.rip
68
+ - lib/rack/openid.rb
69
+ - lib/rack/openid/simple_auth.rb
70
+ - nov-rack-openid.gemspec
71
+ - test/test_openid.rb
72
+ has_rdoc: true
73
+ homepage: http://github.com/nov/rack-openid
74
+ licenses: []
75
+
76
+ post_install_message:
77
+ rdoc_options: []
78
+
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ hash: 3
96
+ segments:
97
+ - 0
98
+ version: "0"
99
+ requirements: []
100
+
101
+ rubyforge_project:
102
+ rubygems_version: 1.4.1
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: Provides a more HTTPish API around the ruby-openid library
106
+ test_files:
107
+ - test/test_openid.rb