Using Laravel signed routes to improve order confirmation security

The order confirmation page on our 3D cards shop is shown to a customer after they complete checkout, and is also linked to in the confirmation and dispatch emails that they receive.

Importantly, this page is not authenticated; we don’t want a heavy sign up / sign in process for customers as that tends to be annoying on other ecommerce systems. I just want to buy some pop-up cards – why do I need to create an account and remember a password etc?

Because the page is not authenticated, it would be possible for someone to guess order IDs and try to iterate all of the order pages to scrape information. To mitigate that, the order page contains no sensitive or personal information. It just shows the order items and order time, to let the customer at least confirm those details. The customer’s email address and delivery address are not shown, so that they can not be leaked to scrapers.

This design is OK but has some drawbacks. It would be nice if the customer could confirm all of their order details on the order page. In particular, we’d like to encourage customers to double-check that their delivery address is correct while there’s still time to change it. A surprising number of orders are lost and must be refunded due to customers entering their delivery address incorrectly.

The first idea we had was to show full details within a time limit after the order is created, e.g. you get the full view for up to one hour after order placement, and after that it just shows the basic non-sensitive version. The vast majority of customers only look at the order confirmation view once anyway. This would be easy to implement, but is risky – a scraper could come in during that hour window and steal personal information (and the attacker does know the system).

The second idea was to note the order id in the session, and then only show full details if the current user’s session has that order id. This would prevent anyone else getting the full details on the order view, and is better than the time limit as there is no window in which a scraper can snatch the details; they will never get the access set in their session. It would mean that once the user’s session expires, they will no longer see full details on their order confirmation page, but that’s acceptable.

The third idea was to use a signature in the URL. The application would generate a signature using the order id and and the application’s secret key, and include that in the order confirmation URL. When displaying the order confirmation view, the application would only display full details if there is a valid signature.

It turns out that Laravel has a signed URLs feature built-in for this kind of situation. You can generate a signed URL like this:

<?php

URL::signedRoute('route.name', ['fooBar' => 42])

You can check if any request has a valid signature like this:

<?php

if ($request->hasValidSignature()) {
        // act accordingly
}

It wouldn’t be difficult to implement this yourself, but it’s nice having it built-in so it requires little effort to apply where necessary.

The signed URL approach is quite nice as it means you can have a permanent order confirmation page with an un-guessable URL. Only the user has the signed URL as you only give it to that user. However, it has a drawback for the same reason – the user might share that URL (intentionally or inadvertently), and reveal their email address and delivery address to whoever has the URL.

Sharing the URL is a valid use-case, though: the customer might want to let someone else know that they have placed the order and show them the confirmation.

In the end, we went with a combination of the session-access and signed URL approaches. We note the order id in the session, and that is used in conjunction with a valid signature to decide how much information to show on the order confirmation page:

By visiting the signed URL that they are given (i.e. redirected to) on order completion, the customer can see the full order details including their email address and their delivery address. This gives them a chance to confirm everything is correct, and they can keep accessing this in the same browser for as long as their session is alive (which is up to 24 hours), for example from links in emails that they receive.

After the session has expired, or if they access the signed URL separately, the user only sees minimal information about their order with no personal information. This means that if the user does share their signed URL, the personal information is not revealed but the order content is still shown.

Visiting the order confirmation view with an unsigned URL or an invalid signature does acknowledge that such an order exists and shows the date it was placed, but does not give any other information. This means that scrapers can figure out our overall order numbers if they want to, but that’s not a concern for us. Scrapers cannot gather any information about order value or content, and most importantly cannot gain any customer information at all.

This combination solution reaches a good balance of utility whilst never revealing personal user information to other people, which is the non-negotiable requirement here.


Tech mentioned