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