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

trav1s's avatar

Auth::attempt() always return false with custom provider. Why?

Hello! I want to use SRP6 in my Laravel application. The registration is working fine, but the authentication is not working and the session is not being created. Can you please tell me what I'm doing wrong?

SRP6Service

<?php

namespace App\Services;

use App\DTO\SRP6Data;

class SRP6Service
{
    public function verifyLogin($username, $password, $salt, $verifier): bool
    {
        // re-calculate the verifier using the provided username + password and the stored salt
        $checkVerifier = $this->calculateSRP6Verifier($username, $password, $salt);

        // compare it against the stored verifier
        return ($verifier === $checkVerifier);
    }

    public function getSaltAndVerifier($username, $password): SRP6Data
    {
        $salt = random_bytes(32);
        $verifier = $this->calculateSRP6Verifier($username, $password, $salt);

        return new SRP6Data(
            salt: $salt,
            verifier: $verifier
        );
    }

    private function calculateSRP6Verifier($username, $password, $salt): string
    {
        // algorithm constants
        $g = gmp_init(7);
        $N = gmp_init('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7', 16);

        // calculate first hash
        $h1 = sha1(strtoupper($username . ':' . $password), true);

        $h2 = sha1($salt . $h1, true);

        // convert to integer (little-endian)
        $h2 = gmp_import($h2, 1, GMP_LSW_FIRST);

        // g^h2 mod N
        $verifier = gmp_powm($g, $h2, $N);

        // convert back to a byte array (little-endian)
        $verifier = gmp_export($verifier, 1, GMP_LSW_FIRST);

        // pad to 32 bytes, remember that zeros go on the end in little-endian!
        $verifier = str_pad($verifier, 32, chr(0), STR_PAD_RIGHT);

        // done!
        return $verifier;
    }
}

SRP6Data

<?php

namespace App\DTO;

class SRP6Data
{
    public function __construct(
        public readonly string $salt,
        public readonly string $verifier
    ) {}

SRP6ServiceProvider

<?php

namespace App\Providers;

use App\Services\SRP6Service;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;

class SRP6ServiceProvider extends EloquentUserProvider
{
    public function validateCredentials(UserContract $user, array $credentials): bool
    {
        /** @var SRP6Service $SRP6Service */
        $SRP6Service = app(SRP6Service::class);

        $username = $credentials['email'];
        $password = $credentials['password'];

        return $SRP6Service->verifyLogin(
            $username,
            $password,
            $user->salt,
            $user->verifier,
        );
    }
}

AuthServiceProvider

    /**
     * Register any authentication / authorization services.
     */
    public function boot(): void
    {
        Auth::provider('srp6', function ($app, array $config) {
            return $app->make(SRP6ServiceProvider::class, $config);
        });
    }

config/auth.php

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'srp6',
            'model' => App\Models\User::class,
        ],
    ],

AuthController

<?php

namespace App\Http\Controllers\Game\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;

class AuthController extends Controller
{
    public function create(): View
    {
        return view('auth.auth');
    }

    /**
     * Handle an incoming authentication request.
     *
     * @param  \App\Http\Requests\LoginRequest $request
     * @return RedirectResponse
     */
    public function store(LoginRequest $request): RedirectResponse
    {
        $data = $request->validated();

        if (Auth::attempt($data)) {
            return redirect()->route('index')->with('success', 'Logged in successfully');
        }

        return back()->with('error', 'Incorrect username or password');
    }

    /**
     * Destroy an authenticated session.
     *
     * @param  \Illuminate\Http\Request $request
     * @return RedirectResponse
     */
    public function destroy(Request $request): RedirectResponse
    {
        Auth::guard('web')->logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return redirect('/login');
    }
}

LoginRequest

<?php

namespace App\Http\Requests;

use App\Models\User;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;

class LoginRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'email' => ['required', 'email', Rule::exists(User::class)],
            'password' => ['required', 'string'],
        ];
    }

    /**
     * Attempt to authenticate the request's credentials.
     *
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function authenticate(): void
    {
        $this->ensureIsNotRateLimited();

        if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
            RateLimiter::hit($this->throttleKey());

            throw ValidationException::withMessages([
                'email' => trans('auth.failed'),
            ]);
        }

        RateLimiter::clear($this->throttleKey());
    }

    /**
     * Ensure the login request is not rate limited.
     *
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function ensureIsNotRateLimited(): void
    {
        if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
            return;
        }

        event(new Lockout($this));

        $seconds = RateLimiter::availableIn($this->throttleKey());

        throw ValidationException::withMessages([
            'email' => trans('auth.throttle', [
                'seconds' => $seconds,
                'minutes' => ceil($seconds / 60),
            ]),
        ]);
    }

    /**
     * Get the rate limiting throttle key for the request.
     *
     * @return string
     */
    public function throttleKey(): string
    {
        return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip());
    }
}

That is, in the store method in AuthController I receive false in if (Auth::attempt($data)) although the data from Request is coming

And here is my RegisterController which works great

<?php

namespace App\Http\Controllers\Game\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\SRP6Service;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules;
use Illuminate\Validation\Rule;
use Illuminate\View\View;

class RegisterController extends Controller
{
    public function create(): View
    {
        return view('auth.register');
    }

    public function store(Request $request, SRP6Service $SRP6Service): JsonResponse
    {
        $validated = $request->validate([
            'email' => ['required', 'string', 'email:rfc,dns', 'max:255', Rule::unique(User::class)],
            'name' => ['required', 'string', 'min:3', 'max:255'],
            'password' => ['required', 'confirmed', Rules\Password::min(4)],
            'agreement' => 'accepted',
        ]);

        $validated['name'] = strtoupper($validated['name']);

        $SRP6Data = $SRP6Service->getSaltAndVerifier($validated['name'], $validated['password']);

        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'salt' => $SRP6Data->salt,
            'verifier' => $SRP6Data->verifier,
        ]);

        event(new Registered($user));

        return response()->json(mb_convert_encoding(['status' => 1, 'msg' => 'Successfully Registered'], 'UTF-8', 'auto'));

    }
}

User Model

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        //'password',
        'salt',
        'verifier',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        //'password',
        'remember_token',
        'salt',
        'verifier',
        'session_key',
        'totp_secret'
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        //'password' => 'hashed',
    ];
}

Thank you for your attention and I will be grateful for any advice and help

0 likes
5 replies
trav1s's avatar

@krisi_gjika password is using here only for compare. I have salt/verifier columns.

I added this to the AuthController:

 $request->authenticate();

And apparently it works but I get the error: "These credentials do not match our records."

It looks like the password comparison is not working, although it should. I can’t understand why this code works perfectly on my old project in Laravel 9 but not on the new one

krisi_gjika's avatar

@trav1s I would start by debugging the validateCredentials if what you are passing is what you expect.

Also check what guard attempt is using.

Please or to participate in this conversation.