require 'crypto_methods'
require 'html/htmltokenizer'
class OpenidController < ApplicationController
SIGNED_FIELDS = %w(mode identity return_to)
######################################################################
# Authentication Scaffolding #
######################################################################
def login
if params[:cancel]
redirect_to params[:fail_to] if params[:fail_to]
elsif params[:user]
cookies['user'] = params[:user]
redirect_to params[:success_to] if params[:success_to]
end
end
def identity
@user = params[:user]
end
def whoami
@user = cookies[:user]
end
def loggedin
@user = cookies[:user]
end
def decide
### TODO: TrustRoot verification
@user = cookies[:user]
@identity = openid.identity
@trust_root = openid.trust_root
@success_to = openid.success_to
@fail_to = openid.fail_to
if not @user or not owns?(openid.identity!)
redirect_to :action=>'login',
'success_to'=>@success_to, 'fail_to'=>@fail_to
elsif request.post?
if params['yes'] == 'yes'
allow = Allow.new
allow.user = @user
allow.trust_root = @trust_root
allow.save!
redirect_to @success_to if @success_to and not @success_to.empty?
else
redirect_to @fail_to if @fail_to and not @fail_to.empty?
end
redirect_to :action=>'main' unless performed?
elsif not @trust_root
render :file => "#{RAILS_ROOT}/public/404.html",
:status => '404 Not Found'
end
end
######################################################################
# OpenId Consumer #
######################################################################
def consumer
case openid.mode
when nil:
return if request.get?
return flash[:notice]='Please enter an Identity URL' if
params['identity'].nil? or params['identity'].empty?
server, delegate = autodiscover params['identity']
return flash[:notice]='No openid.server found for URI' unless server
delegate ||= params['identity']
assoc = Assoc.find :first, :conditions=>['identity=?',params['identity']],
:order=>'issued + interval lifetime second desc'
if assoc
assoc.destroy if assoc.expires_in <= 0
assoc = nil if assoc.expires_in < 30
end
if not assoc
dh = DiffieHellman.new
args = {
'openid.mode'=>'associate',
'openid.assoc_type'=>'HMAC-SHA1',
'openid.session_type'=>'DH-SHA1',
'openid.dh_consumer_public'=>dh.createKeyExchange.btwoc.base64
}
response = URI.parse(server).post(args)
return render(:text=>response.body,
:status=>"#{response.code} #{response.message}") unless
response.code == '200' and response.body[0] != '<'
response = response.body.parsekv
assoc = Assoc.new
assoc.identity = params['identity']
assoc.handle = response['assoc_handle']
assoc.lifetime = response['expires_in'].to_i
if response['dh_server_public']
server_public = response['dh_server_public']
dh_shared = dh.decryptKeyExchange(server_public.unbase64.unbtwoc)
assoc.secret = response['enc_mac_key'].unbase64^dh_shared.btwoc.sha1
else
assoc.secret = response['mac_key'].unbase64
end
assoc.save!
end
args = {
'openid.mode'=>'checkid_setup',
'openid.identity'=>delegate,
'openid.trust_root'=>url_for(:action=>'consumer', :only_path=>false),
'openid.return_to'=>url_for(:action=>'consumer', :only_path=>false),
}
args['openid.assoc_handle']=assoc.handle if assoc
redirect_to URI.parse(server).add_params(args).to_s
when 'cancel':
flash[:notice] = 'Cancelled by user'
when 'id_res':
assoc = Assoc.find :first, :conditions=>['handle=?',openid.assoc_handle!]
if not assoc
flash[:notice] = 'Unrecognized assoc_handle'
elsif not openid.invalidate_handle
params['openid.mode'] = 'id_res'
if params.sign(assoc.secret, openid.signed!.split(',')) == openid.sig!
flash[:notice] = 'Identity verified'
else
flash[:notice] = 'Signature not valid'
end
else
params['openid.mode'] = 'check_authentication'
server, delegate = autodiscover params['identity']
response = URI.parse(server).post
return render(:text=>response.body,
:status=>"#{response.code} #{response.message}") unless
response.code == '200' and response.body[0] != '<'
if response.body.parsekv['is_valid'] == 'true'
flash[:notice] = 'Identity verified'
else
flash[:notice] = 'Identity NOT verified'
end
assoc = Assoc.destroy :all,
:conditions=>['handle=?',openid.invalidate_handle!]
end
else
flash[:notice] = openid.mode + ' not implemented'
end
rescue BadRequest => error
flash[:notice] = error.field + ' is missing or invalid'
rescue Exception => exception
return flash[:notice]=CGI.escapeHTML(exception.to_s)
end
######################################################################
# OpenId Server #
######################################################################
def server
bad_request :mode unless respond_to? "server_" + openid.mode!
send "server_" + openid.mode
rescue BadRequest => error
@field = error.field
render(:file => "#{RAILS_ROOT}/public/400.html",
:status => '400 Bad Request')
end
# http://openid.net/specs.bml#mode-associate
def server_associate
openid.assoc_type ||= 'HMAC-SHA1'
bad_request :assoc_type unless openid.assoc_type=='HMAC-SHA1'
assoc = Assoc.new
assoc.identity = openid.identity
if openid.session_type == 'DH-SHA1'
dh = DiffieHellman.new
dh.p = openid.dh_modulus.unbase64.unbtwoc if openid.dh_modulus
dh.g = openid.dh_gen.unbase64.unbtwoc if openid.dh_gen
dh_shared = dh.decryptKeyExchange openid.dh_consumer_public!.unbase64.unbtwoc
reply = {
:session_type => openid.session_type!,
:dh_server_public => dh.createKeyExchange.btwoc.base64,
:enc_mac_key => (assoc.secret ^ dh_shared.btwoc.sha1).base64
}
elsif [nil,''].include? openid.session_type
reply = { :mac_key => assoc.secret.base64 }
else
bad_request :session_type
end
reply.update(
:assoc_type => openid.assoc_type,
:assoc_handle => assoc.handle,
:expires_in => assoc.lifetime
)
assoc.save!
render :text => reply.to_kv
end
# http://openid.net/specs.bml#mode-checkid_setup
def server_checkid_setup
if check_id?
id_response
else
redirect_to :action=>'decide',
'openid.identity'=>openid.identity!,
'openid.success_to'=>url_for(params),
'openid.fail_to'=>
URI.parse(openid.return_to!).add_params('openid.mode'=>'cancel').to_s,
'openid.trust_root'=>openid.trust_root!
end
end
# http://openid.net/specs.bml#mode-checkid_immediate
def server_checkid_immediate
if check_id?
id_response
else
args = {
'openid.mode' => 'checkid_setup',
'openid.identity' => openid.identity!,
'openid.trust_root' => openid.trust_root!,
'openid.return_to' => openid.return_to!,
}
args['openid.assoc_handle'] = openid.assoc_handle if openid.assoc_handle
reply = {
'openid.mode' => 'id_res',
'openid.user_setup_url' =>
url_for(:server, args.update(:only_path=>false))
}
redirect_to URI.parse(openid.return_to).add_params(reply).to_s
end
end
# http://openid.net/specs.bml#mode-check_authentication
def server_check_authentication
assoc = Assoc.find :first, :conditions=>['handle=?',openid.assoc_handle!]
reply = {'is_valid'=>'false'}
if assoc
if assoc.expires_in <= 0
reply.update('openid.invalidate_handle'=>assoc.handle)
assoc.destroy
else
params['openid.mode'] = 'id_res'
if params.sign(assoc.secret, openid.signed!.split(',')) == openid.sig!
reply = {'is_valid'=>'true'}
end
end
end
render :text => reply.to_kv
end
# raise bad request exception, specifying a field
def bad_request field
bad_request 'openid.'+field.to_s if field.is_a? Symbol
raise BadRequest.new(field.sub(/[?=!]$/, ''))
end
private
######################################################################
# Utility Methods #
######################################################################
# logged in, and owns identity, and trusts the provided root
def check_id?
(cookies[:user]) and owns?(openid.identity!) and
(Allow.find(:first, :conditions=>
['user=? and trust_root=?',cookies[:user], openid.trust_root]))
end
# current logged in user "owns" the specified identity URI?
def owns? url
url == url_for(:action=>'identity',
:user=>cookies[:user], :only_path=>false)
end
# discover server (and optionally, delegate) to use to validate identity
def autodiscover identity
tokenizer = HTMLTokenizer.new URI.parse(identity).get.body
while link = tokenizer.getTag('link')
server ||= link.attr_hash['href'] if
link.attr_hash['rel'] == 'openid.server'
delegate ||= link.attr_hash['href'] if
link.attr_hash['rel'] == 'openid.delegate'
end
return server, delegate
end
# validated identity in the form of a response
def id_response
reply = {
'openid.mode' => 'id_res',
'openid.return_to' => openid.return_to!,
'openid.identity' => openid.identity!,
}
if openid.assoc_handle
assoc = Assoc.find :first, :conditions=>['handle=?',openid.assoc_handle]
if (not assoc) or (assoc.expires_in <= 0)
assoc.destroy if assoc
reply['openid.invalidate_handle'] = openid.assoc_handle
openid.assoc_handle = nil
end
end
if not openid.assoc_handle
assoc = Assoc.new
assoc.save!
assoc.identity = openid.identity
openid.assoc_handle = assoc.handle
end
reply['openid.assoc_handle'] = openid.assoc_handle
reply['openid.signed'] = SIGNED_FIELDS.join(',')
reply['openid.sig'] = reply.sign(assoc.secret, SIGNED_FIELDS)
redirect_to URI.parse(openid.return_to).add_params(reply).to_s
end
# Exception to be thrown for missing or invalid fields
class BadRequest < Exception
attr_reader :field
def initialize field
@field = field
end
end
# provide convenient access to openid params
def openid
if ! @openid
@openid = params.dup
@openid['openid.controller'] = self
def @openid.method_missing symbol, *args
if symbol.to_s[-1] == ?=
self['openid.'+symbol.to_s[0..-2]]=args[0]
elsif symbol.to_s[-1] == ?!
self['openid.'+symbol.to_s[0..-2]] or controller.bad_request(symbol)
else
self['openid.'+symbol.to_s]
end
end
end
@openid
end
end