stefan-kolb/nucleus

View on GitHub
lib/nucleus_api/rack_middleware/basic_auth.rb

Summary

Maintainability
A
25 mins
Test Coverage
module Nucleus
  module API
    module Middleware
      # The {Nucleus::Middleware::BasicAuth} is a layer to handle HTTP Basic authentication in a similar style
      # than Rack itself does. The evaluation returns rack compatible error messages if either credentials are
      # missing (400) or could not be verified (401).<br>
      # The actual verification of the credentials is performed by the authentication block that is passed when
      # initializing this class.
      # @see {::Rack::Auth::Basic}
      #
      # @author Cedric Roeck (cedric.roeck@gmail.com)
      # @since 0.1.0
      class BasicAuth
        include Nucleus::API::ErrorBuilder
        include Nucleus::Logging

        # Initialize a new instance of the authentication middleware layer.
        # @param [Object] app the rack app to call
        # @param [String] realm the realm to be used for authentication
        # @yield [username, password, params, env] Returns a boolean value whether the passed credentials are
        # accepted by the current endpoint
        # @yieldparam [String] username the username to verify
        # @yieldparam [String] password the password to verify
        # @yieldparam [Hash] params the rack route params, e.g. form inputs such as the :endpoint_id
        # @yieldparam [Hash] env the global rack environment, includes values like the X-Request-ID
        # @yieldreturn [Boolean] true if credentials were verified and are correct, false if they are invalid
        def initialize(app, realm = nil, &authenticator)
          @app = app
          @realm = realm
          @authenticator = authenticator
        end

        def call(env)
          auth = ::Rack::Auth::Basic::Request.new(env)

          return unauthorized('No authentication header provided', env) unless auth.provided?

          return bad_request('Bad authentication request', env) unless auth.basic?

          begin
            # should be either valid or throws an exception
            if valid?(auth, env)
              env['REMOTE_USER'] = auth.username
              return @app.call(env)
            end
            unauthorized('Invalid credentials', env)
          rescue Nucleus::Errors::EndpointAuthenticationError => e
            log.debug 'Authentication attempt failed'
            send_response(e.ui_error, e.message, env)
          end
        end

        private

        def unauthorized(dev_msg, env)
          send_response(Nucleus::ErrorMessages::AUTH_UNAUTHORIZED, dev_msg, env,
                        'WWW-Authenticate' => challenge.to_s)
        end

        def bad_request(dev_msg, env)
          send_response(Nucleus::ErrorMessages::AUTH_BAD_REQUEST, dev_msg, env)
        end

        def send_response(error_msg, dev_msg, env, additional_headers = {})
          entity = build_error_entity(error_msg, dev_msg)
          msg = API::Models::Error.new(entity).to_json
          options = @app.instance_variable_get(:@app).instance_variable_get(:@options)
          content_types = Grape::ContentTypes.content_types_for(options[:content_types])
          # fallback to json if no content-type was found
          content_type = HashWithIndifferentAccess.new(content_types)[env['api.format'] || options[:format] || :json]
          headers = { 'Content-Type' => content_type }.merge additional_headers
          ::Rack::Response.new([msg], entity[:status], headers).finish
        end

        def challenge
          format('Basic realm="%s"', @realm)
        end

        def valid?(auth, env)
          route_args = env['rack.routing_args']
          @authenticator.call(*auth.credentials << route_args << env)
        end
      end
    end
  end
end