Middleware and pathFor()


#1

I do implement some payment stuff in my application where an order is created, the user is sent to Paypal and then returns to a specific route with the paypal details. So far so good. Now there is also the option to cancel an order which is another route and even though chances are slim, I don’t want malicious users to cancel other people’s orders by guessing the order ID and calling the cancel route.

So instead of using an integer for the order ID I want to use https://hashids.org/ for keeping the ID unguessable in the URL. I can implement this completely in the controller of course. But I was wondering if I could use a Middleware for that. I can always call hashids.decode() on any passed parameter called order in a middleware.

But is there a way to always call hashids.encode() when a parameter called order is passed to pathFor()?


#2

You could implement a Middleware to map the Hash-ID parameter into a Integer-ID.

Example (not tested)

use Slim\Http\Request;
use Slim\Http\Response;

class HashIdMiddleware {

    public function __invoke(Request $request, Response $response, $next)
    {
        $hashids = new \Hashids\Hashids('this is my salt');
        $id = $request->getQueryParam('id'); // change the parameter name
        $orderId = $hashids->decode($id);

        $request = $request->withAttribute('id', $orderId);

        return $next($request, $response);
}

Then in your controller/action you just have to fetch the integer-ID and pass it to the view.

$orderId = $request->getAttribute('id');

$viewData = [
    'orderId' => $orderId,
];

return $this->view->render($response, 'order.twig', $viewData);

Template usage

<a href="{{ path_for('order', { 'id': orderId }) }}">Order</a>

#3

Well, this answers the obvious path of decoding the hash from passed parameters. My question is about the reverse route generation. I want to pass an integer ID to pathFor and have it generate an URL with a hash ID in it.


#4

I do something similar, but with lots of different IDs. (Essentially all of my primary keys.) So I created a Trait I apply to each of my models that adds a getter taking the id primary key and encoding the ID. Then in my view I can do something like this.

<a href="{{ path_for('order', { 'id': orderHashId }) }}">Order</a>

I keep thinking about moving the encoding and decoding to middleware, but I have too many scenarios where that might break my app.

Keep in mind that hashids are not really unguessable. Preventing someone from canceling an order that isn’t their own with hashids doesn’t sound like a good idea. See the What about Hashids section of this article and What Not do Do from the Hashids library.


#5

Ah using a trait would at least work around having to reimplement this multiple times (not that I’m having that problem currently).

So I guess the answer to my question simply is: Middlewares can not influence the reverse routing mechanism of the router’s pathFor() method.

Thanks for the pointers about the cryptographic problems with hashids – in my case it’s more about a minor annoyance, not actually causing any (financial) harm to users. But I’ll keep it in mind.


#6

You probably can. I haven’t specifically tried, but you can access the route from middleware as it is part of the request.

$hashedOrderId = $request->getAttribute('routeInfo')[2]['orderId']);

You may need to inspect $request->getAttribute('routeInfo') or $request->getAttribute('route') to find what you need.

routeInfo may disappear in Slim 4, but replaced with something else. You might also need determineRouteBeforeAppMiddleware in your settings.


#7

That seems to be the wrong side of routing again. I know that I can manipulate the request in the middleware.

But I want to manipulate the reverse routing (based on named routes).

$container->router->pathFor('order', ['order_id' => 5]) currently returns '/some/path/order/5'.

I want my “Middleware” to make it return '/some/path/order/GxStsZ'. That does not seem to be possible.


#8

That does not seem to be possible.

The middleware could “parse” the html response and change all the id parameters. Hacky but possible.