Adldap2 / Adldap2-Laravel

LDAP Authentication & Management for Laravel
MIT License
911 stars 184 forks source link

UserProvider fails to retrieveById #114

Closed likeadeckofcards closed 8 years ago

likeadeckofcards commented 8 years ago

I am trying to check the LDAP if a username exists and if it doesn't then I want to write the data out to a table to provide a way for users not in the ldap to set up their own account. Everytime I dump the results I get a null value.

Auth.php:

<?php

return [

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

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

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
        ],
    ],

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

        'adldap' => [
            'driver' => 'adldap',
            'model' => App\User::class,
        ],
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'email' => 'auth.emails.password',
            'table' => 'password_resets',
            'expire' => 60,
        ],
    ],
];

adldap_auth.php:

<?php

return [

    'connection' => env('ADLDAP_CONNECTION', 'default'),

    'username_attribute' => ['username' => 'samaccountname'],

    'limitation_filter' => env('ADLDAP_LIMITATION_FILTER', ''),

    'login_fallback' => env('ADLDAP_LOGIN_FALLBACK', true),

    'password_key' => env('ADLDAP_PASSWORD_KEY', 'password'),

    'password_sync' => env('ADLDAP_PASSWORD_SYNC', true),

    'login_attribute' => env('ADLDAP_LOGIN_ATTRIBUTE', 'samaccountname'),

    'bind_user_to_model' => env('ADLDAP_BIND_USER_TO_MODEL', true),

    'sync_attributes' => [
        'FirstName' => 'givenname',
        'LastName' => 'sn',
        'email' => 'mail',
    ],

    'select_attributes' => [ ],
];
stevebauman commented 8 years ago

Hi @infernobass7,

How are you checking to see if an LDAP user exists? Can you show me your code?

You could achieve this a couple ways.

The easiest would most likely involve enabling login_fallback, and then allow users to register through your application. That way it will hit your LDAP server first, and then fall back to the eloquent driver.

likeadeckofcards commented 8 years ago

Here is my test that I am performing

Route::get('test', function() {
    dd(Auth::getProvider()->retrieveById('username'));
});

'username' is my valid username.

I have a fallback database enabled.

stevebauman commented 8 years ago

This isn't the correct usage of the retrieveById() method. You need to pass in a user's primary key (the database ID), not their username.

https://github.com/laravel/framework/blob/5.2/src/Illuminate/Auth/EloquentUserProvider.php#L45

likeadeckofcards commented 8 years ago

I am not sure if this is related but this morning when I try to authenticate on my system it immediately takes me back to the login screen like I was never logged in. For some reason, all of the tokens and sessions are getting changed after login.

stevebauman commented 8 years ago

Perform a search using the Adldap facade rather than using the Auth provider.

$user = Adldap::getDefaultProvider()->search()->users()->find('jdoe');

if($user) {
    // User exists in active directory. 
}
likeadeckofcards commented 8 years ago

Thank you that works.

stevebauman commented 8 years ago

No problem!

Also for your previous question:

I am not sure if this is related but this morning when I try to authenticate on my system it immediately takes me back to the login screen like I was never logged in. For some reason, all of the tokens and sessions are getting changed after login.

If you show your authentication code I can definitely help you out.

likeadeckofcards commented 8 years ago

Here is the code behind loginning in. Basically, I check to see if the username is allowed to register because they are still in a table called 'ids'. If they are in the table then they are given the register page. Otherwise, the logic is passed to the normal login function that is provided out of the box. The table is filled by a call to an internal database that is hosted separately.

public function postLogin(Request $request) {
        if(DB::table('ids')->where('username', '=', $request->username)
                ->where('EmpID','=',$request->password)->count() == 1) {
            return redirect("register")->withInput([
                'username'=>$request->only($this->loginUsername())['username'],
                'EmpID'=>$request->only('password')['password']
            ]);
        }
        return $this->login($request);
    }

I know that the user is logged in because if I dump right before handleUserWasAuthenticated function like below, I get the user model that was authenticated and Auth::guest() returns false. However, when the page is redirected to their original destination they are redirected back to the login form because they are not logged in.

if (Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'))) {
            dd(Auth::user());
            return $this->handleUserWasAuthenticated($request, $throttles);
        }
likeadeckofcards commented 8 years ago

@stevebauman Have you had any success figuring out why it would be causing this issue?

likeadeckofcards commented 8 years ago

@stevebauman Just so you know. I opened a thread at laracasts because if I disable LDAP authentication it still does not authenticate correctly. Here is the link: https://laracasts.com/discuss/channels/laravel/authentication-succeeds-but-acts-like-it-fails

stevebauman commented 8 years ago

Hey @infernobass7, apologies for the long response on this.

Since the user is successfully logging in as you've discovered, maybe try a different session driver in your config/session.php file?

For example, try the database session driver and see if the session exists in the database table.

stevebauman commented 8 years ago

Also just for good measure, remove Adldap2 from the picture entirely and try logging in with the default laravel eloquent auth driver. If you're able to authenticate fine with that and the session persists, we can troubleshoot with Adldap2 from there.

likeadeckofcards commented 8 years ago

I just removed adldap and checked both the database and file. Before the login is submitted there is not a session and then after it is created and persist but somehow the session is changing from when it is submitted to when the user is redirected to the home page after logging in

Process shown below: Login form is displayed no Session Login form is submitted Session ID in the authenticate middleware. Login form is redisplayed and no session

[2016-06-02 15:23:07] local.INFO: Login Form Session:   
[2016-06-02 15:23:13] local.INFO: Login Submission Session: 5b1066bb8119cf3aed3cf181697254389be75b95  
[2016-06-02 15:23:14] local.INFO: Auth Session: a4432bc0d64e62dcde10023d8863932b873f3865  
[2016-06-02 15:23:14] local.INFO: Login Form Session:   
stevebauman commented 8 years ago

This is strange. I'm not really sure why the session would be changing.

It sounds like something may be logging you out. Can you post your entire AuthController code?

likeadeckofcards commented 8 years ago

Here is the AuthController:

<?php

namespace App\Http\Controllers\Auth;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

class AuthController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Registration & Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users, as well as the
    | authentication of existing users. By default, this controller uses
    | a simple trait to add these behaviors. Why don't you explore it?
    |
    */

    use AuthenticatesAndRegistersUsers, ThrottlesLogins;

    /**
     * Where to redirect users after login / registration.
     *
     * @var string
     */
    protected $redirectTo = '/';

    protected $username = 'username';

    /**
     * Create a new authentication controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware($this->guestMiddleware(), ['except' => 'logout']);
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'FirstName' => 'required|max:255',
            'LastName' => 'required|max:255',
            'email' => 'required|unique:users',
            'EmpID' => 'required|digits:6|exists:ids,EmpID',
            'username' => 'required|max:50|unique:users',
            'password' => 'required|confirmed|min:8|letters|numbers|case_diff|symbols',
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return User
     */
    protected function create(array $data)
    {
        return User::create([
            'FirstName' => $data['FirstName'],
            'LastName'  => $data['LastName'],
            'email'     => $data['email'],
            'EmpID'     => $data['EmpID'],
            'username'  => $data['username'],
            'password'  => bcrypt($data['password']),
            'groups'    => "intranet-user",
        ]);
    }

    public function postLogin(Request $request) {
        Log::info('Login Submission Session: ' . $request->session()->getId());
        if(DB::table('ids')->where('username', '=', $request->username)
                ->where('EmpID','=',$request->password)->count()==1) {
            return redirect("register")->withInput([
                'username'=>$request->only($this->loginUsername())['username'],
                'EmpID'=>$request->only('password')['password']
            ]);
        }

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

Here is the AuthenticatesUsers Trait:

<?php

namespace Illuminate\Foundation\Auth;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Log;

trait AuthenticatesUsers
{
    use RedirectsUsers;

    /**
     * Show the application login form.
     *
     * @return \Illuminate\Http\Response
     */
    public function getLogin()
    {
        return $this->showLoginForm();
    }

    /**
     * Show the application login form.
     *
     * @return \Illuminate\Http\Response
     */
    public function showLoginForm()
    {
        Log::info('Login Form Session: ' . Request::capture()->getSession());
        $view = property_exists($this, 'loginView')
                    ? $this->loginView : 'auth.authenticate';

        if (view()->exists($view)) {
            return view($view);
        }

        return view('auth.login');
    }

    /**
     * Handle a login request to the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function postLogin(Request $request)
    {
        return $this->login($request);
    }

    /**
     * Handle a login request to the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function login(Request $request)
    {
        $this->validateLogin($request);

        // If the class is using the ThrottlesLogins trait, we can automatically throttle
        // the login attempts for this application. We'll key this by the username and
        // the IP address of the client making these requests into this application.
        $throttles = $this->isUsingThrottlesLoginsTrait();

        if ($throttles && $lockedOut = $this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

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

        $credentials = $this->getCredentials($request);

        if (Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'))) {
            return $this->handleUserWasAuthenticated($request, $throttles);
        }

        // If the login attempt was unsuccessful we will increment the number of attempts
        // to login and redirect the user back to the login form. Of course, when this
        // user surpasses their maximum number of attempts they will get locked out.
        if ($throttles && ! $lockedOut) {
            $this->incrementLoginAttempts($request);
        }

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

    /**
     * Validate the user login request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     */
    protected function validateLogin(Request $request)
    {
        $this->validate($request, [
            $this->loginUsername() => 'required', 'password' => 'required',
        ]);
    }

    /**
     * Send the response after the user was authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  bool  $throttles
     * @return \Illuminate\Http\Response
     */
    protected function handleUserWasAuthenticated(Request $request, $throttles)
    {
        if ($throttles) {
            $this->clearLoginAttempts($request);
        }

        if (method_exists($this, 'authenticated')) {
            return $this->authenticated($request, Auth::guard($this->getGuard())->user());
        }

        return redirect()->intended($this->redirectPath());
    }

    /**
     * Get the failed login response instance.
     *
     * @param \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendFailedLoginResponse(Request $request)
    {
        return redirect()->back()
            ->withInput($request->only($this->loginUsername(), 'remember'))
            ->withErrors([
                $this->loginUsername() => $this->getFailedLoginMessage(),
            ]);
    }

    /**
     * Get the failed login message.
     *
     * @return string
     */
    protected function getFailedLoginMessage()
    {
        return Lang::has('auth.failed')
                ? Lang::get('auth.failed')
                : 'These credentials do not match our records.';
    }

    /**
     * Get the needed authorization credentials from the request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    protected function getCredentials(Request $request)
    {
        return $request->only($this->loginUsername(), 'password');
    }

    /**
     * Log the user out of the application.
     *
     * @return \Illuminate\Http\Response
     */
    public function getLogout()
    {
        return $this->logout();
    }

    /**
     * Log the user out of the application.
     *
     * @return \Illuminate\Http\Response
     */
    public function logout()
    {
        Auth::guard($this->getGuard())->logout();

        return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
    }

    /**
     * Get the guest middleware for the application.
     */
    public function guestMiddleware()
    {
        $guard = $this->getGuard();

        return $guard ? 'guest:'.$guard : 'guest';
    }

    /**
     * Get the login username to be used by the controller.
     *
     * @return string
     */
    public function loginUsername()
    {
        return property_exists($this, 'username') ? $this->username : 'email';
    }

    /**
     * Determine if the class is using the ThrottlesLogins trait.
     *
     * @return bool
     */
    protected function isUsingThrottlesLoginsTrait()
    {
        return in_array(
            ThrottlesLogins::class, class_uses_recursive(static::class)
        );
    }

    /**
     * Get the guard to be used during authentication.
     *
     * @return string|null
     */
    protected function getGuard()
    {
        return property_exists($this, 'guard') ? $this->guard : null;
    }
}
stevebauman commented 8 years ago

Hmmm everything seems fine with your controller.

I would try creating a clean laravel project (laravel new blog) and then try re-scaffolding auth (php artisan make:auth), and try authenticating. Then we can determine if it's something on your machine, or the project itself.

likeadeckofcards commented 8 years ago

I have another project running on this machine as well that does not have any issues staying logged in. It uses the base with ldap-connector but it started on laravel 5.2

stevebauman commented 8 years ago

Ok great, what's the version of laravel you're running on for this current application?

likeadeckofcards commented 8 years ago

It was on 5.1 and I working on updating it to 5.2.

stevebauman commented 8 years ago

Can you show me your app/Http/Kernel.php file?

likeadeckofcards commented 8 years ago

Here it is:

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'public' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
        ],

        'intranet' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \App\Http\Middleware\Authenticate::class,
        ],

        'scanners' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\RedirectIfNotFromAScanner::class,
            \App\Http\Middleware\ScannerAuthenticate::class,
        ],

        'api' => [
            'throttle:60,1',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'can' => \Illuminate\Foundation\Http\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
        'supervisor' => \App\Http\Middleware\RedirectIfNotASupervisor::class,
        'admin' => \App\Http\Middleware\RedirectIfNotAnAdmin::class,
        'scanner' => \App\Http\Middleware\RedirectIfNotFromAScanner::class,
        'scanner-auth' => \App\Http\Middleware\ScannerAuthenticate::class,
    ];
}
stevebauman commented 8 years ago

Just as a test, can you throw the \Illuminate\Session\Middleware\StartSession::class inside the global protected $middleware = [] stack and try authenticating?

likeadeckofcards commented 8 years ago

Nope. It stills has issues.

stevebauman commented 8 years ago

Hmm... How about your app/Http/Middleware/Authenticate::class? Can you post the code here?

likeadeckofcards commented 8 years ago

Here it is:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;

class Authenticate
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        Log::info('Auth Session: ' . $request->session()->getId());
        if (Auth::guard($guard)->guest()) {
            if ($request->ajax() || $request->wantsJson()) {
                return response('Unauthorized.', 401);
            } else {
                return redirect()->guest('login');
            }
        }

        return $next($request);
    }
}
stevebauman commented 8 years ago

Hmm I'm really stumped. This seems like a laravel issue somewhere.

I could start a remote session sometime and help you try and debug this if you'd like, it would just make it a lot easier instead of having to post all of your code here.

likeadeckofcards commented 8 years ago

Yeah. Let me try something real quick. I am trying to transfer code to a new install without copying files in case there is something small that I missed.

likeadeckofcards commented 8 years ago

Thank you @stevebauman for all of your help. It looks like my issue was stemming from some code that got carried over from laravel 5.1. I just transferred all the code into a fresh install and it seems to be working now.

stevebauman commented 8 years ago

Great! Really glad you've solved the issue.

Thanks for following up.