lynndylanhurley/devise_token_auth

View on GitHub
app/controllers/devise_token_auth/concerns/set_user_by_token.rb

Summary

Maintainability
C
1 day
Test Coverage
A
98%
# frozen_string_literal: true

module DeviseTokenAuth::Concerns::SetUserByToken
  extend ActiveSupport::Concern
  include DeviseTokenAuth::Concerns::ResourceFinder

  included do
    before_action :set_request_start
    after_action :update_auth_header
  end

  protected

  # keep track of request duration
  def set_request_start
    @request_started_at = Time.zone.now
    @used_auth_by_token = true

    # initialize instance variables
    @token ||= DeviseTokenAuth::TokenFactory.new
    @resource ||= nil
    @is_batch_request ||= nil
  end

  # user auth
  def set_user_by_token(mapping = nil)
    # determine target authentication class
    rc = resource_class(mapping)

    # no default user defined
    return unless rc

    # gets the headers names, which was set in the initialize file
    uid_name = DeviseTokenAuth.headers_names[:'uid']
    other_uid_name = DeviseTokenAuth.other_uid && DeviseTokenAuth.headers_names[DeviseTokenAuth.other_uid.to_sym]
    access_token_name = DeviseTokenAuth.headers_names[:'access-token']
    client_name = DeviseTokenAuth.headers_names[:'client']
    authorization_name = DeviseTokenAuth.headers_names[:"authorization"]

    # Read Authorization token and decode it if present
    decoded_authorization_token = decode_bearer_token(request.headers[authorization_name])

    # gets values from cookie if configured and present
    parsed_auth_cookie = {}
    if DeviseTokenAuth.cookie_enabled
      auth_cookie = request.cookies[DeviseTokenAuth.cookie_name]
      if auth_cookie.present?
        parsed_auth_cookie = JSON.parse(auth_cookie)
      end
    end

    # parse header for values necessary for authentication
    uid              = request.headers[uid_name] || params[uid_name] || parsed_auth_cookie[uid_name] || decoded_authorization_token[uid_name]
    other_uid        = other_uid_name && request.headers[other_uid_name] || params[other_uid_name] || parsed_auth_cookie[other_uid_name]
    @token           = DeviseTokenAuth::TokenFactory.new unless @token
    @token.token     ||= request.headers[access_token_name] || params[access_token_name] || parsed_auth_cookie[access_token_name] || decoded_authorization_token[access_token_name]
    @token.client ||= request.headers[client_name] || params[client_name] || parsed_auth_cookie[client_name] || decoded_authorization_token[client_name]

    # client isn't required, set to 'default' if absent
    @token.client ||= 'default'

    # check for an existing user, authenticated via warden/devise, if enabled
    if DeviseTokenAuth.enable_standard_devise_support
      devise_warden_user = warden.user(mapping)
      if devise_warden_user && devise_warden_user.tokens[@token.client].nil?
        @used_auth_by_token = false
        @resource = devise_warden_user
        # REVIEW: The following line _should_ be safe to remove;
        #  the generated token does not get used anywhere.
        # @resource.create_new_auth_token
      end
    end

    # user has already been found and authenticated
    return @resource if @resource && @resource.is_a?(rc)

    # ensure we clear the client
    unless @token.present?
      @token.client = nil
      return
    end

    # mitigate timing attacks by finding by uid instead of auth token
    user = (uid && rc.dta_find_by(uid: uid)) || (other_uid && rc.dta_find_by("#{DeviseTokenAuth.other_uid}": other_uid))
    scope = rc.to_s.underscore.to_sym

    if user && user.valid_token?(@token.token, @token.client)
      # sign_in with bypass: true will be deprecated in the next version of Devise
      if respond_to?(:bypass_sign_in) && DeviseTokenAuth.bypass_sign_in
        bypass_sign_in(user, scope: scope)
      else
        sign_in(scope, user, store: false, event: :fetch, bypass: DeviseTokenAuth.bypass_sign_in)
      end
      return @resource = user
    else
      # zero all values previously set values
      @token.client = nil
      return @resource = nil
    end
  end

  def update_auth_header
    # cannot save object if model has invalid params
    return unless @resource && @token.client

    # Generate new client with existing authentication
    @token.client = nil unless @used_auth_by_token

    if @used_auth_by_token && !DeviseTokenAuth.change_headers_on_each_request
      # should not append auth header if @resource related token was
      # cleared by sign out in the meantime
      return if @resource.reload.tokens[@token.client].nil?

      auth_header = @resource.build_auth_headers(@token.token, @token.client)

      # update the response header
      response.headers.merge!(auth_header)

      # set a server cookie if configured
      if DeviseTokenAuth.cookie_enabled
        set_cookie(auth_header)
      end
    else
      unless @resource.reload.valid?
        @resource = @resource.class.find(@resource.to_param) # errors remain after reload
        # if we left the model in a bad state, something is wrong in our app
        unless @resource.valid?
          raise DeviseTokenAuth::Errors::InvalidModel, "Cannot set auth token in invalid model. Errors: #{@resource.errors.full_messages}"
        end
      end
      refresh_headers
    end
  end

  private

  def decode_bearer_token(bearer_token)
    return {} if bearer_token.blank?

    encoded_token = bearer_token.split.last # Removes the 'Bearer' from the string
    JSON.parse(Base64.strict_decode64(encoded_token)) rescue {}
  end

  def refresh_headers
    # Lock the user record during any auth_header updates to ensure
    # we don't have write contention from multiple threads
    @resource.with_lock do
      # should not append auth header if @resource related token was
      # cleared by sign out in the meantime
      return if @used_auth_by_token && @resource.tokens[@token.client].nil?

      _auth_header_from_batch_request = auth_header_from_batch_request

      # update the response header
      response.headers.merge!(_auth_header_from_batch_request)

      # set a server cookie if configured and is not a batch request
      if DeviseTokenAuth.cookie_enabled && !@is_batch_request
        set_cookie(_auth_header_from_batch_request)
      end
    end # end lock
  end

  def set_cookie(auth_header)
    cookies[DeviseTokenAuth.cookie_name] = DeviseTokenAuth.cookie_attributes.merge(value: auth_header.to_json)
  end

  def is_batch_request?(user, client)
    !params[:unbatch] &&
      user.tokens[client] &&
      user.tokens[client]['updated_at'] &&
      user.tokens[client]['updated_at'].to_time > @request_started_at - DeviseTokenAuth.batch_request_buffer_throttle
  end

  def auth_header_from_batch_request
    # determine batch request status after request processing, in case
    # another processes has updated it during that processing
    @is_batch_request = is_batch_request?(@resource, @token.client)

    auth_header = {}
    # extend expiration of batch buffer to account for the duration of
    # this request
    if @is_batch_request
      auth_header = @resource.extend_batch_buffer(@token.token, @token.client)

      # Do not return token for batch requests to avoid invalidated
      # tokens returned to the client in case of race conditions.
      # Use a blank string for the header to still be present and
      # being passed in a XHR response in case of
      # 304 Not Modified responses.
      auth_header[DeviseTokenAuth.headers_names[:"access-token"]] = ' '
      auth_header[DeviseTokenAuth.headers_names[:"expiry"]] = ' '
    else
      # update Authorization response header with new token
      auth_header = @resource.create_new_auth_token(@token.client)
    end
    auth_header
  end
end