Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jackFlick's avatar

How to create API login attempts

Hi All,

I've been searching for a while now how to implement an API login attempts in Sanctum. I've been seeing a lot of response for login attempts using the Auth of Laravel which is for Web. Is someone already tried doing it using API? or is there a package that I can use?

Hope that someone can help me work on API login attempts with lock out return and where to exactly put them. Currently this is my login controller

public function login(Request $request)
    {
        try {
            $request->validate([
                'email' => 'email|required',
                'password' => 'required'
            ]);
            $credentials = request(['email', 'password']);
            if (!Auth::attempt($credentials)) {
                return response()->json([
                    'status_code' => 401,
                    'message' => 'Unauthorized'
                ]);
            }
            $user = User::all()->where('email', $request->email)->where('email_verified_at', '<>', NULL)->first();

            if (!$user) {
                return [
                    'status_code' => 401,
                    "message" => 'Email is not verified',
                    "success" => false
                ];
            }

            if (!Hash::check($request->password, $user->password, [])) {
                throw new \Exception('Error in Login');
            }
            $tokenResult = $user->createToken('authToken')->plainTextToken;
            return response()->json([
                'status_code' => 200,
                'access_token' => $tokenResult,
                'token_type' => 'Bearer',
            ]);
        } catch (ValidationException $error) {
            return response()->json([
                'status_code' => 500,
                'message' => 'Error in Login',
                'error' => $error,
            ]);
        }
    }
0 likes
3 replies
Nakov's avatar

What you are looking for is called Rate Limiter.

If you open the App/Http/Kernel.php class you'll see that a throttle middleware is applied to the api route groups as well.

You can still apply the middleware to your route directly:

Route::post('login' ... )->middleware('throttle:60,1');

this means that it will allow 60 requests, and then block any future request for 1 minute, in which case 429 error response will be returned.

Here is a good article as well

https://medium.com/swlh/laravel-rate-limiting-in-production-926c4d581886

3 likes
Tippin's avatar
Tippin
Best Answer
Level 13

@nakov Only issue with using limiter as middleware for the logins means failed or not, it counts towards the limit, where you may only want to increment attempts on fail.

@jvbalcita So what I ended up doing was basically copying what the base login controller (OG/UI) did using the rate limiter into a trait to use in my api login (though I use passport).

Trait

<?php

namespace App\Http\Controllers\Auth\Concerns;

use Illuminate\Auth\Events\Lockout;
use Illuminate\Cache\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

trait ThrottlesAttempts
{
    /**
     * Determine if the user has too many failed login attempts.
     *
     * @param Request $request
     * @return bool
     */
    protected function hasTooManyAttempts(Request $request): bool
    {
        return $this->limiter()->tooManyAttempts(
            $this->throttleKey($request), $this->maxAttempts()
        );
    }

    /**
     * Increment the login attempts for the user.
     *
     * @param Request $request
     * @return void
     */
    protected function incrementAttempts(Request $request): void
    {
        $this->limiter()->hit(
            $this->throttleKey($request), $this->decayMinutes() * 60
        );
    }

    /**
     * Redirect the user after determining they are locked out.
     *
     * @param Request $request
     * @return void
     * @throws ValidationException
     */
    protected function sendLockoutResponse(Request $request): void
    {
        $seconds = $this->limiter()->availableIn(
            $this->throttleKey($request)
        );

        throw ValidationException::withMessages([
            $this->throttleKeyName() => [Lang::get('auth.throttle', [
                'seconds' => $seconds,
                'minutes' => ceil($seconds / 60),
            ])],
        ])->status(Response::HTTP_TOO_MANY_REQUESTS);
    }

    /**
     * Clear the login locks for the given user credentials.
     *
     * @param Request $request
     * @return void
     */
    protected function clearAttempts(Request $request): void
    {
        $this->limiter()->clear($this->throttleKey($request));
    }

    /**
     * Fire an event when a lockout occurs.
     *
     * @param Request $request
     * @return void
     */
    protected function fireLockoutEvent(Request $request): void
    {
        event(new Lockout($request));
    }

    /**
     * Get the throttle key for the given request.
     *
     * @param Request $request
     * @return string
     */
    protected function throttleKey(Request $request): string
    {
        return Str::lower($request->input($this->throttleKeyName())).'|'.$request->ip();
    }

    /**
     * Get the rate limiter instance.
     *
     * @return RateLimiter
     */
    protected function limiter(): RateLimiter
    {
        return app(RateLimiter::class);
    }

    /**
     * Get the maximum number of attempts to allow.
     *
     * @return int
     */
    public function maxAttempts(): int
    {
        return property_exists($this, 'maxAttempts')
            ? $this->maxAttempts
            : 5;
    }

    /**
     * Get the number of minutes to throttle for.
     *
     * @return int
     */
    public function decayMinutes(): int
    {
        return property_exists($this, 'decayMinutes')
            ? $this->decayMinutes
            : 1;
    }

    /**
     * Get the key used to throttle request
     *
     * @return string
     */
    public function throttleKeyName(): string
    {
        return property_exists($this, 'throttleKeyName')
            ? $this->throttleKeyName
            : 'email';
    }
}

Controller

use ThrottlesAttempts;


class ApiLogin extends Controller
{
    /**
     * Handle a login request to the application.
     */
    public function login(Request $request)
    {
        if($this->hasTooManyAttempts($request))
        {
            return $this->sendLockoutResponse($request);
        }

        if (!Auth::attempt($credentials)) {
            $this->incrementAttempts($request);

            return response()->json([
                'status_code' => 401,
                'message' => 'Unauthorized'
            ]);
        }
        //auth attempt good

        $this->clearAttempts($request);

        //w.e else you do
    }
1 like
jackFlick's avatar

I agree will @tippin that I will still allow correct and incorrect password without providing failed login attempts response to the user.

@tippin I will have to try this approach and hopefully works.

Thanks anyways.

1 like

Please or to participate in this conversation.