14

I am trying to slowly integrate Laravel into a legacy PHP application. One of the tasks is to automatically register a Laravel user session when a user logs in to the old app. I am not trying to implement Laravel authentication, I really just want to piggyback off of existing functionality and force a specific user to be logged in without checking credentials. What I have so far has been cobbled together from other people's hacks I have found around:

// Laravel authentication hook - Boostrap application
require_once getcwd() . '/../laravel/bootstrap/autoload.php';
$app = require_once getcwd() . '/../laravel/bootstrap/app.php';
$kernel = $app->make('Illuminate\Contracts\Console\Kernel');
$kernel->bootstrap();
$app->boot(); 

// Start Laravel session
$request = Illuminate\Http\Request::capture();
$response = $app->make('Symfony\Component\HttpFoundation\Response');
$startSession = new Illuminate\Session\Middleware\StartSession($app['session']);
// Associate server session with the authenticating user id
// I have also tried loading user model instance and then $app['auth']->login($user)
$app['auth']->loginUsingId($user_id);

$app['session']->driver()->start();
// Terminate middleware response chain with naked response
$response = $startSession->handle($request, function() use($response) {
    return $response; // This response will have session cookie attached to it
});

$response->send();

After this I get a laravel_session cookie that has contents on the client. During the login request after the code above executes, if I dd(Auth::user()) then I get the user I just logged in with. However, on subsequent requests Auth::user() and $this->request->user() both return null in all contexts.

How can I force an active Laravel user session without actually authenticating that will persist across requests?


The ultimate outcome is that Laravel will be running as a 'sub-application' underneath the legacy app while existing features are pulled in one by one so that both will exist for a period of time until all features are implemented in Laravel and it will replace the existing app in full. If it makes more sense to try to take over the legacy authentication with Laravel rather than the other way around I'm open to that, but I'd rather avoid having to change the underlying users table (legacy authentication is happening over LDAP, so there are no passwords locally, there's no remember_token, but that's easy enough to add if I have to). I really am just looking for the shortest path with the least amount of effort/headache.

Jeff Lambert
  • 24,395
  • 4
  • 69
  • 96

4 Answers4

5

This is a little bit tricky because Laravel use encrypted cookies which is handled by EncryptCookies middleware. You can get your code working if you disable it but I wouldn't recommend it.

The solution is to use Laravel EncryptCookies middleware to decrypt the request and then encrypt the response. This will make the session created by your legacy authentication readable by Laravel.

Consider this is called login.php file, which needs $user_id to log the user by id to laravel.

<?php

require_once getcwd() . '/../laravel/bootstrap/autoload.php';
$app = require_once getcwd() . '/../laravel/bootstrap/app.php';
$kernel = $app->make('Illuminate\Contracts\Console\Kernel');
$kernel->bootstrap();
$app->boot();

$request = Illuminate\Http\Request::capture();
$response = $app->make('Symfony\Component\HttpFoundation\Response');
$startSession = new Illuminate\Session\Middleware\StartSession($app['session']);
$encryptCookies = new App\Http\Middleware\EncryptCookies($app['encrypter']);

$app['session']->driver()->start();

$app['auth']->loginUsingId($user_id);

$response = $encryptCookies->handle($request, function ($request) use ($startSession, $response)
{
    return $startSession->handle($request, function () use ($response)
    {
        return $response;
    });
});

$app['session']->driver()->save();

var_dump($app['auth']->user());

$response->send();

Inside the closure of $encryptCookies->handle(), you can read the request after decryption and this is where you can modify the session. And when you return the response, it will be encrypted again and you can then send it to the browser.

To read the session in another file you can simply do this:

<?php

require_once getcwd() . '/../laravel/bootstrap/autoload.php';
$app = require_once getcwd() . '/../laravel/bootstrap/app.php';
$kernel = $app->make('Illuminate\Contracts\Console\Kernel');
$kernel->bootstrap();
$app->boot();

$request = Illuminate\Http\Request::capture();
$response = $app->make('Symfony\Component\HttpFoundation\Response');
$startSession = new Illuminate\Session\Middleware\StartSession($app['session']);
$encryptCookies = new App\Http\Middleware\EncryptCookies($app['encrypter']);

$response = $encryptCookies->handle($request, function ($request) use ($startSession, $response)
{
    return $startSession->handle($request, function () use ($response)
    {
        return $response;
    });
});

var_dump($app['auth']->user());

$response->send();
Amr El-Naggar
  • 446
  • 3
  • 6
  • This seems to work, and is much more in line with how I want to approach this, thank you. I didn't even consider encrypting the cookie! I'm going to poke it for an hour or two and if it still works I think this one is the winner. – Jeff Lambert Mar 29 '17 at 13:20
  • Nice @AmrEl-Naggar , glad to see that someone was able to figure it out – Uberswe Mar 30 '17 at 12:31
3

This won't answer your Laravel question directly, but thinking a little bit outside the box, what you are trying to achieve in principle is SSO (single sign on).

For the sake of migration only this might be an overkill solution, but if you're planning to migrate more interconnected websites or develop an app, it's worth considering.

There are three options (as usual):

If it comes to 3rd party library I can recommend Auth0 service, which can take authentication burden off you. Depending on number of users it might even come as a free solution. I'm recommending this based on my own experience with the service.

More about SSO

Auth0 SSO example

lchachurski
  • 1,770
  • 16
  • 21
  • This is a good observation, thank you. Ultimately we will be going with OAuth using laravel/passport since that is officially supported, but currently we're just looking to support a single client side web application at the moment and trying to get the project on a sound footing. – Jeff Lambert Mar 29 '17 at 13:26
2

I would suggest it's worth considering having Laravel as the main app, allowing a pass-through to the legacy code for routes that you've not yet converted. I did this with a huge legacy app that had everything you'd expect from an app written 10 years ago, and though making it all work together was a pain at the start (similar to the issues you're having, but reversed), as it got completed I was merely deleting old code until there was nothing left. With your approach, I can't see what will happen when it's time to finally retire the old code.

I ended up with:

/app
  /Console
  /Http
  etc...
  /old
    /lib
    /screens
    /config
    index.php

An OldController was setup with:

/**
 * Catches all routes for old code
 *
 * @codeCoverageIgnore
 * @return \Illuminate\Http\Response
 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
 */
public function index($all = null)
{
    ob_start();
    require __DIR__.'/../../old/index.php';
    $html = ob_get_clean();
    return response()->string($html);
}

The old/index.php had to do things like:

// TODO: In place to avoid E_DEPRECATED errors for mysql
error_reporting(E_ALL ^ E_DEPRECATED);
session_start();

// Allow all hardcoded includes in old/ to work
$path = __DIR__;
set_include_path(get_include_path() . PATH_SEPARATOR . $path);

// Set up the global user to suit old code
if (Auth::user()) {
    $myUser = Auth::user()->toArray();
    $myUser['requests'] = getRequestList($myUser['uid']);
    $loggedin = true;
    Log::debug('using Auth::user() id ' . $myUser['uid']);
} else {
    $loggedin = false;
} 
// Translate Laravel flashed data to existing message arrays
if (session()->has('login_error_array')) {
    $login_error_array = session()->get('login_error_array');
}

I did some questionable things by always using Laravel auth and a LegacyHasher for old passwords, in AuthController:

 /**
 * Login
 *
 * @return \Illuminate\Http\RedirectResponse
 */
public function postLogin()
{
    // Logic is a bit cleaner to follow if we fail late instead of early
    if ($this->hasParams(['login_email', 'login_password'])) {
        // Login using Laravel password
        $credentials = [
            'email' => strtolower(Input::get('login_email')),
            'password' => Input::get('login_password'),
            'active' => ['Y', 'B']
            ];

        // Use the extended user provider to allow using an array of values in the credentials
        $provider = new ExtendedEloquentUserProvider(new BcryptHasher(), config('auth.model'));
        Auth::setProvider($provider);
        if (Auth::attempt($credentials, Input::has('login_remember'))) {

            Log::info(sprintf('successful login from: %s', $credentials['email']));

            return redirect('/');
        }

        // Try with legacy password
        $provider = new ExtendedEloquentUserProvider(new LegacyHasher(), config('auth.model'));
        Auth::setProvider($provider);
        if (Auth::attempt($credentials, Input::has('login_remember'))) {
            // Save correctly hashed password
            // TODO: Only add this once this is definitely working ok as it messes up the DB for legacy code
            //Auth::user()->password = Hash::make(Input::get('login_password'));
            //Auth::user()->save();
            Log::info(
                sprintf(
                    'legacy password for login %s, id %d',
                    $credentials['email'],
                    Auth::user()->uid
                )
            );

            Log::info(sprintf('successful login from: %s', $credentials['email']));

            return redirect('/');
        }
    }

    Log::warning(sprintf('login failed for: %s', Input::get('login_email')));
    // TODO: Use flashed errors for legacy compatibility
    session()->flash('login_error_array', ['Wrong Username or Password']);

    return redirect('login.htm')->withInput();
}

LegacyHasher (yes, it did...)

/**
 * Check the given plain value against a hash.
 *
 * @param  string $value
 * @param  string $hashedValue
 * @param  array  $options
 * @return bool
 */
public function check($value, $hashedValue, array $options = array())
{
    // Unused param
    $options = [];
    return md5($value) == $hashedValue;
}

And a load of other stuff that is way beyond scope here. But it is doable, though painful. After a fair while working on this, all old code is removed and I've got a full testable Laravel app. It's got a few hundred TODOs scattered around liberally, but is much more manageable.

I'm happy to help with more details if you choose to go in this direction.

markdwhite
  • 2,360
  • 19
  • 24
  • Thanks for this. I considered going the route of having Laravel taking over authentication, and really at some point I'm going to have to pull that feature in anyway, so it does make sense to try to do it upfront. But with timing considerations I don't think I want to take that risk up front and would rather pull it in at a later time, just because the Legacy app is spread _very_ thin and I am worried integrating Laravel authentication in it would be a nightmare since I would potentially have to touch hundreds of files. – Jeff Lambert Mar 29 '17 at 13:23
  • 1
    I've done 4 of these for the same client now, and the last one took about 15 mins to setup a clean Laravel install with a catchall route to the OldController. I've shown some 'in progress' code above with the use of Auth::user() in old/index.php, but to start with all that was needed was to catch the output and echo it, using a Response::string() macro. But essentailly, yes - it's possibly going to be a nightmare but it can be done. http://softwareengineering.stackexchange.com/questions/6268/when-is-a-big-rewrite-the-answer – markdwhite Mar 30 '17 at 03:23
1

You are not calling save() on Session which is why your session does not persist. I modified the last few lines of your code like so:

// Laravel authentication hook - Boostrap application
require_once getcwd() . '/../laravel/bootstrap/autoload.php';
$app = require_once getcwd() . '/../laravel/bootstrap/app.php';
$kernel = $app->make('Illuminate\Contracts\Console\Kernel');
$kernel->bootstrap();
$app->boot(); 

// Start Laravel session
$request = Illuminate\Http\Request::capture();
$response = $app->make('Symfony\Component\HttpFoundation\Response');
$startSession = new Illuminate\Session\Middleware\StartSession($app['session']);
// Associate server session with the authenticating user id
// I have also tried loading user model instance and then $app['auth']->login($user)
$app['auth']->loginUsingId($user_id);

$app['session']->driver()->start();
// Terminate middleware response chain with naked response
$response = $startSession->handle($request, function() use($response) {
    return $response; // This response will have session cookie attached to it
});

 // Added the following two lines
 var_dump($app['auth']->user()); //This is for debugging
 $app['session']->driver()->save();

$response->send();

I copied this code, only removing the $app['auth']->loginUsingId($user_id); line, to a second file and ran it and the dump shows the same user still logged in. Logging out and removing the line that saves the session will stop this from working. Hopefully this is the solution that you were looking for.

Uberswe
  • 1,038
  • 2
  • 16
  • 36
  • This definitely seems on the right track, though for whatever reason I'm still not getting `Auth::user()` to return anything other than null on the second request (the first one as you have does correctly dump out the logging in user. I thought I had it on the next request also, but then forgot I had just mocked out a user so that I could move forward). I'll keep fiddling around to see if I have a mistake somewhere else, and if I can get it working the bounty is yours. Thanks! – Jeff Lambert Mar 28 '17 at 15:37
  • Hmm weird `var_dump(Auth::user());` works for me on the login page and the other page I created. I have to go now but I need to try it on a clean homestead VM to make sure it's not something I did in my project. Interesting question as I may have uses for this in the future. – Uberswe Mar 28 '17 at 16:24
  • Yes, I was a little surprised I hadn't found it asked an answered yet, I would imagine this has already been implemented by somebody somewhere. – Jeff Lambert Mar 28 '17 at 20:12