sfelix-martins / passport-multiauth

Add support to multi-authentication to Laravel Passport
MIT License
288 stars 51 forks source link

Use passport-multiauth with CreateFreshApiToken #55

Open jflatscher opened 6 years ago

jflatscher commented 6 years ago

Hi! I would like to use passport-multiauth with CreateFreshApiToken class, so I can use my api with my javascript application. 'web' => [ ... \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class ], When I replace the basic auth class with the multiauth class in $routeMiddleware in Kernel.php, I get 401 Unauthorized error

//'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 'auth' => \SMartins\PassportMultiauth\Http\Middleware\MultiAuthenticate::class,

Is there any possibility to make it work with multiauth?

sfelix-martins commented 5 years ago

@jflatscher Sorry for the delay. Have you found a solution to your problem? Please, let me know.

stijnkamp commented 5 years ago

How did you solve the issue? I am still struggling to get this working.

gudorian commented 5 years ago

I'm facing the same issue and have been doing some digging in why this isn't working as well ways to solve this.

Also note, though I'm used to work with Passport from multiple projects, I understand it at an implementation level but I've just dived into how the package was written. Same goes for MultiAuth with the exception that I only use it in one project where also I was not the one to implement it. So I would appreciate any feedback regarding proposed solution below.

Why isn't it working

First, MultiAuth doesn't look for the TokenCookie that's created when the middleware Laravel\Passport\Http\Middleware\CreateFreshApiToken is used. With default Passport configuration, a cookie (laravel_token) will be added to the response normally in one of your web-routes. That cookie will be sent in XHR request which contains an PassportToken instead of the Authorization: Bearer [token] header.

So problem seems to start in SMartins\PassportMultiauth\Http\Middleware\MultiAuthenticate:75

$psrRequest = $this->server->validateAuthenticatedRequest($psrRequest);

What will happen is that since the Authorization header is missing, a AuthenticationException is thrown and we get a 401 response. So the middleware is never looking for the CookieToken.

Looking at how Passport is resolving the user in Laravel\Passport\Guards\TokenGuard:

/**
 * Get the user for the incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return mixed
 */
public function user(Request $request)
{
    if ($request->bearerToken()) {
        return $this->authenticateViaBearerToken($request);
    } elseif ($request->cookie(Passport::cookie())) {
        return $this->authenticateViaCookie($request);
    }
}

We find a hint on where to start with the solution.

Possible solution

In SMartins\PassportMultiauth\Http\Middleware\MultiAuthenticate we need to add a check for which token type is used.

try {
    if ($request->bearerToken()) {
        $psrRequest = $this->server->validateAuthenticatedRequest($psrRequest);
        if (! ($accessToken = $this->getAccessTokenFromRequest($psrRequest))) {
            throw new AuthenticationException('Unauthenticated', $guards);
        }
        $guard = $this->getTokenGuard($accessToken, $guards);
        $this->authenticate($request, $guard);
    } elseif ($request->cookie(Passport::cookie())) {
        // Validate -> Authenticate CookieToken
    }
} catch (OAuthServerException $e) {

I've not solved how to solve this correctly and securely validate/authenticate the CookieToken yet, any hints/suggestions would be appreciated.

Next, since the CookieToken isn't stored in the oauth_access_tokens table, we can't resolve the provider in the same way as MultiAuth does with a BearerToken.

Currently Laravel\Passport\Http\Middleware\CreateFreshApiToken functionality only allows for two arguments that's saved with the CookieToken, Authorizable Id and token. If I've understood correctly, we would need to also add provider and store it with the CookieToken to be able to resolve the correct Authorizable type.

The cookie is created when a user is authenticated, so instead of changing the Passport middleware MultiAuth can provide it's own, ie. SMartins\PassportMultiauth\Http\CreateFreshMultiAuthApiToken, which should be included instead of the one provided by Passport.

Also the CookieFactory class used by Passport to create the CookieToken only allows for two arguments as well so MultiAuth will need to provide it's own version of that as well. Not sure if it's a reliable way to get the provider, but something similar to this might work:

<?php

namespace SMartins\PassportMultiauth\Http\Middleware;

use Closure;
use Illuminate\Http\Response;
use Laravel\Passport\Http\Middleware\CreateFreshApiToken;
use Laravel\Passport\Passport;
use Illuminate\Http\JsonResponse;
use SMartins\PassportMultiauth\Factories\MultiAuthApiTokenCookieFactory as ApiTokenCookieFactory;
use SMartins\PassportMultiauth\Config\AuthConfigHelper;

class CreateFreshMultiAuthApiToken extends CreateFreshApiToken
{
    /**
     * Create a new middleware instance.
     *
     * @param  SMartins\PassportMultiauth\Factories\ApiTokenCookieFactory  $cookieFactory
     * @return void
     */
    public function __construct(ApiTokenCookieFactory $cookieFactory)
    {
        $this->cookieFactory = $cookieFactory;
    }

    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure                 $next
     * @param string|null              $guard
     *
     * @return mixed
     * @throws \SMartins\PassportMultiauth\Exceptions\MissingConfigException
     */
    public function handle($request, Closure $next, $guard = null)
    {
        $this->guard = $guard;

        $response = $next($request);
        if ($this->shouldReceiveFreshToken($request, $response)) {
            $provider = AuthConfigHelper::getUserProvider($request->user($this->guard));

            $response->withCookie($this->cookieFactory->make(
                $request->user($this->guard)->getKey(), $request->session()->token(), $provider
            ));
        }

        return $response;
    }
}
<?php

namespace SMartins\PassportMultiauth\Factories;

use Carbon\Carbon;
use Firebase\JWT\JWT;
use Laravel\Passport\Passport;
use Symfony\Component\HttpFoundation\Cookie;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Config\Repository as Config;

class MultiAuthApiTokenCookieFactory
{
    /**
     * The configuration repository implementation.
     *
     * @var \Illuminate\Contracts\Config\Repository
     */
    protected $config;

    /**
     * The encrypter implementation.
     *
     * @var \Illuminate\Contracts\Encryption\Encrypter
     */
    protected $encrypter;

    /**
     * Create an API token cookie factory instance.
     *
     * @param  \Illuminate\Contracts\Config\Repository  $config
     * @param  \Illuminate\Contracts\Encryption\Encrypter  $encrypter
     * @return void
     */
    public function __construct(Config $config, Encrypter $encrypter)
    {
        $this->config = $config;
        $this->encrypter = $encrypter;
    }

    /**
     * Create a new API token cookie.
     *
     * @param mixed  $userId
     * @param string $csrfToken
     * @param        $provider
     * @return \Symfony\Component\HttpFoundation\Cookie
     */
    public function make($userId, $csrfToken, $provider)
    {
        $config = $this->config->get('session');

        $expiration = Carbon::now()->addMinutes($config['lifetime']);

        return new Cookie(
            Passport::cookie(),
            $this->createToken($userId, $csrfToken, $provider, $expiration),
            $expiration,
            $config['path'],
            $config['domain'],
            $config['secure'],
            true,
            false,
            $config['same_site'] ?? null
        );
    }

    /**
     * Create a new JWT token for the given user ID and CSRF token.
     *
     * @param mixed          $userId
     * @param string         $csrfToken
     * @param                $provider
     * @param \Carbon\Carbon $expiration
     * @return string
     */
    protected function createToken($userId, $csrfToken, $provider, Carbon $expiration)
    {
        return JWT::encode([
            'sub' => $userId,
            'csrf' => $csrfToken,
            'provider' => $provider,
            'expiry' => $expiration->getTimestamp(),
        ], $this->encrypter->getKey());
    }
}

Then it should just be the validation/authentication of the CookieToken left.

@sfelix-martins would appreciate any feedback you have about this or someone else that's interested in getting this solved.