devfu-rack-openid-proxy 0.1.0
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/README.markdown +21 -0
- data/Rakefile +68 -0
- data/VERSION +1 -0
- data/lib/rack-openid-proxy.rb +153 -0
- data/lib/rack/openid/proxy.rb +1 -0
- data/spec/rack-openid-proxy_spec.rb +162 -0
- metadata +58 -0
data/README.markdown
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
= OpenID::Proxy
|
2
|
+
|
3
|
+
This is an OpenID Proxy application.
|
4
|
+
|
5
|
+
Sometimes applications run in an environment where it would be impossible
|
6
|
+
or very difficult to use an OpenID consumer.
|
7
|
+
|
8
|
+
This proxy is basically a very, very simple web service for making OpenID requests,
|
9
|
+
without having to have a full-blown local OpenID consumer.
|
10
|
+
|
11
|
+
This can be used as a Rack application or as a Rack middleware (to add a proxy to your local application).
|
12
|
+
|
13
|
+
This uses Rack::OpenID (which used ruby-openid) to do the heavy lifting.
|
14
|
+
|
15
|
+
== Installation
|
16
|
+
|
17
|
+
... coming soon ...
|
18
|
+
|
19
|
+
== Usage
|
20
|
+
|
21
|
+
... coming soon ...
|
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
puts "\nGem: rack-openid-proxy\n\n"
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'jeweler'
|
10
|
+
Jeweler::Tasks.new do |s|
|
11
|
+
s.name = 'rack-openid-proxy'
|
12
|
+
s.summary = 'A Rack app/middleware to act as a proxy for OpenID authentication'
|
13
|
+
s.email = 'remi@remitaylor.com'
|
14
|
+
s.homepage = 'http://github.com/devfu/rack-openid-proxy'
|
15
|
+
s.description = 'A Rack app/middleware to act as a proxy for OpenID authentication'
|
16
|
+
s.authors = %w( remi )
|
17
|
+
s.files = FileList['[A-Z]*', '{lib,spec,bin,examples}/**/*']
|
18
|
+
# s.add_dependency 'person-gemname'
|
19
|
+
# s.executables << 'script'
|
20
|
+
# s.rubyforge_project = 'gemname'
|
21
|
+
# s.extra_rdoc_files = %w( README.rdoc )
|
22
|
+
end
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
25
|
+
end
|
26
|
+
|
27
|
+
Spec::Rake::SpecTask.new do |t|
|
28
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "Run all examples with RCov"
|
32
|
+
Spec::Rake::SpecTask.new('rcov') do |t|
|
33
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
34
|
+
t.rcov = true
|
35
|
+
end
|
36
|
+
|
37
|
+
# require 'hanna'
|
38
|
+
# require 'darkfish-rdoc'
|
39
|
+
|
40
|
+
Rake::RDocTask.new do |rdoc|
|
41
|
+
rdoc.rdoc_dir = 'rdoc'
|
42
|
+
rdoc.title = 'rack-openid-proxy'
|
43
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
44
|
+
# rdoc.options += ["--template=#{`allison --path`}"] # sudo gem install allison
|
45
|
+
# rdoc.options += %w( -f darkfish ) # sudo gem install darkfish-rdoc
|
46
|
+
# rdoc.options += %w( -T hanna ) # sudo gem install mislav-hanna
|
47
|
+
rdoc.options += %w( -m README.rdoc ) # the initial page displayed
|
48
|
+
rdoc.rdoc_files.include('README.rdoc')
|
49
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
50
|
+
end
|
51
|
+
|
52
|
+
desc 'Confirm that gemspec is $SAFE'
|
53
|
+
task :safe do
|
54
|
+
require 'yaml'
|
55
|
+
require 'rubygems/specification'
|
56
|
+
data = File.read('rack-openid-proxy.gemspec')
|
57
|
+
spec = nil
|
58
|
+
if data !~ %r{!ruby/object:Gem::Specification}
|
59
|
+
Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
|
60
|
+
else
|
61
|
+
spec = YAML.load(data)
|
62
|
+
end
|
63
|
+
spec.validate
|
64
|
+
puts spec
|
65
|
+
puts "OK"
|
66
|
+
end
|
67
|
+
|
68
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,153 @@
|
|
1
|
+
%w( rubygems rack rack/openid digest/sha2 uri ).each {|lib| require lib }
|
2
|
+
|
3
|
+
class Object
|
4
|
+
def to_hash
|
5
|
+
YAML.load to_yaml.gsub(/ !ruby\/object.*/, '')
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Rack #:nodoc:
|
10
|
+
|
11
|
+
class OpenID #:nodoc:
|
12
|
+
|
13
|
+
# A simple OpenID Proxy Rack application / middleware
|
14
|
+
#
|
15
|
+
# == Notes
|
16
|
+
#
|
17
|
+
# * This *requires* that a Rack::Session middleware be enabled
|
18
|
+
# * This *requires* that the Rack::OpenID middleware be enabled (for now)
|
19
|
+
#
|
20
|
+
class Proxy
|
21
|
+
|
22
|
+
# this is stored in the Rack env (it gets set by Rack::OpenID)
|
23
|
+
OPENID_RESPONSE = 'rack.openid.response'
|
24
|
+
|
25
|
+
# this is stored in the session
|
26
|
+
RETURN_PATH = 'rack-openid-proxy.return_path'
|
27
|
+
|
28
|
+
class << self
|
29
|
+
attr_accessor :formatters
|
30
|
+
end
|
31
|
+
|
32
|
+
@formatters = {
|
33
|
+
:yml => lambda {|o| o.to_yaml },
|
34
|
+
:json => lambda {|o| require 'json'; o.to_hash.to_json },
|
35
|
+
:xml => lambda {|o| require 'yaxml'; YAXML::Yaxml.new(o.to_hash.to_yaml, :root_name => 'openid').to_yaxml }
|
36
|
+
}
|
37
|
+
|
38
|
+
DEFAULT_OPTIONS = {
|
39
|
+
:path => '/openid',
|
40
|
+
:token_store => {},
|
41
|
+
:force_ssl => false,
|
42
|
+
:delete_token_on_request => true
|
43
|
+
}
|
44
|
+
|
45
|
+
# The path that, when requested, does OpenID authentication
|
46
|
+
#
|
47
|
+
# * When used as a middleware, any other paths will call the inner Rack app
|
48
|
+
# * When used as a standalone all, any other paths will return a 404
|
49
|
+
#
|
50
|
+
attr_accessor :path
|
51
|
+
|
52
|
+
# If set to true, any OpenID-related requests that come in to
|
53
|
+
# this app as http:// will be redirected to https://
|
54
|
+
attr_accessor :force_ssl
|
55
|
+
|
56
|
+
# If set to true, tokens and their responses will be deleted after the
|
57
|
+
# first time they are requested
|
58
|
+
attr_accessor :delete_token_on_request
|
59
|
+
|
60
|
+
attr_accessor :token_store
|
61
|
+
|
62
|
+
def initialize *args
|
63
|
+
@app = args.shift if args.first.respond_to? :call
|
64
|
+
options = args.first || {}
|
65
|
+
|
66
|
+
DEFAULT_OPTIONS.each {|name, value| send "#{name}=", value }
|
67
|
+
options.each {|name, value| send "#{name}=", value } if options
|
68
|
+
|
69
|
+
raise_validation_exception unless valid?
|
70
|
+
end
|
71
|
+
|
72
|
+
def call env
|
73
|
+
request = Rack::Request.new env
|
74
|
+
|
75
|
+
if request.path_info =~ /^#{ path }\.?(.*)/
|
76
|
+
format = $1
|
77
|
+
|
78
|
+
if response = env[OPENID_RESPONSE]
|
79
|
+
token = "#{ Time.now.to_i }-#{ Digest::SHA2.hexdigest(response.to_yaml) }"
|
80
|
+
set token, response
|
81
|
+
uri = URI.parse env['rack.session'][RETURN_PATH]
|
82
|
+
uri.query = (uri.query.nil?) ? "token=#{token}" : "#{uri.query}&token=#{URI.escape(token)}"
|
83
|
+
[ 302, {'Location' => uri.to_s }, [] ]
|
84
|
+
|
85
|
+
elsif token = request.params['token']
|
86
|
+
return [ 302, {'Location' => request.url.to_s.sub('http','https')}, []
|
87
|
+
] if force_ssl && request.scheme != 'https' && env['HTTP_X_FORWARDED_PROTO'] != 'https'
|
88
|
+
response = get token
|
89
|
+
if response
|
90
|
+
render response, format
|
91
|
+
else
|
92
|
+
[ 200, {'Content-Type' => 'text/html'}, ["Token not found"] ]
|
93
|
+
end
|
94
|
+
|
95
|
+
elsif request.params['url']
|
96
|
+
env['rack.session'][RETURN_PATH] = request.referer
|
97
|
+
return [ 302, {'Location' => request.url.to_s.sub('http','https')}, []
|
98
|
+
] if force_ssl && request.scheme != 'https' && env['HTTP_X_FORWARDED_PROTO'] != 'https'
|
99
|
+
[ 401, {'WWW-Authenticate' => "OpenID identifier=\"#{ request.params['url'] }\""}, ["Redirecting to OpenID login ..."] ]
|
100
|
+
|
101
|
+
else
|
102
|
+
return [ 302, {'Location' => request.url.to_s.sub('http','https')}, []
|
103
|
+
] if force_ssl && request.scheme != 'https' && env['HTTP_X_FORWARDED_PROTO'] != 'https'
|
104
|
+
[ 200, {'Content-Type' => 'text/html'}, ["Unknown action. Did you forget to set ?url= or ?token= ?"] ]
|
105
|
+
end
|
106
|
+
|
107
|
+
elsif @app
|
108
|
+
@app.call env
|
109
|
+
|
110
|
+
else
|
111
|
+
[ 404, {'Content-Type' => 'text/html'}, ["Page not found"] ]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def get token
|
116
|
+
response = token_store[token]
|
117
|
+
token_store.delete token if delete_token_on_request
|
118
|
+
response
|
119
|
+
end
|
120
|
+
|
121
|
+
def set token, response
|
122
|
+
token_store[token] = response
|
123
|
+
end
|
124
|
+
|
125
|
+
protected
|
126
|
+
|
127
|
+
def formatters= hash_of_formatters
|
128
|
+
self.class.formatters.merge! hash_of_formatters
|
129
|
+
end
|
130
|
+
|
131
|
+
def render response, format = :json
|
132
|
+
format = :json if format.to_s.strip.empty?
|
133
|
+
raise "Invalid format: #{ format.inspect }" unless self.class.formatters.keys.include? format.to_sym
|
134
|
+
[ 200, {'Content-Type' => 'text/plain'}, [ self.class.formatters[format.to_sym].call(response) ] ] # all text/plain for now
|
135
|
+
end
|
136
|
+
|
137
|
+
# VALIDATIONS
|
138
|
+
|
139
|
+
def valid?
|
140
|
+
@errors = []
|
141
|
+
# @errors << ":foo option is required" unless path
|
142
|
+
@errors.empty?
|
143
|
+
end
|
144
|
+
|
145
|
+
def raise_validation_exception
|
146
|
+
raise @errors.join(', ')
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rack-openid-proxy'
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/rack-openid-proxy'
|
2
|
+
require 'spec'
|
3
|
+
require 'rackbox'
|
4
|
+
require 'rack/contrib'
|
5
|
+
|
6
|
+
# NOTE these actually hit OpenID servers! (yikes!)
|
7
|
+
|
8
|
+
# Rack::OpenID::Proxy has 3 different states that it can be in in ... these let us reproduce those
|
9
|
+
|
10
|
+
def openid_request options = {}
|
11
|
+
referer = options.delete(:referer) || 'http://www.google.com'
|
12
|
+
|
13
|
+
Rack::Session::Cookie.new(Rack::Config.new(Rack::OpenID.new( Rack::OpenID::Proxy.new(options) )){ |env|
|
14
|
+
env['rack.session'][Rack::OpenID::Proxy::RETURN_PATH] = referer
|
15
|
+
})
|
16
|
+
end
|
17
|
+
|
18
|
+
def openid_response options = {}
|
19
|
+
referer = options.delete(:referer) || 'http://www.google.com'
|
20
|
+
response = options.delete(:response) || { :OpenID => %w( response object ) }
|
21
|
+
|
22
|
+
Rack::Session::Cookie.new(Rack::Config.new(Rack::OpenID.new( Rack::OpenID::Proxy.new(options) )){ |env|
|
23
|
+
env[Rack::OpenID::Proxy::OPENID_RESPONSE] = response
|
24
|
+
env['rack.session'][Rack::OpenID::Proxy::RETURN_PATH] = referer
|
25
|
+
})
|
26
|
+
end
|
27
|
+
|
28
|
+
def token_request options = {}
|
29
|
+
token = options.delete(:token) || '1234567890-qwertyuiop'
|
30
|
+
response = options.delete(:response) || { :OpenID => %w( response object ) }
|
31
|
+
|
32
|
+
app = Rack::OpenID::Proxy.new({ :delete_token_on_request => false }.merge(options))
|
33
|
+
app.set token, response
|
34
|
+
|
35
|
+
# Rack::Session::Cookie.new(Rack::Config.new(Rack::OpenID.new( app )){ |env| })
|
36
|
+
Rack::Session::Cookie.new(Rack::OpenID.new( app ))
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_token url_or_response
|
40
|
+
if url_or_response.respond_to? :headers
|
41
|
+
get_token url_or_response.headers['Location']
|
42
|
+
else
|
43
|
+
url_or_response.match(/token=(\d{10}-\w{64})/)[1]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
Spec::Matchers.define :start_with do |string_should_start_with|
|
48
|
+
match do |actual_string|
|
49
|
+
actual_string.start_with? string_should_start_with
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe Rack::OpenID::Proxy do
|
54
|
+
|
55
|
+
it 'should work as a standalone Rack app' do
|
56
|
+
response = RackBox.request openid_request, '/openid?url=remitaylor.wordpress.com'
|
57
|
+
response.status.should == 303
|
58
|
+
response.headers['Location'].should start_with('http://remitaylor.wordpress.com/?openidserver')
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should 404 if path other than /openid is hit (standalone app)' do
|
62
|
+
response = RackBox.request openid_request, '/'
|
63
|
+
response.status.should == 404
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should work as a Rack middleware'
|
67
|
+
|
68
|
+
it 'should persist referer across session'
|
69
|
+
|
70
|
+
it 'should redirect back to the original referer (with token) after OpenID auth is complete' do
|
71
|
+
response = RackBox.request openid_response(:referer => 'http://remi.org'), '/openid'
|
72
|
+
response.status.should == 302
|
73
|
+
response.headers['Location'].should start_with('http://remi.org?token=')
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should be able to override where we get redirected to via ?redirect_to= (override referer)'
|
77
|
+
|
78
|
+
it 'should be able to customize where/how data (tokens) is persisted [data store]'
|
79
|
+
|
80
|
+
it 'should be able to customize where/how data (tokens) is persisted [get/set]'
|
81
|
+
|
82
|
+
it 'should return response when token provided' do
|
83
|
+
response = RackBox.request token_request(:token => 'abc', :response => 'Hello'), '/openid?token=abc'
|
84
|
+
response.status.should == 200
|
85
|
+
response.body.should == '"Hello"' # should default to JSON
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should be able to return response in various formats' do
|
89
|
+
app = token_request(:token => 'abc', :response => { :hello => %w( there people ) })
|
90
|
+
|
91
|
+
response = RackBox.request app, '/openid.json?token=abc'
|
92
|
+
response.status.should == 200
|
93
|
+
response.body.should == '{"hello":["there","people"]}'
|
94
|
+
|
95
|
+
response = RackBox.request app, '/openid.yml?token=abc'
|
96
|
+
response.status.should == 200
|
97
|
+
response.body.should == ({ :hello => %w( there people ) }).to_yaml
|
98
|
+
|
99
|
+
response = RackBox.request app, '/openid.xml?token=abc'
|
100
|
+
response.status.should == 200
|
101
|
+
response.body.should == "<openid xmlns:yaml='http://yaml.org/xml'><hello><_>there</_><_>people</_></hello></openid>" # gross but works
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'should return useful code and message if token is not found' do
|
105
|
+
response = RackBox.request token_request(:token => 'abc', :response => 'Hello'), '/openid?token=no-exist'
|
106
|
+
response.status.should == 200
|
107
|
+
response.body.downcase.should include('not found')
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should allow you to manage formatters' do
|
111
|
+
app = token_request(:token => 'abc', :response => { :hello => %w( there people ) },
|
112
|
+
:formatters => { :json => lambda {|o| require 'json'; o.to_json.upcase } })
|
113
|
+
|
114
|
+
response = RackBox.request app, '/openid.json?token=abc'
|
115
|
+
response.status.should == 200
|
116
|
+
response.body.should == '{"HELLO":["THERE","PEOPLE"]}'
|
117
|
+
|
118
|
+
app = token_request(:token => 'abc', :response => { :hello => %w( there people ) },
|
119
|
+
:formatters => { :test => lambda {|o| 'hello from custom formatter' } })
|
120
|
+
response = RackBox.request app, '/openid.test?token=abc'
|
121
|
+
response.status.should == 200
|
122
|
+
response.body.should == 'hello from custom formatter'
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'should allow you to customize the path (which does the OpenID request)' do
|
126
|
+
app = openid_response(:referer => 'http://remi.org', :path => '/do-openid')
|
127
|
+
|
128
|
+
response = RackBox.request app, '/openid'
|
129
|
+
response.status.should == 404
|
130
|
+
|
131
|
+
response = RackBox.request app, '/do-openid'
|
132
|
+
response.status.should == 302
|
133
|
+
response.headers['Location'].should start_with('http://remi.org?token=')
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should allow you to white|black list referers'
|
137
|
+
|
138
|
+
it 'should allow you to expire tokens (based on time since creation)'
|
139
|
+
|
140
|
+
it 'should allow you to expire tokens (based on number of times accessed)' do
|
141
|
+
app = token_request(:token => 'xyz', :response => "hi", :delete_token_on_request => true)
|
142
|
+
RackBox.request(app, '/openid?token=xyz').body.downcase.should == '"hi"'
|
143
|
+
RackBox.request(app, '/openid?token=xyz').body.downcase.should include("not found")
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should allow you to specify that all requests must be via SSL' do
|
147
|
+
response = RackBox.request openid_request(:referer => 'http://remi.org', :force_ssl => true), '/openid'
|
148
|
+
response.status.should == 302
|
149
|
+
response.headers['Location'].should start_with("https://")
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'should return a useful error message if no url' do
|
153
|
+
response = RackBox.request openid_request(:referer => 'http://remi.org'), '/openid'
|
154
|
+
response.status.should == 200
|
155
|
+
response.body.downcase.should include('url')
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'should return a useful error message if no session'
|
159
|
+
|
160
|
+
it 'should return a useful error message if no Rack::OpenID'
|
161
|
+
|
162
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: devfu-rack-openid-proxy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- remi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-30 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A Rack app/middleware to act as a proxy for OpenID authentication
|
17
|
+
email: remi@remitaylor.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.markdown
|
24
|
+
files:
|
25
|
+
- README.markdown
|
26
|
+
- Rakefile
|
27
|
+
- VERSION
|
28
|
+
- lib/rack-openid-proxy.rb
|
29
|
+
- lib/rack/openid/proxy.rb
|
30
|
+
- spec/rack-openid-proxy_spec.rb
|
31
|
+
has_rdoc: false
|
32
|
+
homepage: http://github.com/devfu/rack-openid-proxy
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options:
|
35
|
+
- --charset=UTF-8
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: "0"
|
43
|
+
version:
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 1.2.0
|
54
|
+
signing_key:
|
55
|
+
specification_version: 3
|
56
|
+
summary: A Rack app/middleware to act as a proxy for OpenID authentication
|
57
|
+
test_files:
|
58
|
+
- spec/rack-openid-proxy_spec.rb
|