1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
|
require 'rack/request'
require 'rack/utils'
require 'openid'
require 'openid/consumer'
require 'openid/extensions/sreg'
require 'openid/extensions/ax'
require 'openid/extensions/oauth'
require 'openid/extensions/pape'
module Rack
# A Rack middleware that provides a more HTTPish API around the
# ruby-openid library.
#
# 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.
#
# On competition, the OpenID response is automatically verified and
# assigned to env["rack.openid.response"].
class OpenID
# Helper method for building the "WWW-Authenticate" header value.
#
# Rack::OpenID.build_header(:identifier => "http://josh.openid.com/")
# #=> OpenID identifier="http://josh.openid.com/"
def self.build_header(params = {})
'OpenID ' + params.map { |key, value|
if value.is_a?(Array)
"#{key}=\"#{value.join(',')}\""
else
"#{key}=\"#{value}\""
end
}.join(', ')
end
# Helper method for parsing "WWW-Authenticate" header values into
# a hash.
#
# Rack::OpenID.parse_header("OpenID identifier='http://josh.openid.com/'")
# #=> {:identifier => "http://josh.openid.com/"}
def self.parse_header(str)
params = {}
if str =~ AUTHENTICATE_REGEXP
str = str.gsub(/#{AUTHENTICATE_REGEXP}\s+/, '')
str.split(', ').each { |pair|
key, *value = pair.split('=')
value = value.join('=')
value.gsub!(/^\"/, '').gsub!(/\"$/, "")
value = value.split(',')
params[key] = value.length > 1 ? value : value.first
}
end
params
end
class TimeoutResponse
include ::OpenID::Consumer::Response
STATUS = :failure
end
class MissingResponse
include ::OpenID::Consumer::Response
STATUS = :missing
end
HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS)
RESPONSE = "rack.openid.response"
AUTHENTICATE_HEADER = "WWW-Authenticate"
AUTHENTICATE_REGEXP = /^OpenID/
URL_FIELD_SELECTOR = lambda { |field| field.to_s =~ %r{^https?://} }
# Initialize middleware with application and optional OpenID::Store.
# If no store is given, OpenID::Store::Memory is used.
#
# use Rack::OpenID
#
# or
#
# use Rack::OpenID, OpenID::Store::Memcache.new
def initialize(app, store = nil)
@app = app
@store = store || default_store
end
# Standard Rack +call+ dispatch that accepts an +env+ and
# returns a [status, header, body] response.
def call(env)
req = Rack::Request.new(env)
sanitize_params!(req.params)
if req.params["openid.mode"]
complete_authentication(env)
end
status, headers, body = @app.call(env)
qs = headers[AUTHENTICATE_HEADER]
if status.to_i == 401 && qs && qs.match(AUTHENTICATE_REGEXP)
begin_authentication(env, qs)
else
[status, headers, body]
end
end
private
def sanitize_params!(params)
['openid.sig', 'openid.response_nonce'].each do |param|
(params[param] || '').gsub!(' ', '+')
end
end
def begin_authentication(env, qs)
req = Rack::Request.new(env)
params = self.class.parse_header(qs)
session = env["rack.session"]
unless session
raise RuntimeError, "Rack::OpenID requires a session"
end
consumer = ::OpenID::Consumer.new(session, @store)
identifier = params['identifier'] || params['identity']
begin
oidreq = consumer.begin(identifier)
add_simple_registration_fields(oidreq, params)
add_attribute_exchange_fields(oidreq, params)
add_oauth_fields(oidreq, params)
add_pape_fields(oidreq, params)
url = open_id_redirect_url(req, oidreq, params)
return redirect_to(url)
rescue ::OpenID::OpenIDError, Timeout::Error => e
env[RESPONSE] = MissingResponse.new
return @app.call(env)
end
end
def complete_authentication(env)
req = Rack::Request.new(env)
session = env["rack.session"]
unless session
raise RuntimeError, "Rack::OpenID requires a session"
end
oidresp = timeout_protection_from_identity_server {
consumer = ::OpenID::Consumer.new(session, @store)
consumer.complete(flatten_params(req.params), req.url)
}
env[RESPONSE] = oidresp
method = req.GET["_method"]
override_request_method(env, method)
sanitize_query_string(env)
end
def flatten_params(params)
Rack::Utils.parse_query(Rack::Utils.build_nested_query(params))
end
def override_request_method(env, method)
return unless method
method = method.upcase
if HTTP_METHODS.include?(method)
env["REQUEST_METHOD"] = method
end
end
def sanitize_query_string(env)
query_hash = env["rack.request.query_hash"]
query_hash.delete("_method")
query_hash.delete_if do |key, value|
key =~ /^openid\./
end
env["QUERY_STRING"] = env["rack.request.query_string"] =
Rack::Utils.build_query(env["rack.request.query_hash"])
qs = env["QUERY_STRING"]
request_uri = (env["PATH_INFO"] || "").dup
request_uri << "?" + qs unless qs == ""
env["REQUEST_URI"] = request_uri
end
def scheme_with_host_and_port(req, host = nil)
url = req.scheme + "://"
url << (host || req.host)
scheme, port = req.scheme, req.port
if scheme == "https" && port != 443 ||
scheme == "http" && port != 80
url << ":#{port}"
end
url
end
def realm(req, domain = nil)
if domain
scheme_with_host_and_port(req, domain)
else
scheme_with_host_and_port(req)
end
end
def request_url(req)
url = scheme_with_host_and_port(req)
url << req.script_name
url << req.path_info
url << "?#{req.query_string}" if req.query_string.to_s.length > 0
url
end
def redirect_to(url)
[303, {"Content-Type" => "text/html", "Location" => url}, []]
end
def open_id_redirect_url(req, oidreq, options)
trust_root = options["trust_root"]
return_to = options["return_to"]
method = options["method"]
immediate = options["immediate"] == "true"
realm = realm(req, options["realm_domain"])
request_url = request_url(req)
if return_to
method ||= "get"
else
return_to = request_url
method ||= req.request_method
end
method = method.to_s.downcase
oidreq.return_to_args['_method'] = method unless method == "get"
oidreq.redirect_url(trust_root || realm, return_to || request_url, immediate)
end
def add_simple_registration_fields(oidreq, fields)
sregreq = ::OpenID::SReg::Request.new
required = Array(fields['required']).reject(&URL_FIELD_SELECTOR)
sregreq.request_fields(required, true) if required.any?
optional = Array(fields['optional']).reject(&URL_FIELD_SELECTOR)
sregreq.request_fields(optional, false) if optional.any?
policy_url = fields['policy_url']
sregreq.policy_url = policy_url if policy_url
oidreq.add_extension(sregreq)
end
def add_attribute_exchange_fields(oidreq, fields)
axreq = ::OpenID::AX::FetchRequest.new
required = Array(fields['required']).select(&URL_FIELD_SELECTOR)
optional = Array(fields['optional']).select(&URL_FIELD_SELECTOR)
if required.any? || optional.any?
required.each do |field|
axreq.add(::OpenID::AX::AttrInfo.new(field, nil, true))
end
optional.each do |field|
axreq.add(::OpenID::AX::AttrInfo.new(field, nil, false))
end
oidreq.add_extension(axreq)
end
end
def add_oauth_fields(oidreq, fields)
if (consumer = fields['oauth[consumer]']) && (scope = fields['oauth[scope]'])
oauthreq = ::OpenID::OAuth::Request.new(consumer, Array(scope).join(' '))
oidreq.add_extension(oauthreq)
end
end
def add_pape_fields(oidreq, fields)
preferred_auth_policies = fields['pape[preferred_auth_policies]']
max_auth_age = fields['pape[max_auth_age]']
if preferred_auth_policies || max_auth_age
preferred_auth_policies = preferred_auth_policies.split if preferred_auth_policies.is_a?(String)
pape_request = ::OpenID::PAPE::Request.new(preferred_auth_policies || [], max_auth_age)
oidreq.add_extension(pape_request)
end
end
def default_store
require 'openid/store/memory'
::OpenID::Store::Memory.new
end
def timeout_protection_from_identity_server
yield
rescue Timeout::Error
TimeoutResponse.new
end
end
end
|