DirectoryTree / LdapRecord

A fully-featured LDAP framework.
https://ldaprecord.com
MIT License
507 stars 45 forks source link

[LdapRecord-Laravel] Authentication occurs twice using Laravel Fortify #219

Closed fcno closed 3 years ago

fcno commented 3 years ago

Using LdapRecord - 1.7.3 Fortify with custom UI 1.6 Laravel 8

Hello. I don't know if this would be the normal procedure, but it seems to me, from the log bellow, that the authentication routine is running twice. It would be normal or there is probably something wrong with my application, because if that's the case, I've tried to investigate here and I couldn't find where the problem could be. Any tips?


[2020-10-19 13:59:38] local.INFO: User [xxx] has been successfully located for authentication.  
[2020-10-19 13:59:38] local.INFO: Object with name [xxx] is being synchronized.  
[2020-10-19 13:59:38] local.INFO: Object with name [xxx] has been successfully synchronized.  
[2020-10-19 13:59:38] local.INFO: User [xxx] is authenticating.  
[2020-10-19 13:59:38] local.INFO: User [xxx] has successfully passed LDAP authentication.  
[2020-10-19 13:59:38] local.INFO: User [xxx] has been successfully located for authentication.  
[2020-10-19 13:59:38] local.INFO: Object with name [xxx] is being synchronized.  
[2020-10-19 13:59:38] local.INFO: Object with name [xxx] has been successfully synchronized.  
[2020-10-19 13:59:38] local.INFO: User [xxx] is authenticating.  
[2020-10-19 13:59:38] local.INFO: User [xxx] has successfully passed LDAP authentication.  
stevebauman commented 3 years ago

Hi @Babiute,

How are you authenticating with Fortify? Can you post your usage? For example:

Fortify::authenticateUsing('...');
fcno commented 3 years ago

Hi @stevebauman .

        // app/providers/AuthServiceProvider

        Fortify::authenticateUsing(function ($request) {
            // Determine the guard name by the submitted domain.
            $guard = $request->domain == 'alpha' ? 'alpha' : 'bravo';

            //Set default guard
            config(['fortify.guard' => $guard]);
            Auth::shouldUse($guard);

            $validated = Auth::validate([
                'samaccountname' => $request->input('username'),
                'password' => $request->input('password')
            ]);

            return $validate ? Auth::getLastAttempted() : null;
        });
stevebauman commented 3 years ago

Ok, can you share your login view so I can understand how you're sending the post request?

stevebauman commented 3 years ago

Also, do you have two-factor authentication enabled inside of your config/fortify.php file?

fcno commented 3 years ago

About the routes, I do not have my own login/logout routes. Im using the Login and Logout routes provided by Fortify default installation.

@include('layout.partial.top')

<nav class="navbar navbar-dark bg-secondary">
  <span class="navbar-brand">{{ config('app.name') }}</span>
</nav>

<section class="container">
  <div class="row justify-content-center">
    <div class="col-12 col-lg-6 my-3">

      <img class="mx-auto my-3 d-block" id="logo" src="{{ asset('imagem/logo.svg') }}" alt="logo">

      <form>
      @csrf

      {{--Input usuário--}}
        <input 
            class="form-control my-3 @error('username') is-invalid @enderror"
            type="text"
            name="username"
            id="username"
            placeholder="Usuário de rede"
            autocomplete="off"
            title="Informar o seu usuário de rede"
            required
            autofocus
            value="{{ old('username') }}">

        {{--Input senha--}}
        <input 
            class="form-control my-3 @error('password') is-invalid @enderror"
            type="password"
            name="password"
            id="password"
            placeholder="Senha de rede"
            autocomplete="off"
            title="Informar a sua senha de rede"
            required>

        {{--Select órgão--}}
        <select
            class="form-control my-3 @error('domain') is-invalid @enderror"
            name="domain"
            id="domain"
            required>
            <option value="">Escolha o órgão</option>
            @foreach (config('orgao.nome_completo') as $orgao => $nome_completo)
            <option {{ old('domain') == $orgao ? 'selected' : '' }} value="{{ $orgao }}" >{{ $nome_completo }}</option>
            @endforeach
        </select>

        {{--Botão para entrar na aplicação--}}
        <button
            class="btn btn-primary btn-block my-3"
            type="submit"
            formmethod="POST"
            formaction="{{ route('login') }}"
            name="button-autenticar"
            title="Acessar o sistema">
              Entrar
        </button>
      </form>

      @include('layout.partial.errors')

    </div>
  </div>
</section>

@include('layout.partial.footer')
fcno commented 3 years ago

Also, do you have two-factor authentication enabled inside of your config/fortify.php file?

No. Every default feature is disabled.

stevebauman commented 3 years ago

I do not have my own login/logout routes. Im using the Login and Logout routes provided by Fortify default installation.

Yup absolutely -- I did not ask for the routes.

Can you post your App\Models\User.php?

fcno commented 3 years ago

About the routes, I was just trying to antecipate a possible question 👍

As I'm using multi-domain authentication, Im not using the default model. Im using the 'alpha' user.

<?php

namespace App\Ldap\Alpha;

use LdapRecord\Models\ActiveDirectory\User as LdapUser;

class User extends LdapUser
{
    protected $connection = 'alpha';
}
stevebauman commented 3 years ago

No worries!

Aren't you using database synchronization? That's what the logs indicate.

I'm looking for your applications Eloquent User model -- not the LDAP model. 👍

Your Eloquent user model would be configured inside of each guard inside of your config/auth.php file. For example:

// config/auth.php

'providers' => [
    // ...

    'ldap' => [
        'driver' => 'ldap',
        'model' => LdapRecord\Models\ActiveDirectory\User::class,
        'database' => [
            'model' => App\Models\User::class, // <-- This model here.
            '...',
        ],
    ],
],
fcno commented 3 years ago

Yes I got it. It was an error to post the synchronization log. I enabled synchronization today (step by step), so I can learn how to properly use each feature in your library But this double authentication has been going on since before. The correct log would be this, from the branch without synchronization that I think is not what causes the problem.

[2020-10-19 15:22:03] local.INFO: User [xxx] has been successfully located for authentication.  
[2020-10-19 15:22:03] local.INFO: User [xxx] is authenticating.  
[2020-10-19 15:22:03] local.INFO: User [xxx] has successfully passed LDAP authentication.  
[2020-10-19 15:22:03] local.INFO: User [xxx] has been successfully located for authentication.  
[2020-10-19 15:22:03] local.INFO: User [xxx] is authenticating.  
[2020-10-19 15:22:03] local.INFO: User [xxx] has successfully passed LDAP authentication.  

Either way follows the model, but it doesn't even exist in the plain authentication branch where the double authentication is happening too.

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 LdapRecord\Laravel\Auth\LdapAuthenticatable;
use LdapRecord\Laravel\Auth\AuthenticatesWithLdap;

class User extends Authenticatable implements LdapAuthenticatable
{
    use HasFactory, Notifiable, AuthenticatesWithLdap;

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

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}
stevebauman commented 3 years ago

I think I've discovered the issue. In Laravel Fortify, by default, it will always attempt the $authenticateUsingCallback twice if you do not override the $authenticateThroughCallback inside of the loginPipeline method shown here:

https://github.com/laravel/fortify/blob/c77b1832eed4223fa9e8fe1a5afc795c00c126a5/src/Http/Controllers/AuthenticatedSessionController.php#L69-L89

    protected function loginPipeline(LoginRequest $request)
    {
        if (Fortify::$authenticateThroughCallback) {
            return (new Pipeline(app()))->send($request)->through(array_filter(
                call_user_func(Fortify::$authenticateThroughCallback, $request)
            ));
        }

        if (is_array(config('fortify.pipelines.login'))) {
            return (new Pipeline(app()))->send($request)->through(array_filter(
                config('fortify.pipelines.login')
            ));
        }

        return (new Pipeline(app()))->send($request)->through(array_filter([
            config('fortify.limiters.login') ? null : EnsureLoginIsNotThrottled::class,
            RedirectIfTwoFactorAuthenticatable::class, // <-- `$authenticateUsingCallback` called here
            AttemptToAuthenticate::class, // <-- `$authenticateUsingCallback` called here
            PrepareAuthenticatedSession::class,
        ]));
    }

First, the RedirectIfTwoFactorAuthenticatable action is used, which calls the $authenticateUsingCallback (regardless if you have the feature disabled):

https://github.com/laravel/fortify/blob/c77b1832eed4223fa9e8fe1a5afc795c00c126a5/src/Http/Controllers/AuthenticatedSessionController.php#L85

https://github.com/laravel/fortify/blob/c77b1832eed4223fa9e8fe1a5afc795c00c126a5/src/Actions/RedirectIfTwoFactorAuthenticatable.php#L68-L74

The $authenticateUsingCallback is called a second time inside of the AttemptToAuthenticate action:

https://github.com/laravel/fortify/blob/c77b1832eed4223fa9e8fe1a5afc795c00c126a5/src/Actions/AttemptToAuthenticate.php#L48-L50

To work around this, you either have to define your own login pipeline inside of your config/fortify.php file, or define an $authenticateThroughCallback using Fortify::authenticateThrough().

fcno commented 3 years ago

Hey Steve. Thx for the quick replay. As I'm still in the learning curve of Laravel, I do not know how to properly do it. But thx for you help and I hope it helps who face the same problem :)

stevebauman commented 3 years ago

Hi @Babiute, last night I pushed a PR into Laravel Fortify that resolves this issue and it was merged this morning 🎉 :

https://github.com/laravel/fortify/pull/127#event-3898653390

Once a new release of Laravel Fortify is created, it should contain this fix 👍