tuupola / slim-basic-auth

PSR-7 and PSR-15 HTTP Basic Authentication Middleware
MIT License
440 stars 66 forks source link

Accessing slim request/app from inside custom authenticator #107

Closed sudofox closed 3 years ago

sudofox commented 3 years ago

I'm very new to Slim in general, and have been searching for a simple way to do take an Authorization: Basic ... header and use it to make decisions. I found your library and thankfully it seems to work. However, I'm struggling a bit. I need to do three things:

  1. Get basic auth and then check if the creds are correct. This is working fine, and results in a $user User object (from Propel ORM).
  2. Get the current route and then check if the user has permissions to access it.
  3. Somehow make $user available to every route handler as I will need to be able to know who authenticated

Right now I have no clue how to do 2 and 3.

class InternalApiAuthenticator implements AuthenticatorInterface {
    public function __invoke(array $arguments): bool {

        if (!isset($_SERVER["PHP_AUTH_USER"], $_SERVER["PHP_AUTH_PW"])) {
            return false;
        }

        $username = $_SERVER["PHP_AUTH_USER"];
        $password = $_SERVER["PHP_AUTH_PW"];

        $user = new App\RBAC\UserQuery;
        $user = $user->findOneByUsername($username);
        if (!$user) {
            return false;
        }

        // check password

        if (!$user->check_password($password)) {
            return false;
        }

        $the_route  = getRouteSomehow(); // /foo/bar/baz
        $PathUtil   = new App\RBAC\Action\PathUtil;
        $actionPath = $PathUtil->apiToActionPath($the_route); // Foo.Bar.Baz

        if (!$user->check_permissions($actionPath)) {
            return false;
        }

        return true;
    }
}

and this is where I'm adding it in:

$app = AppFactory::create();

$app->add(new HttpBasicAuthentication([
    "path" => "/",
    "realm" => "Protected",
    "relaxed" => ["foo.bar.redacted.com"],
    "authenticator" => new InternalApiAuthenticator
]));

$app->get('/foo/bar/baz', function (Request $request, Response $response, $args) {
    $response->getBody()->write(json_encode($_SERVER, JSON_PRETTY_PRINT));
    return $response;
});

$app->run();

Is there any way to do this? I've been spinning my wheels for a bit.

sudofox commented 3 years ago

I've gotten back to digging into this and I think it might be possible to use a rule or something, since the __invoke method required by the RuleInterface actually takes ServerRequestInterface ...? It seems like undocumented functionality though

This all seems rather flimsy to me though -- still unclear how I would first authenticate the user and then get the API method to check against their permissions. Maybe I'll have to drop this library or something but I feel like there's an obvious answer I'm missing here since being able to check if a user has permissions to access a particular route beyond a hardcoded set of credentials/paths should be elementary.

sudofox commented 3 years ago

I figured out an easier way to do it without using this library (sorry!)

I'm putting it here to save anyone else the time.

Here's a stripped down version that checks for basic auth and such, Slim 4 of course.

<?php

use YourApp\AuthMiddleware;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteContext;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;

class AuthMiddleware implements MiddlewareInterface {
    /**
     * @var ResponseFactoryInterface
     */
    private $responseFactory;

    /**
     * @param ResponseFactoryInterface $responseFactory
     */
    public function __construct(ResponseFactoryInterface $responseFactory) {
        $this->responseFactory = $responseFactory;
    }

    /**
     * @inheritDoc
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {

        $failure = $this->responseFactory->createResponse();
        $failure->getBody()->write('Authorization denied');
        $failure = $failure->withStatus(401);

        if (!isset($_SERVER["PHP_AUTH_USER"], $_SERVER["PHP_AUTH_PW"])) {
            // auth not provided
            return $failure->withAddedHeader("X-Debug", "auth not provided");
        }

        $username = $_SERVER["PHP_AUTH_USER"];
        $password = $_SERVER["PHP_AUTH_PW"];

        // do your auth stuff here

        $auth_condition_that_should_be_true = true;

        $user = [ "id" => 1 ]; // whatever data or object you need

        if (!$auth_condition_that_should_be_true) {
            // user doesn't exist
            return $failure->withAddedHeader("X-Debug", "user doesn't exist");
        }

        // now we can check that the user has permission
        $routeContext = RouteContext::fromRequest($request);
        $route = $routeContext->getRoute();
        $the_route  = $route->getPattern(); // /foo/bar/baz    

        $the_user_has_permission_for_route = true;

        if (!$the_user_has_permission_for_route) {
            // user doesn't have permission
            return $failure->withAddedHeader("X-Debug", "no permission for action");
        }

        // load the user so it's accessible in the request handler
        $newRequest = $request->withAttribute("user", $user);

        // Keep processing middleware queue as normal
        return $handler->handle($newRequest);
    }
}

And you can then do this:

<?php

// ... composer and whatever else ...

use YourApp\AuthMiddleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Slim\Psr7\Response;

$app = AppFactory::create();

$app->addMiddleware(new AuthMiddleware($app->getResponseFactory()));
$app->addRoutingMiddleware();

$app->get('/your/api/path', function (Request $request, Response $response, $args) {

    // you can get your user stuff here
    $user = $request->getAttribute("user");

    $result["user"] = [
        "id"             => $user->getId(),
        "username"       => $user->getUsername(),
    ];

    $response->getBody()->write(json_encode($result, JSON_PRETTY_PRINT));
    return $response;
});

$app->run();