# Copyright 2014, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

require 'multi_json'
require 'googleauth/signet'
require 'googleauth/user_authorizer'
require 'googleauth/user_refresh'
require 'securerandom'

module Google
  module Auth
    # Varation on {Google::Auth::UserAuthorizer} adapted for Rack based
    # web applications.
    #
    # Example usage:
    #
    #     get('/') do
    #       user_id = request.session['user_email']
    #       credentials = authorizer.get_credentials(user_id, request)
    #       if credentials.nil?
    #         redirect authorizer.get_authorization_url(user_id: user_id,
    #                                                   request: request)
    #       end
    #       # Credentials are valid, can call APIs
    #       ...
    #    end
    #
    #    get('/oauth2callback') do
    #      url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(
    #        request)
    #      redirect url
    #    end
    #
    # Instead of implementing the callback directly, applications are
    # encouraged to use {Google::Auth::Web::AuthCallbackApp} instead.
    #
    # For rails apps, see {Google::Auth::ControllerHelpers}
    #
    # @see {Google::Auth::AuthCallbackApp}
    # @see {Google::Auth::ControllerHelpers}
    # @note Requires sessions are enabled
    class WebUserAuthorizer < Google::Auth::UserAuthorizer
      STATE_PARAM = 'state'
      AUTH_CODE_KEY = 'code'
      ERROR_CODE_KEY = 'error'
      SESSION_ID_KEY = 'session_id'
      CALLBACK_STATE_KEY = 'g-auth-callback'
      CURRENT_URI_KEY = 'current_uri'
      XSRF_KEY = 'g-xsrf-token'
      SCOPE_KEY = 'scope'

      NIL_REQUEST_ERROR = 'Request is required.'
      NIL_SESSION_ERROR = 'Sessions must be enabled'
      MISSING_AUTH_CODE_ERROR = 'Missing authorization code in request'
      AUTHORIZATION_ERROR = 'Authorization error: %s'
      INVALID_STATE_TOKEN_ERROR = 'State token does not match expected value'

      class << self
        attr_accessor :default
      end

      # Handle the result of the oauth callback. This version defers the
      # exchange of the code by temporarily stashing the results in the user's
      # session. This allows apps to use the generic
      # {Google::Auth::WebUserAuthorizer::CallbackApp} handler for the callback
      # without any additional customization.
      #
      # Apps that wish to handle the callback directly should use
      # {#handle_auth_callback} instead.
      #
      # @param [Rack::Request] request
      #  Current request
      def self.handle_auth_callback_deferred(request)
        callback_state, redirect_uri = extract_callback_state(request)
        request.session[CALLBACK_STATE_KEY] = MultiJson.dump(callback_state)
        redirect_uri
      end

      # Initialize the authorizer
      #
      # @param [Google::Auth::ClientID] client_id
      #  Configured ID & secret for this application
      # @param [String, Array<String>] scope
      #  Authorization scope to request
      # @param [Google::Auth::Stores::TokenStore] token_store
      #  Backing storage for persisting user credentials
      # @param [String] callback_uri
      #  URL (either absolute or relative) of the auth callback. Defaults
      #  to '/oauth2callback'
      def initialize(client_id, scope, token_store, callback_uri = nil)
        super(client_id, scope, token_store, callback_uri)
      end

      # Handle the result of the oauth callback. Exchanges the authorization
      # code from the request and persists to storage.
      #
      # @param [String] user_id
      #  Unique ID of the user for loading/storing credentials.
      # @param [Rack::Request] request
      #  Current request
      # @return (Google::Auth::UserRefreshCredentials, String)
      #  credentials & next URL to redirect to
      def handle_auth_callback(user_id, request)
        callback_state, redirect_uri = WebUserAuthorizer.extract_callback_state(
          request)
        WebUserAuthorizer.validate_callback_state(callback_state, request)
        credentials = get_and_store_credentials_from_code(
          user_id: user_id,
          code: callback_state[AUTH_CODE_KEY],
          scope: callback_state[SCOPE_KEY],
          base_url: request.url)
        [credentials, redirect_uri]
      end

      # Build the URL for requesting authorization.
      #
      # @param [String] login_hint
      #  Login hint if need to authorize a specific account. Should be a
      #  user's email address or unique profile ID.
      # @param [Rack::Request] request
      #  Current request
      # @param [String] redirect_to
      #  Optional URL to proceed to after authorization complete. Defaults to
      #  the current URL.
      # @param [String, Array<String>] scope
      #  Authorization scope to request. Overrides the instance scopes if
      #  not nil.
      # @return [String]
      #  Authorization url
      def get_authorization_url(options = {})
        options = options.dup
        request = options[:request]
        fail NIL_REQUEST_ERROR if request.nil?
        fail NIL_SESSION_ERROR if request.session.nil?

        redirect_to = options[:redirect_to] || request.url
        request.session[XSRF_KEY] = SecureRandom.base64
        options[:state] = MultiJson.dump(
          SESSION_ID_KEY => request.session[XSRF_KEY],
          CURRENT_URI_KEY => redirect_to)
        options[:base_url] = request.url
        super(options)
      end

      # Fetch stored credentials for the user.
      #
      # @param [String] user_id
      #  Unique ID of the user for loading/storing credentials.
      # @param [Rack::Request] request
      #  Current request
      # @param [Array<String>, String] scope
      #  If specified, only returns credentials that have all the \
      #  requested scopes
      # @return [Google::Auth::UserRefreshCredentials]
      #  Stored credentials, nil if none present
      # @raise [Signet::AuthorizationError]
      #  May raise an error if an authorization code is present in the session
      #  and exchange of the code fails
      def get_credentials(user_id, request, scope = nil)
        if request.session.key?(CALLBACK_STATE_KEY)
          # Note - in theory, no need to check required scope as this is
          # expected to be called immediately after a return from authorization
          state_json = request.session.delete(CALLBACK_STATE_KEY)
          callback_state = MultiJson.load(state_json)
          WebUserAuthorizer.validate_callback_state(callback_state, request)
          get_and_store_credentials_from_code(
            user_id: user_id,
            code: callback_state[AUTH_CODE_KEY],
            scope: callback_state[SCOPE_KEY],
            base_url: request.url)
        else
          super(user_id, scope)
        end
      end

      def self.extract_callback_state(request)
        state = MultiJson.load(request[STATE_PARAM] || '{}')
        redirect_uri = state[CURRENT_URI_KEY]
        callback_state = {
          AUTH_CODE_KEY => request[AUTH_CODE_KEY],
          ERROR_CODE_KEY =>  request[ERROR_CODE_KEY],
          SESSION_ID_KEY => state[SESSION_ID_KEY],
          SCOPE_KEY => request[SCOPE_KEY]
        }
        [callback_state, redirect_uri]
      end

      # Verifies the results of an authorization callback
      #
      # @param [Hash] state
      #  Callback state
      # @option state [String] AUTH_CODE_KEY
      #  The authorization code
      # @option state [String] ERROR_CODE_KEY
      #  Error message if failed
      # @param [Rack::Request] request
      #  Current request
      def self.validate_callback_state(state, request)
        if state[AUTH_CODE_KEY].nil?
          fail Signet::AuthorizationError, MISSING_AUTH_CODE_ERROR
        elsif state[ERROR_CODE_KEY]
          fail Signet::AuthorizationError,
               sprintf(AUTHORIZATION_ERROR, state[ERROR_CODE_KEY])
        elsif request.session[XSRF_KEY] != state[SESSION_ID_KEY]
          fail Signet::AuthorizationError, INVALID_STATE_TOKEN_ERROR
        end
      end

      # Small Rack app which acts as the default callback handler for the app.
      #
      # To configure in Rails, add to routes.rb:
      #
      #     match '/oauth2callback',
      #           to: Google::Auth::WebUserAuthorizer::CallbackApp,
      #           via: :all
      #
      # With Rackup, add to config.ru:
      #
      #     map '/oauth2callback' do
      #       run Google::Auth::WebUserAuthorizer::CallbackApp
      #     end
      #
      # Or in a classic Sinatra app:
      #
      #     get('/oauth2callback') do
      #       Google::Auth::WebUserAuthorizer::CallbackApp.call(env)
      #     end
      #
      # @see {Google::Auth::WebUserAuthorizer}
      class CallbackApp
        LOCATION_HEADER = 'Location'
        REDIR_STATUS = 302
        ERROR_STATUS = 500

        # Handle a rack request. Simply stores the results the authorization
        # in the session temporarily and redirects back to to the previously
        # saved redirect URL. Credentials can be later retrieved by calling.
        # {Google::Auth::Web::WebUserAuthorizer#get_credentials}
        #
        # See {Google::Auth::Web::WebUserAuthorizer#get_authorization_uri}
        # for how to initiate authorization requests.
        #
        # @param [Hash] env
        #  Rack environment
        # @return [Array]
        #  HTTP response
        def self.call(env)
          request = Rack::Request.new(env)
          return_url = WebUserAuthorizer.handle_auth_callback_deferred(request)
          if return_url
            [REDIR_STATUS, { LOCATION_HEADER => return_url }, []]
          else
            [ERROR_STATUS, {}, ['No return URL is present in the request.']]
          end
        end

        def call(env)
          self.class.call(env)
        end
      end
    end
  end
end
