odan / slim4-skeleton

A Slim 4 Skeleton
https://odan.github.io/slim4-skeleton/
MIT License
439 stars 80 forks source link

Validating JWT protected resource #57

Closed mapicard closed 3 years ago

mapicard commented 3 years ago

First off, many thanks for your tutorials, they are very helpful. I've followed the JWT section in your eBook and all is well but how do I go from there to protect some resources with this token. For example, if I want to pull a user profile from a USER table, I need to make sure the token is not only valid but corresponds to this specific user. It seems logical to inspect the token to retrieve the 'uid' but where? When JwtAuthMdwr completes, if it is a valid token, control is passed to the routeAction... should I get the token and inspect for proper uid at this point ? If so, how ?

odan commented 3 years ago

Hi @mapicard

When the API / JWT authentication is done, the request contains the valid "uid" as attribute within the request object.

$request = $request->withAttribute('uid', $token->claims()->get('uid'));

You could (for example) convert that identifying information from the Presentation (User Interface) layer into the Application layer, and letting the Application layer coordinate the creation of the Domain layer User instance via Infrastructure implementations. Read more

To map the "uid" into a real user value object or DTO you can map that within a custom middleware:

<?php

namespace App\Middleware;

use App\Domain\Auth\UserAuth;
use App\Domain\Auth\UserCredentialExchanger;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class UserAuthMiddleware implements MiddlewareInterface
{
    private UserAuth $userAuth;

    public function __construct(UserCredentialExchanger $credentialExchanger, UserAuth $userAuth)
    {
        $this->credentialExchanger = $credentialExchanger;
        $this->userAuth = $userAuth;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $userId = $request->getAttribute('uid')

        if (!$uid) {
            return $this->responseFactory->createResponse()
                ->withHeader('Content-Type', 'application/json')
                ->withStatus(401, 'Unauthorized');
        }

       // Map userId to domain layer user instance
       $user = $this->credentialExchanger->getUser($userId);

       // Set user into the UserAuth instance
       $this->userAuth->setUser($user);

        return $handler->handle($request);
    }
}

Then you need an additional authorization check that should happen before the service starts to retrieve the data. The implementation for the authorization depends on your specific requirements. RBAC or ACL etc.

mapicard commented 3 years ago

Hi @odan,

You could (for example) convert that identifying information from the Presentation (User Interface) layer into the Application layer, and letting the Application layer coordinate the creation of the Domain layer User instance via Infrastructure implementations. Read more

This is dense, I definitely need to "Read more". Thanks for the pointers.

mapicard commented 3 years ago

additional authorization check that should happen before the service starts to retrieve the data.

Should i conclude that this check should be done in the Action module? For example to make sure only the owner of a profile can update it, and assuming the uid in the token is the sequential $userId of the userRepo, i would simply...

if ( $request->getAttribute('uid') === $userId ) {
    $this->userUpdater->updateUser($userId, $data)
}
else {
    ... unauthorized
odan commented 3 years ago

The uid could also be something different, like a UUID or an email address. It depedens on your specific use case. There is no "rule" that says it must be the ID of the table "users". At the end it must be a unique identifier (credential) for you application.

I think your example should work but it would also produce a lot of redundant code.

I would try to add middleware that initializes the auth component with the current user based on the UID of the request with the JWT. My example from above shows how this middleware might look. The specific implementation of course depends on the used Auth component.

Maybe I will write an additional article about this specific topic.