DirectoryTree / LdapRecord-Laravel

Multi-domain LDAP Authentication & Management for Laravel.
https://ldaprecord.com/docs/laravel/v3
MIT License
512 stars 54 forks source link

[Bug] Auth::user() returns null after successful login #670

Closed lucaspeeters closed 2 months ago

lucaspeeters commented 2 months ago

Environment:

Bug: After correctly connecting the user to my application and synchronizing with my database, I can't access the authenticated user. Auth::user() returns null.

auth.php

    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'ldap',
            'model' => LdapRecord\Models\ActiveDirectory\User::class,
            'rules' => [],
            'scopes' => [],
            'database' => [
                'model' => App\Models\User::class,
                'sync_passwords' => false,
                'sync_attributes' => [
                    'name' => 'cn',
                    'email' => 'mail',
                    'lang' => 'preferredlanguage'
                ],
            ],
        ],

ldap.php

    'default' => env('LDAP_CONNECTION', 'default'),

    'connections' => [
        'default' => [
            'hosts' => [env('LDAP_HOST', '127.0.0.1')],
            'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
            'password' => env('LDAP_PASSWORD', 'secret'),
            'port' => env('LDAP_PORT', 389),
            'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
            'timeout' => env('LDAP_TIMEOUT', 5),
            'use_ssl' => env('LDAP_SSL', false),
            'use_tls' => env('LDAP_TLS', false),
            'use_sasl' => env('LDAP_SASL', false),
            'sasl_options' => [
                // 'mech' => 'GSSAPI',
            ],
        ],
    ],

    'logging' => [
        'enabled' => env('LDAP_LOGGING', true),
        'channel' => env('LOG_CHANNEL', 'stack'),
        'level' => env('LOG_LEVEL', 'info'),
    ],

    'cache' => [
        'enabled' => env('LDAP_CACHE', false),
        'driver' => env('CACHE_DRIVER', 'file'),
    ],

User.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
use LdapRecord\Laravel\Auth\HasLdapUser;
use LdapRecord\Laravel\Auth\LdapAuthenticatable;
use LdapRecord\Laravel\Auth\AuthenticatesWithLdap;

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

LoginController.php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
use LdapRecord\Laravel\Auth\ListensForLdapBindFailure;

class LoginController extends Controller
{
    use AuthenticatesUsers, ListensForLdapBindFailure;

    protected $redirectTo = '/';

    public function __construct()
    {
        $this->middleware('guest')->except('logout');
        $this->middleware('auth')->only('logout');
    }

    protected function credentials(Request $request)
    {
        return [
            'mail' => $request->email,
            'password' => $request->password,
        ];
    }

    public function login(Request $request)
    {
        $this->validateLogin($request);

        if (method_exists($this, 'hasTooManyLoginAttempts') &&
            $this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }
        if ($this->attemptLogin($request)) {
            if ($request->hasSession()) {
                $request->session()->put('auth.password_confirmed_at', time());
            }

            return $this->sendLoginResponse($request);
        }

        $this->incrementLoginAttempts($request);

        return $this->sendFailedLoginResponse($request);
    }

UserController.php (Here's where the problem lies)

use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;

class UserController extends Controller
{
    public function get(): Response
    {
        dd(Auth::user());
        return response(auth()->user());
    }

I remain at your disposal should you require any further information.

Thank you

stevebauman commented 2 months ago

Hi @lucaspeeters, what session driver are you currently using?

lucaspeeters commented 2 months ago

Hey @stevebauman, I use the database for the session driver

'driver' => env('SESSION_DRIVER', 'database'),

I'm using Laravel v11 for the first time, so I've left this setting at default for now.

stevebauman commented 2 months ago

Thanks @lucaspeeters. Can you try using file and see if you encounter the same issue?

lucaspeeters commented 2 months ago

@stevebauman thank you for your help and your time. I've already tried it, but it doesn't change anything. In addition to changing 'database' to 'file' in session.php, is there anything else to do in other files?

I don't know if this helps but in LoginController when I do a dd(Auth::user()); just before return $this->sendLoginResponse($request); like below:

public function login(Request $request)
    {
        $this->validateLogin($request);

        if (method_exists($this, 'hasTooManyLoginAttempts') &&
            $this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }
        if ($this->attemptLogin($request)) {
            if ($request->hasSession()) {
                $request->session()->put('auth.password_confirmed_at', time());
            }

            dd(auth()->user());
            return $this->sendLoginResponse($request);
        }

        $this->incrementLoginAttempts($request);

        return $this->sendFailedLoginResponse($request);
    }

I receive this in return:

App\Models\User {#1314 ▼ // app/Http/Controllers/Auth/LoginController.php:88
  #connection: "mysql"
  #table: null
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: []
  #withCount: []
  +preventsLazyLoading: false
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: true
  #escapeWhenCastingToString: false
  #attributes: array:9 [▶]
  #original: array:9 [▶]
  #changes: []
  #casts: array:2 [▶]
  #classCastCache: []
  #attributeCastCache: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: []
  #touches: []
  +timestamps: true
  +usesUniqueIds: false
  #hidden: array:2 [▶]
  #visible: []
  #fillable: array:10 [▶]
  #guarded: array:1 [▶]
  #authPasswordName: "password"
  #rememberTokenName: "remember_token"
  #ldapUserModel: null
}

Everything seems fine to me, but is it normal for '#ldapUserModel' to equal null?

stevebauman commented 2 months ago

Thanks @lucaspeeters.

Everything seems fine to me, but is it normal for '#ldapUserModel' to equal null?

Yup that's fine -- that will only be set once it's been retrieved once by the ldap accessor:

https://github.com/DirectoryTree/LdapRecord-Laravel/blob/55543a4a602cde9a4f97ce3371c49678ffacbbe9/src/Auth/HasLdapUser.php#L17-L20

stevebauman commented 2 months ago

@lucaspeeters Can you confirm that you have an LDAP username and password configured in your .env as shown in the docs?

https://ldaprecord.com/docs/laravel/v3/configuration#using-a-published-configuration-file

LDAP_USERNAME="cn=user,dc=local,dc=com"
LDAP_PASSWORD=secret
lucaspeeters commented 2 months ago

@stevebauman I have all the variables below complete and I'm pretty sure of their values because it worked with the old 'adldap2/adldap2-laravel' package. PS: your package is really incredible, thank you for all your hard work.

LDAP_DEFAULT_HOSTS LDAP_DEFAULT_USERNAME LDAP_DEFAULT_PASSWORD LDAP_DEFAULT_PORT LDAP_DEFAULT_BASE_DN LDAP_DEFAULT_TIMEOUT LDAP_DEFAULT_SSL LDAP_DEFAULT_TLS LDAP_DEFAULT_SASL

image

Should it be without the “DEFAULT”? If I try the connection fails

image

stevebauman commented 2 months ago

@lucaspeeters Thanks for your kind words!

I think there may be an issue here with configuration. It appears there is a published config/ldap.php file, as well as .env credentials using dynamic connection configuration. You can only use one or the other, mentioned in the docs here:

https://ldaprecord.com/docs/laravel/v3/configuration#configuration

Screenshot 2024-09-08 at 3 48 46 PM

You can still use your .env to configure the values inside of the configuration, but if you're prefixing those .env values with a connection name (in this case, DEFAULT), then these will conflict with each other.

Try removing _DEFAULT from all your .env variables and then re-authenticate:

LDAP_HOSTS=
LDAP_USERNAME=
LDAP_PASSWORD=
LDAP_PORT=
LDAP_BASE_DN=
LDAP_TIMEOUT=
LDAP_SSL=
LDAP_TLS=
LDAP_SASL=
lucaspeeters commented 2 months ago

Hey @stevebauman,

Yes I had noticed and changed that over the weekend, I also looked a bit in the Laravel middlewares and finally found a solution. I guess both helped.

Thanks for your answers and your time!

If you have a Code wallet (from getcode), don't hesitate to send me your tip card so I can tip you.

stevebauman commented 2 months ago

Excellent! 🎉 Glad you're up and running and have resolved the issue. Happy to help @lucaspeeters.

No worries on tipping (I appreciate it though!). I'll always support bug reports regardless of financial incentive. ❤️