DirectoryTree / LdapRecord-Laravel

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

[Support] Login via REST API failed until the user authenticates at least 1 time from the web application #652

Closed FrancescoD3V closed 2 months ago

FrancescoD3V commented 2 months ago

Hi guys, once again I need your amazing support! I have my own laravel application which provides a web interface where you can authenticate with your domain credentials and access your target area.

My application also exposes Rest APIs that allow Android devices to authenticate with the same domain credentials entered from the web.

Everything works great, but I'm having a synchronization issue related to passwords. Let me explain better: My application launches the "php artisan ldap:import --no-interaction" command when the docker container is started to synchronize all the users of the domain. I remember reading in the documentation that when the users are inserted for the first time in the database, the password field is generated with a "fake" password, waiting for the user to log in from the web interface and be able to individually synchronize their password in the database with the ldap server.

What actually happens is what has been described, the password changes when the user logs in from the web.

This synchronization event can only happen if the user uses the web interface since in the Laravel configuration, the session driver uses the ldap provider.

If a user logs in for the first time via Rest API, Laravel will not be able to synchronize the password with the Activedirectory server because in my configuration the APIs use another provider that I created.

I'll share my configuration with you:

//// auth.php

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

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

    'ldap' => [
        'driver' => 'ldap',
        'model' => LdapRecord\Models\ActiveDirectory\User::class,
        'database' => [
            'model' => App\Models\User::class,
            'sync_passwords' => true,
            'sync_attributes' => [
                'name' => 'cn',
                'email' => 'mail',
                'displayName' => 'displayname',
                'description' => 'description',
            ],
            'sync_existing' => [
                'email' => 'mail',
            ],
        ],
    ],
],

//// auth.php

I would like to find a simple way to make my driver can use ldaprecord to authenticate itself or if it is not possible to find an alternative solution.

At the moment the only way is to ask the user to log in at least the first time via the web interface

I share the authentication code via API Rest

//// myAuthController.php public function login(Request $request) { $request->validate([ 'email' => 'required|string', 'password' => 'required|string', ]);

    $credentials = [
        'email' => $request->get('email'),
        'password' => $request->get('password')
    ]; 

    $token = Auth::guard('api')->attempt($credentials);
    if (!$token) {
        return response()->json([
            'status' => 'error',
            'message' => 'Unauthorized',
        ], 401);
    }

    $user = Auth::guard('api')->user();
    return response()->json([
        'status' => 'success',
        'authorisation' => [
            'token' => $token,
            'type' => 'bearer',
        ]
    ]);
} 

//// myAuthController.php

Environment:

stevebauman commented 2 months ago

Hey @FrancescoD3V,

I remember reading in the documentation that when the users are inserted for the first time in the database, the password field is generated with a "fake" password,

Yea this is correct -- LDAP servers do not return plain text passwords from searches, so it's impossible to store their real password during this process. The only way you can access the user's plain text password to store it in the local database is when they give it to you with an authentication attempt.

To do what you're looking for, you'll probably have to perform the same logical flow as the typical session based guard, but without accessing the session, since your api route group would not have the session started/booted.

I believe you should be able to do this by creating an LdapRecord DatabaseUserProvider instance manually. Something like this should get you started:

// Remember to create a credentials array meant to search your LDAP attribute (mail, userPrincipalName, etc.)
$credentials = [
    'mail' => $request->get('email'),
    'password' => $request->get('password'),
];

/** @var \LdapRecord\Laravel\Auth\DatabaseUserProvider $provider */
$provider = Auth::createUserProvider('ldap');

// Unsaved Eloquent user will be returned, or null if the LDAP user doesn't exist.
/** @var \App\Models\User */
$user = $provider->retrieveByCredentials($credentials);

// This will save the Eloquent user model (and its password) if the credentials are valid
if (! $provider->validateCredentials($user, $credentials)) {
    return 'error';
}

$token = Auth::guard('api')->attempt([
    'email' => $request->get('email'),
    'password' => $request->get('password'),
]);

// ...
FrancescoD3V commented 2 months ago

Hi Steve, your code works! I can update the hashed password if the credentials are entered.

I add a check to the code to verify that $user is not empty because if you try to pass a user that does not exist in the eloquent database, I get an error like this:

TypeError: LdapRecord\Laravel\Auth\DatabaseUserProvider::validateCredentials(): Argument #1 ($user) must be of type Illuminate\Contracts\Auth\Authenticatable, null given, called in /var/www/app/Http/Controllers/API/Auth/AuthController.php on line 77 in file /var/www/vendor/directorytree/ldaprecord-laravel/src/Auth/DatabaseUserProvider.php on line 174

Result:

$credentials = [
    'mail' => $request->get('email'),
    'password' => $request->get('password'),
];

/** @var \LdapRecord\Laravel\Auth\DatabaseUserProvider $provider */
$provider = Auth::createUserProvider('ldap');

// Unsaved Eloquent user will be returned, or null if the LDAP user doesn't exist.
/** @var \App\Models\User */
$user = $provider->retrieveByCredentials($credentials);

// This will save the Eloquent user model (and its password) if the credentials are valid
if ($user == null || !$provider->validateCredentials($user, $credentials_with_name_users)) {
    return 'error';
}

$token = Auth::guard('api')->attempt([
    'email' => $request->get('email'),
    'password' => $request->get('password'),
]);
stevebauman commented 2 months ago

That's great @FrancescoD3V! I'm glad you're up and running and were able to resolve the issue.

Also, thanks for posting the detailed reply, as others may find this useful in the future 🙏