app/controllers/server_controller.rb
class ServerController < ApplicationController
  
  # CSRF-protection must be skipped, because incoming
  # OpenID requests lack an authenticity token
  skip_before_filter :verify_authenticity_token
  # Error handling
  rescue_from OpenID::Server::ProtocolError, :with => :render_openid_error
  # Actions other than index require a logged in user
  before_filter :login_required, :except => [:index, :cancel, :seatbelt_config, :seatbelt_login_state]
  before_filter :ensure_valid_checkid_request, :except => [:index, :cancel, :seatbelt_config, :seatbelt_login_state]
  after_filter :clear_checkid_request, :only => [:cancel, :complete]
  # These methods are used to display information about the request to the user
  helper_method :sreg_request, :ax_fetch_request, :ax_store_request
  
  # This is the server endpoint which handles all incoming OpenID requests.
  # Associate and CheckAuth requests are answered directly - functionality
  # therefor is provided by the ruby-openid gem. Handling of CheckId requests
  # dependents on the users login state (see handle_checkid_request).
  # Yadis requests return information about this endpoint.
  def index
    clear_checkid_request
    respond_to do |format|
      format.html do
        if openid_request.is_a?(OpenID::Server::CheckIDRequest)
          handle_checkid_request
        elsif openid_request
          handle_non_checkid_request
        else
          render :text => t(:this_is_openid_not_a_human_ressource)
        end
      end
      format.xrds
    end
  end
  
  # This action decides how to process the current request and serves as
  # dispatcher and re-entry in case the request could not be processed 
  # directly (for instance if the user had to log in first).
  # When the user has already trusted the relying party, the request will
  # be answered based on the users release policy. If the request is immediate
  # (relying party wants no user interaction, used e.g. for ajax requests)
  # the request can only be answered if no further information (like simple 
  # registration data) is requested. Otherwise the user will be redirected
  # to the decision page.
  def proceed
    identity = identifier(current_account)
    if @site = current_account.sites.find_by_url(checkid_request.trust_root)
      resp = checkid_request.answer(true, nil, identity)
      resp = add_sreg(resp, @site.sreg_properties) if sreg_request
      resp = add_ax(resp, @site.ax_properties) if ax_fetch_request
      resp = add_pape(resp, auth_policies, auth_level, auth_time)
      render_response(resp)
    elsif checkid_request.immediate && (sreg_request || ax_store_request || ax_fetch_request)
      render_response(checkid_request.answer(false))
    elsif checkid_request.immediate
      render_response(checkid_request.answer(true, nil, identity))
    else
      redirect_to decide_path
    end
  end
  
  # Displays the decision page on that the user can confirm the request and
  # choose which data should be transfered to the relying party.
  def decide
    @site = current_account.sites.find_or_initialize_by_url(checkid_request.trust_root)
    @site.persona = current_account.personas.find(params[:persona_id] || :first) if sreg_request || ax_store_request || ax_fetch_request
  end
  
  # This action is called by submitting the decision form, the information entered by
  # the user is used to answer the request. If the user decides to always trust the
  # relying party, a new site according to the release policies the will be created.
  def complete
    if params[:cancel]
      cancel
    else  
      resp = checkid_request.answer(true, nil, identifier(current_account))
      if params[:always]
        @site = current_account.sites.find_or_create_by_persona_id_and_url(params[:site][:persona_id], params[:site][:url])
        @site.update_attributes(params[:site])
      elsif sreg_request || ax_fetch_request
        @site = current_account.sites.find_or_initialize_by_persona_id_and_url(params[:site][:persona_id], params[:site][:url])
        @site.attributes = params[:site]
      elsif ax_store_request
        @site = current_account.sites.find_or_initialize_by_persona_id_and_url(params[:site][:persona_id], params[:site][:url])
        not_supported, not_accepted, accepted = [], [], []
        ax_store_request.data.each do |type_uri, values|
          if property = Persona.attribute_name_for_type_uri(type_uri)
            store_attribute = params[:site][:ax_store][property.to_sym]
            if store_attribute && !store_attribute[:value].blank?
              @site.persona.update_attribute(property, values.first)
              accepted << type_uri
            else
              not_accepted << type_uri
            end
          else
            not_supported << type_uri
          end
        end
        ax_store_response = (accepted.count > 0) ? OpenID::AX::StoreResponse.new : OpenID::AX::StoreResponse.new(false, "None of the attributes were accepted.")
        resp.add_extension(ax_store_response)
      end
      resp = add_pape(resp, auth_policies, auth_level, auth_time)
      resp = add_sreg(resp, @site.sreg_properties) if sreg_request && @site.sreg_properties
      resp = add_ax(resp, @site.ax_properties) if ax_fetch_request && @site.ax_properties
      render_response(resp)
    end
  end
  
  # Cancels the current OpenID request
  def cancel
    redirect_to checkid_request.cancel_url
  end
  
  protected
  
  # Decides how to process an incoming checkid request. If the user is
  # already logged in he will be forwarded to the proceed action. If
  # the user is not logged in and the request is immediate, the request
  # cannot be answered successfully. In case the user is not logged in,
  # the request will be stored and the user is asked to log in.
  def handle_checkid_request
    if allow_verification?
      save_checkid_request
      redirect_to proceed_path
    elsif openid_request.immediate
      render_response(openid_request.answer(false))
    else
      reset_session
      request = save_checkid_request
      session[:return_to] = proceed_path
      redirect_to( request.from_trusted_domain? ? login_path : safe_login_path )
    end
  end
  
  # Stores the current OpenID request.
  # Returns the OpenIdRequest
  def save_checkid_request
    clear_checkid_request
    request = OpenIdRequest.create(:parameters => openid_params)
    session[:request_token] = request.token

    request
  end
  
  # Deletes the old request when a new one comes in.
  def clear_checkid_request
    unless session[:request_token].blank?
      OpenIdRequest.destroy_all :token => session[:request_token]
      session[:request_token] = nil
    end
  end
  
  # Use this as before_filter for every CheckID request based action.
  # Loads the current openid request and cancels if none can be found.
  # The user has to log in, if he has not verified his ownership of
  # the identifier, yet.
  def ensure_valid_checkid_request
    self.openid_request = checkid_request
    if !openid_request.is_a?(OpenID::Server::CheckIDRequest)
      redirect_to root_path, :alert => t(:identity_verification_request_invalid)
    elsif !allow_verification?
      flash[:notice] = logged_in? && !pape_requirements_met?(auth_time) ?
        t(:service_provider_requires_reauthentication_last_login_too_long_ago) :
        t(:login_to_verify_identity)
      session[:return_to] = proceed_path
      redirect_to login_path
    end
  end
  
  # The user must be logged in, he must be the owner of the claimed identifier
  # and the PAPE requirements must be met if applicable.
  def allow_verification?
    logged_in? && correct_identifier? && pape_requirements_met?(auth_time)
  end
  
  # Is the user allowed to verify the claimed identifier? The user
  # must be logged in, so that we know his identifier or the identifier
  # has to be selected by the server (id_select).
  def correct_identifier?
    (openid_request.identity == identifier(current_account) || openid_request.id_select)
  end
  
  # Clears the stored request and answers
  def render_response(resp)
    clear_checkid_request
    render_openid_response(resp)
  end
  
  # Transforms the parameters from the form to valid AX response values
  def transform_ax_data(parameters)
    data = {}
    parameters.each_pair do |key, details|
      if details['value']
        data["type.#{key}"] = details['type']
        data["value.#{key}"] = details['value']
      end
    end
    data
  end
  
  # Renders the exception message as text output
  def render_openid_error(exception)
    error = case exception
    when OpenID::Server::MalformedTrustRoot then "Malformed trust root '#{exception.to_s}'"
    else exception.to_s
    end
    render :text => "Invalid OpenID request: #{error}", :status => 500
  end
  
  private
  
  # The NIST Assurance Level, see:
  # http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#anchor12
  def auth_level
    if Masquerade::Application::Config['use_ssl']
      current_account.last_authenticated_with_yubikey? ? 3 : 2
    else
      0
    end
  end
  
  def auth_time
    current_account.last_authenticated_at
  end
  
  def auth_policies
    current_account.last_authenticated_with_yubikey? ? 
      [OpenID::PAPE::AUTH_MULTI_FACTOR, OpenID::PAPE::AUTH_PHISHING_RESISTANT] :
      []
  end
  
end
