Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

RoboRobok's avatar

Laravel signed URL with hidden expiration time

I'm playing with Laravel's signed URLs and they are awesome. I really like the feature of expiring signed URLs, but there's one thing that I'd like to change.

An example of a signed URL with expiration time is:

/foo?expires=1634335939&signature=31ee9463d1f3d0584d435e6bde2bfa66d7dcde3ee1f998b270e62d1a7584f356

I'm not sure why it was decided to include the expires in the query string, I don't like that. This value is being included in the signature anyway (obviously), so showing it to the public doesn't make much sense and I'd prefer to keep it private.

Can temporarySignedRoute() be configured to hide the expires query param?

0 likes
35 replies
MichalOravec's avatar

Show me the code where the expires is included in the signature?

You won't find anything like that...

RoboRobok's avatar

@MichalOravec I mean that the signature validates the expiration time, so you can't tamper with it by editing the timestamp in the URL.

tykus's avatar

@MichalOravec Theexpires query param is included in the signature hash

    public function signedRoute($name, $parameters = [], $expiration = null, $absolute = true)
    {
        $this->ensureSignedRouteParametersAreNotReserved(
            $parameters = Arr::wrap($parameters)
        );

        if ($expiration) {
//here it is added to the $parameters
            $parameters = $parameters + ['expires' => $this->availableAt($expiration)];  
        }

        ksort($parameters);

        $key = call_user_func($this->keyResolver);

// and here the URL & query parameters are hashed
        return $this->route($name, $parameters + [
            'signature' => hash_hmac('sha256', $this->route($name, $parameters, $absolute), $key),
        ], $absolute);
    }
RoboRobok's avatar

@tykus I've been watching a movie when you posted, so didn't look at it. Please don't kill me.

tykus's avatar

@RoboRobok not trying to kill you - you called me out with incorrect information

@tykus small correction: it's not a hash. It's encrypted.

🤷‍♂️

RoboRobok's avatar

@tykus I didn't call out anybody, I just really thought it was encrypted.

Funfare's avatar

The expires parameter needs to be in the URL. The temporary signed route is technically the same as a normal signed route, just the expires parameter is checked by laravel. You can't get the time from the signature, the time is just a part of the URL.

If you want to hide it, you need to do it by yourself. e.g. store the hash with the expire time in the database and set there an expire date. But out of the box you need the parameter

RoboRobok's avatar

@Funfare you actually can get the expiration timestamp back, because the signature is encrypted, not hashed.

Funfare's avatar

@RoboRobok There you are wrong. Look into the code, laravel generates a hash and is not encypting it

return $this->route($name, $parameters + [
            'signature' => hash_hmac('sha256', $this->route($name, $parameters, $absolute), $key),
        ], $absolute);

hash_hmac is a hash function, so you cannot decrypt it.

1 like
RoboRobok's avatar

@Funfare okay, I must have confused it with something else. I experimented and I got different signatures after changing the app key. And the app key is normally only used for encryption. Sorry about that. If it's hashed, then it makes more sense.

Funfare's avatar

@RoboRobok hmac is a hash function based on a secret key, so this is why the signature changes when you change the app key. (and so no one else can create the hash of an url that is valid on your page)

RoboRobok's avatar

@Funfare do you know if any other hashes in Laravel use the app key? I know that passwords don't.

RoboRobok's avatar

@jlrdw yes, that's why I got confused when I learned that signatures are being hashed with it.

Edit: I have seen that tweet a few days ago :)

martinbean's avatar

okay, I must have confused it with something else. I experimented and I got different signatures after changing the app key. And the app key is normally only used for encryption. Sorry about that. If it's hashed, then it makes more sense.

@RoboRobok That’s how hashing with a salt works. Every time you hash the same plaintext value, you’ll get a different hash. Try hashing the same password: you’ll get different hashes in your database. The hash didn’t change because you changed the app key; the hash changed because it would have any way.

And it helps to have your facts right before you start calling people out and telling them they’re wrong on things like hashing versus encryption. Sounds like you could do with reading up on the basics of hashing too given you don’t seem to know the same value can produce different hashes.

Back to the question at hand, the expires parameter is required because it forms part of the signature. If it wasn’t there, then the signature would just be of the URL and would then be never-expiring. But by checking the signature, Laravel can safely assume the given expires value hasn’t been tampered with via the client (otherwise the signature wouldn’t match) and therefore rely on it as to whether the timestamp has passed and URL expired.

RoboRobok's avatar

@martinbean you missed the point. Nobody here confuses encryption with hashing as a concept. Like I said, app key in Laravel wasn't supposed to have anything to do with hashing, yet it's used as salt for signature hashing. And that's quite strange. Why isn't salt generated similarily to password hashing in this case?

Snapey's avatar

OK, I wanted to make sure you were not thinking this because you observed that when you changed the app.key and the signature changed

If using the app.key is an issue for you;

https://dyrynda.com.au/blog/customising-laravels-url-signing-key

But the point of this thread is that the signature is only making sure the rest of the URL has not been tampered with. If you want to hide the expiry in some way then use a signed URL without the expiry time and do your own check in some other way. For instance, it it were an invite to perform some action, you should be able to check when that action was created and then check the difference to current time.

Snapey's avatar

I must have confused it with something else. I experimented and I got different signatures after changing the app key.

You will get a different signature every time irrespective of changing the app.key

RoboRobok's avatar

@Snapey hmm, I don't think so though. Can you try it and confirm? Because my yesterday's tests don't confirm it.

Snapey's avatar

@RoboRobok You are correct. This is using a HMAC function which will always return the same signature given the same inputs, and is used for message authentication where both parties know the same shared secret (in this case the app.key)

https://en.wikipedia.org/wiki/HMAC

Michael's post discusses setting your own shared secret so that two apps can check the validity of the signature without sharing the app.key

RoboRobok's avatar

@Snapey thanks for confirming. Yeah, so also having expires in the query string makes perfect sense now, as I learned that the signature is being hashed and not encrypted. Expiration date is still part of the payload for hashing (to prevent manually changing it), but we can't read it back from the signature to validate if the URL expired or not. It all makes perfect sense now.

martinbean's avatar

@RoboRobok Would have made perfect sense if you had just taken 5 minutes to read the source code too, instead of constantly asking people to “confirm” whilst also incorrectly telling people they’re wrong.

Please or to participate in this conversation.