c.schmidl's avatar

Internal usage of Passport for own API

Hey there,

I'm currently trying to implement a RESTful API which is later on used by different clients (web, android, ios). I'm using Laravel 5.3, the Dingo API Package and Laravel Passport. I configured Passport so far that it is working like in the video given by Taylor Otwell (https://laracasts.com/series/whats-new-in-laravel-5-3/episodes/13).

I know that Passport offers the following default routes by putting Passport::routes(); into the boot()-method of App\Providers\AuthServiceProvider:

Passport Routes

This works just fine but I don't need the whole OAuth 2 stack provided by passport with all its provided routes, I just want to use it for authentication at the API-Endpoints and therefore using Password Grants. Password Grants work just fine with the provided default routes.

What I want to do know:

  • Use my own AuthenticationController who is using Passport in the background and publishes its own routes. Passport's default routes shouldn't be accessible from the public. What's the proper way to do this? Invoking the default routes given by Passport inside my controller which are somehow protected from the public? Or using the actual Passport classes and invoking the corresponding methods and figure out by myself how everything works together?

My AuthController looks like this:

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use GuzzleHttp\Psr7\ServerRequest;
use Illuminate\Http\Request;

use App\Http\Requests;
use Illuminate\Support\Facades\Auth;
use Laravel\Passport\Http\Controllers\AccessTokenController;
use Laravel\Passport\Passport;

class AuthController extends ApiController
{

    protected $accessTokenController;

    /**
     * AuthController constructor.
     * @param AccessTokenController $accessTokenController
     */
    public function __construct(AccessTokenController $accessTokenController)
    {
        $this->accessTokenController = $accessTokenController;
    }


    /**
     * Authenticate the user based on his/her access token.
     *
     * @param Request $request
     * @return \Laravel\Passport\Http\Controllers\Response
     */
    public function getAuthenticatedUser(Request $request)
    {
        return Auth::guard('api')->authenticate($request);
    
    // I tried to figure out what's the proper way to use passport
    // just for internal usage. But I'm pretty sure that
    // this is not the way to go!
        //$serverRequest = ServerRequest::fromGlobals();

        //return $this->accessTokenController->issueToken($serverRequest);
    }

    /**
     * Takes the input from the POST Request and stores a new User
     * into the database with the field 'activated' to false.
     * After successfully doing the signup procedure, the user
     * should be emailed with a confirmation link that he has to
     * visit to confirm his/her identity and to use the full
     * Application/Api.
     *
     * I don't know if the Response should already contain an access token.
     * Maybe it's an good idea to respond with an access token which has
     * a limited scope as long as the email adress hasn't been confirmed.
     */
    public function signup()
    {

    }


    /**
     * A simple POST Request which logsout the user and invalidates
     * the current used access token. And also deletes the
     * access token?
     *
     */
    public function logout()
    {

    }


    /**
     * A POST Request with a generated identifier which has been
     * sent to the user to confirm his/her identity.
     *
     */
    public function confirmSignup()
    {

    }


    /**
     * When the user forgot his/her password,
     * the user can use this endpoint to request
     * the recovery procedure. An email gets send
     * with code/link included to reset his/her password.
     * The code/link is only valid for a limited time period.
     */
    public function recovery()
    {

    }

    /**
     * After requesting the recovery, the user
     * can use this endpoint to provide his/her given
     * code/link to reset his/her password.
     */
    public function reset()
    {

    }

    /**
     * Post Request where the User provides
     * his/her refreshToken and gets a new token.
     * The old token gets invalidated.
     */
    public function getFreshToken()
    {

    }

    /**
     *  issueToken() is essentially the same as login()
     * POST Request where the User provides his/her credentials
     * email:password and gets a new access token
     */
    public function issueToken()
    {

    }
}

Sidenote: The Dingo Package works with Passport by making just a few adjustments:

  1. config/api.php
    /*
    |--------------------------------------------------------------------------
    | Authentication Providers
    |--------------------------------------------------------------------------
    |
    | The authentication providers that should be used when attempting to
    | authenticate an incoming API request.
    |
    */

    'auth' => [
        'custom' => \App\Providers\PassportDingoProvider::class
    ],
  1. app\Providers\PassportDingoProvider.php
<?php

namespace App\Providers;

use Dingo\Api\Auth\Provider\Authorization;
use Dingo\Api\Routing\Route;
use Illuminate\Http\Request;

class PassportDingoProvider extends Authorization
{
    /**
     * Get the providers authorization method.
     *
     * @return string
     */
    public function getAuthorizationMethod()
    {
        return 'bearer';
    }

    /**
     * Authenticate the request and return the authenticated user instance.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Dingo\Api\Routing\Route $route
     *
     * @return mixed
     */
    public function authenticate(Request $request, Route $route)
    {
        return $request->user();
    }
}
0 likes
9 replies
Rens's avatar

I'm trying to do the same thing, but without success. I guess it should also be possible to not only directly return the internal responses from Passport's controllers, but also wrap them in custom responses to enable your own formats.

Did you figure it out already?

c.schmidl's avatar
c.schmidl
OP
Best Answer
Level 4

Remove the Passport::routes(); statement from the boot()-method in App\Providers\AuthServiceProvider. This statement just adds the standard routes of Passport for your convenience. As you can see from my screenshot above, the explicit mapping from route to controller is already visible and you just have to define it yourself as you wish.

If you want to define a method for issuing a token in your custom AuthController, then just define it like this

    public function issueToken(ServerRequestInterface $request)
    {
        return $this->accessTokenController->issueToken($request);
    }

The AccessTokenController's issueToken-method expects an instance of the ServerRequestInterface in order to work properly. If you just define it like above, then Laravel will create such an instance for you automatically by typehinting it. You can return the response as above or wrap it with your own response. As I remember correctly, then all the responses are of the type of an HttpResponse which you can alter as you like before returning it to the user. All important Controller-methods regarding Passport work this way.

Don't forget to add the mapping of the routes to you routes/api.php file!

The tricky parts here is about the error/exception-handling. I tried to surround the logic with a try-catch-block but somehow it got ignored. If you take a look at the issueToken-method of the AccessTokenController, then you see why this is the case.

    /**
     * Authorize a client to access the user's account.
     *
     * @param  ServerRequestInterface  $request
     * @return Response
     */
    public function issueToken(ServerRequestInterface $request)
    {
        $response = $this->withErrorHandling(function () use ($request) {
            return $this->server->respondToAccessTokenRequest($request, new Psr7Response);
        });

        if ($response->getStatusCode() < 200 || $response->getStatusCode() > 299) {
            return $response;
        }

        $payload = json_decode($response->getBody()->__toString(), true);

        if (isset($payload['access_token'])) {
            // $this->revokeOtherAccessTokens($payload);
        }

        return $response;
    }

The important method here is "withErrorHandling". If the construction of a response fails in any way then this method is taking care of it. This method is part of the trait "HandlesOAuthErrors".

    /**
     * Perform the given callback with exception handling.
     *
     * @param  \Closure  $callback
     * @return Response
     */
    protected function withErrorHandling($callback)
    {
        try {
            return $callback();
        } catch (OAuthServerException $e) {
            $this->exceptionHandler()->report($e);

            return $e->generateHttpResponse(new Psr7Response);
        } catch (Exception $e) {
            $this->exceptionHandler()->report($e);

            return new Response($e->getMessage(), 500);
        } catch (Throwable $e) {
            $this->exceptionHandler()->report(new FatalThrowableError($e));

            return new Response($e->getMessage(), 500);
        }
    }

All kinds of exceptions are catched by this method and are propagated to the exceptionHandlers' report-method.

So, if you even want to alter Passport's standard exceptions, then just alter your App\Exceptions\ExceptionHandler's report-method. For example, you can do it like this:

    public function report(Exception $exception)
    {
        if($exception instanceof OAuthServerException){

            $this->generateDingoErrorResponse($exception);
        }

        parent::report($exception);
    }

Your logic regarding error-messages for OAuthServerExceptions is then contained in the generateDingoErrorResponse-method. You don't have to do it exactly like that but I hope that I could help you out a little bit and give you new ideas to tackle this problem :)

2 likes
Rens's avatar

Thanks for the elaborate answer! One question though about this part:

"You can return the response as above or wrap it with your own response. As I remember correctly, then all the responses are of the type of an HttpResponse which you can alter as you like before returning it to the user."

That doesn't seem to work for me. I can return the response using your statement, but when I try saving it into a variable and altering it in someway, that doesn't work. Probably because it's in some stream? Have you had any success accessing it and creating a custom response (wrapper)?

c.schmidl's avatar

It works without problems.

    public function issueToken(ServerRequestInterface $request)
    {
        $tokenResponse = $this->accessTokenController->issueToken($request);

        return response($tokenResponse->getBody(), 200);
    }

The response given by the issueToken-method is actually an instance of the Zend\Diactoros\Response class which is using the Zend\Diactoros\MessageTrait. If you want to modify the information contained in the tokenResponse and use it in your own response, you should take a closer look at the MessageTrait and its available methods.

<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @see       http://github.com/zendframework/zend-diactoros for the canonical source repository
 * @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
 */

namespace Zend\Diactoros;

use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;

/**
 * Trait implementing the various methods defined in MessageInterface.
 *
 * @see https://github.com/php-fig/http-message/tree/master/src/MessageInterface.php
 */
trait MessageTrait
{
    /**
     * List of all registered headers, as key => array of values.
     *
     * @var array
     */
    protected $headers = [];

    /**
     * Map of normalized header name to original name used to register header.
     *
     * @var array
     */
    protected $headerNames = [];

    /**
     * @var string
     */
    private $protocol = '1.1';

    /**
     * @var StreamInterface
     */
    private $stream;

    /**
     * Retrieves the HTTP protocol version as a string.
     *
     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
     *
     * @return string HTTP protocol version.
     */
    public function getProtocolVersion()
    {
        return $this->protocol;
    }

    /**
     * Return an instance with the specified HTTP protocol version.
     *
     * The version string MUST contain only the HTTP version number (e.g.,
     * "1.1", "1.0").
     *
     * This method MUST be implemented in such a way as to retain the
     * immutability of the message, and MUST return an instance that has the
     * new protocol version.
     *
     * @param string $version HTTP protocol version
     * @return static
     */
    public function withProtocolVersion($version)
    {
        $this->validateProtocolVersion($version);
        $new = clone $this;
        $new->protocol = $version;
        return $new;
    }

    /**
     * Retrieves all message headers.
     *
     * The keys represent the header name as it will be sent over the wire, and
     * each value is an array of strings associated with the header.
     *
     *     // Represent the headers as a string
     *     foreach ($message->getHeaders() as $name => $values) {
     *         echo $name . ": " . implode(", ", $values);
     *     }
     *
     *     // Emit headers iteratively:
     *     foreach ($message->getHeaders() as $name => $values) {
     *         foreach ($values as $value) {
     *             header(sprintf('%s: %s', $name, $value), false);
     *         }
     *     }
     *
     * @return array Returns an associative array of the message's headers. Each
     *     key MUST be a header name, and each value MUST be an array of strings.
     */
    public function getHeaders()
    {
        return $this->headers;
    }

    /**
     * Checks if a header exists by the given case-insensitive name.
     *
     * @param string $header Case-insensitive header name.
     * @return bool Returns true if any header names match the given header
     *     name using a case-insensitive string comparison. Returns false if
     *     no matching header name is found in the message.
     */
    public function hasHeader($header)
    {
        return array_key_exists(strtolower($header), $this->headerNames);
    }

    /**
     * Retrieves a message header value by the given case-insensitive name.
     *
     * This method returns an array of all the header values of the given
     * case-insensitive header name.
     *
     * If the header does not appear in the message, this method MUST return an
     * empty array.
     *
     * @param string $header Case-insensitive header field name.
     * @return string[] An array of string values as provided for the given
     *    header. If the header does not appear in the message, this method MUST
     *    return an empty array.
     */
    public function getHeader($header)
    {
        if (! $this->hasHeader($header)) {
            return [];
        }

        $header = $this->headerNames[strtolower($header)];
        $value  = $this->headers[$header];
        $value  = is_array($value) ? $value : [$value];

        return $value;
    }

    /**
     * Retrieves a comma-separated string of the values for a single header.
     *
     * This method returns all of the header values of the given
     * case-insensitive header name as a string concatenated together using
     * a comma.
     *
     * NOTE: Not all header values may be appropriately represented using
     * comma concatenation. For such headers, use getHeader() instead
     * and supply your own delimiter when concatenating.
     *
     * If the header does not appear in the message, this method MUST return
     * an empty string.
     *
     * @param string $name Case-insensitive header field name.
     * @return string A string of values as provided for the given header
     *    concatenated together using a comma. If the header does not appear in
     *    the message, this method MUST return an empty string.
     */
    public function getHeaderLine($name)
    {
        $value = $this->getHeader($name);
        if (empty($value)) {
            return '';
        }

        return implode(',', $value);
    }

    /**
     * Return an instance with the provided header, replacing any existing
     * values of any headers with the same case-insensitive name.
     *
     * While header names are case-insensitive, the casing of the header will
     * be preserved by this function, and returned from getHeaders().
     *
     * This method MUST be implemented in such a way as to retain the
     * immutability of the message, and MUST return an instance that has the
     * new and/or updated header and value.
     *
     * @param string $header Case-insensitive header field name.
     * @param string|string[] $value Header value(s).
     * @return static
     * @throws \InvalidArgumentException for invalid header names or values.
     */
    public function withHeader($header, $value)
    {
        if (is_string($value)) {
            $value = [$value];
        }

        if (! is_array($value) || ! $this->arrayContainsOnlyStrings($value)) {
            throw new InvalidArgumentException(
                'Invalid header value; must be a string or array of strings'
            );
        }

        HeaderSecurity::assertValidName($header);
        self::assertValidHeaderValue($value);

        $normalized = strtolower($header);

        $new = clone $this;
        if ($new->hasHeader($header)) {
            unset($new->headers[$new->headerNames[$normalized]]);
        }
        $new->headerNames[$normalized] = $header;
        $new->headers[$header]         = $value;

        return $new;
    }

    /**
     * Return an instance with the specified header appended with the
     * given value.
     *
     * Existing values for the specified header will be maintained. The new
     * value(s) will be appended to the existing list. If the header did not
     * exist previously, it will be added.
     *
     * This method MUST be implemented in such a way as to retain the
     * immutability of the message, and MUST return an instance that has the
     * new header and/or value.
     *
     * @param string $header Case-insensitive header field name to add.
     * @param string|string[] $value Header value(s).
     * @return static
     * @throws \InvalidArgumentException for invalid header names or values.
     */
    public function withAddedHeader($header, $value)
    {
        if (is_string($value)) {
            $value = [ $value ];
        }

        if (! is_array($value) || ! $this->arrayContainsOnlyStrings($value)) {
            throw new InvalidArgumentException(
                'Invalid header value; must be a string or array of strings'
            );
        }

        HeaderSecurity::assertValidName($header);
        self::assertValidHeaderValue($value);

        if (! $this->hasHeader($header)) {
            return $this->withHeader($header, $value);
        }

        $normalized = strtolower($header);
        $header     = $this->headerNames[$normalized];

        $new = clone $this;
        $new->headers[$header] = array_merge($this->headers[$header], $value);
        return $new;
    }

    /**
     * Return an instance without the specified header.
     *
     * Header resolution MUST be done without case-sensitivity.
     *
     * This method MUST be implemented in such a way as to retain the
     * immutability of the message, and MUST return an instance that removes
     * the named header.
     *
     * @param string $header Case-insensitive header field name to remove.
     * @return static
     */
    public function withoutHeader($header)
    {
        if (! $this->hasHeader($header)) {
            return clone $this;
        }

        $normalized = strtolower($header);
        $original   = $this->headerNames[$normalized];

        $new = clone $this;
        unset($new->headers[$original], $new->headerNames[$normalized]);
        return $new;
    }

    /**
     * Gets the body of the message.
     *
     * @return StreamInterface Returns the body as a stream.
     */
    public function getBody()
    {
        return $this->stream;
    }

    /**
     * Return an instance with the specified message body.
     *
     * The body MUST be a StreamInterface object.
     *
     * This method MUST be implemented in such a way as to retain the
     * immutability of the message, and MUST return a new instance that has the
     * new body stream.
     *
     * @param StreamInterface $body Body.
     * @return static
     * @throws \InvalidArgumentException When the body is not valid.
     */
    public function withBody(StreamInterface $body)
    {
        $new = clone $this;
        $new->stream = $body;
        return $new;
    }

    private function getStream($stream, $modeIfNotInstance)
    {
        if ($stream instanceof StreamInterface) {
            return $stream;
        }

        if (! is_string($stream) && ! is_resource($stream)) {
            throw new InvalidArgumentException(
                'Stream must be a string stream resource identifier, '
                . 'an actual stream resource, '
                . 'or a Psr\Http\Message\StreamInterface implementation'
            );
        }

        return new Stream($stream, $modeIfNotInstance);
    }

    /**
     * Test that an array contains only strings
     *
     * @param array $array
     * @return bool
     */
    private function arrayContainsOnlyStrings(array $array)
    {
        return array_reduce($array, [__CLASS__, 'filterStringValue'], true);
    }

    /**
     * Filter a set of headers to ensure they are in the correct internal format.
     *
     * Used by message constructors to allow setting all initial headers at once.
     *
     * @param array $originalHeaders Headers to filter.
     * @return array Filtered headers and names.
     */
    private function filterHeaders(array $originalHeaders)
    {
        $headerNames = $headers = [];
        foreach ($originalHeaders as $header => $value) {
            if (! is_string($header)) {
                throw new InvalidArgumentException(sprintf(
                    'Invalid header name; expected non-empty string, received %s',
                    gettype($header)
                ));
            }

            if (! is_array($value) && ! is_string($value) && ! is_numeric($value)) {
                throw new InvalidArgumentException(sprintf(
                    'Invalid header value type; expected number, string, or array; received %s',
                    (is_object($value) ? get_class($value) : gettype($value))
                ));
            }

            if (is_array($value)) {
                array_walk($value, function ($item) {
                    if (! is_string($item) && ! is_numeric($item)) {
                        throw new InvalidArgumentException(sprintf(
                            'Invalid header value type; expected number, string, or array; received %s',
                            (is_object($item) ? get_class($item) : gettype($item))
                        ));
                    }
                });
            }

            if (! is_array($value)) {
                $value = [ $value ];
            }

            $headerNames[strtolower($header)] = $header;
            $headers[$header] = $value;
        }

        return [$headerNames, $headers];
    }

    /**
     * Test if a value is a string
     *
     * Used with array_reduce.
     *
     * @param bool $carry
     * @param mixed $item
     * @return bool
     */
    private static function filterStringValue($carry, $item)
    {
        if (! is_string($item)) {
            return false;
        }
        return $carry;
    }

    /**
     * Assert that the provided header values are valid.
     *
     * @see http://tools.ietf.org/html/rfc7230#section-3.2
     * @param string[] $values
     * @throws InvalidArgumentException
     */
    private static function assertValidHeaderValue(array $values)
    {
        array_walk($values, __NAMESPACE__ . '\HeaderSecurity::assertValid');
    }

    /**
     * Validate the HTTP protocol version
     *
     * @param string $version
     * @throws InvalidArgumentException on invalid HTTP protocol version
     */
    private function validateProtocolVersion($version)
    {
        if (empty($version)) {
            throw new InvalidArgumentException(sprintf(
                'HTTP protocol version can not be empty'
            ));
        }
        if (! is_string($version)) {
            throw new InvalidArgumentException(sprintf(
                'Unsupported HTTP protocol version; must be a string, received %s',
                (is_object($version) ? get_class($version) : gettype($version))
            ));
        }

        // HTTP/1 uses a "<major>.<minor>" numbering scheme to indicate
        // versions of the protocol, while HTTP/2 does not.
        if (! preg_match('#^(1\.[01]|2)$#', $version)) {
            throw new InvalidArgumentException(sprintf(
                'Unsupported HTTP protocol version "%s" provided',
                $version
            ));
        }
    }
}

Rens's avatar

It works when you return the response directy, but when I try to manipulate things go wrong:

        $tokenResponse = $this->accessTokenController->issueToken($request);

        $body = json_decode($tokenResponse->getBody());
        //$body = json_decode($tokenResponse->getBody()->getContents()); // Also not working.

        // TODO: add something to the body here.

        return response($body, 200);

Maybe I'm trying to extract things in the wrong way here?

c.schmidl's avatar
    public function issueToken(ServerRequestInterface $request)
    {
        $tokenResponse = $this->accessTokenController->issueToken($request);
        $bodyAsStream = $tokenResponse->getBody();

        // Seek to the beginning of the stream.
        $bodyAsStream->rewind();

        $body = json_decode($bodyAsStream->getContents());

        //dd($body);

        return response($body->access_token, 200);
    }

There you go.

Rens's avatar

Great, that works! Thanks!

1 like
Bartestro's avatar

Hi @c.schmidl

I'm using your solution with dingo and passport.

My question is, how to create internal route to api defined in dingo and protected by passport?

So to be more precise, I'm using dingo internal routing with: 'dispatcher->be(Auth::user())->get...' to api route

When I have '["middleware" => "api.auth"]' on this route I get unauthenticated message.

BTW. Of course with proper passport token it gets authenticated from postman

This was working fine, when I used original dingo jwt auth, but as I changed it to passport, it doesn't work anymore. Do you have any idea on how to get around this?

Bartestro's avatar

Hi @c.schmidl again,

I decided to call methods, from api controllers directly, in my app web routes.

Seems kinda obvious, but I wanted to keep api routes separated, and some problems popped out.

So this solves my problem, but I still wonder, if you could propose any solution for that?

Please or to participate in this conversation.