Why Use Signed URLs0:00
Let's talk about signed URLs. Sometimes you want to expose a public URL that performs some kind of user or account-specific destructive action. A common example is I send you a newsletter, and within that email is a unsubscribe link, where you click it and it instantly unsubscribes your account from future emails or newsletters. Now think about it. How do you create that link without requiring that they sign in to their account to authenticate? It's kind of tricky. For example, you could have your domain slash users slash their ID slash unsubscribe. But yeah, that's a public URL, which means I could presumably replace this with any user.
Generating Signed Routes1:05
only one person can access it. That's what a signed URL is for. I'll give you the quick 101, and then we'll review an actual example. Let's give the homepage a name of home. This is important. In Laravel, a signed URL points to a route. OK, so let's open up my terminal. We'll boot up php artisan tinker. And here's what we want. This uses the URL generator, and I want a signed route to home.
And here's what we want. This uses the URL generator, and I want a signed route to home. So notice I do get the correct URL, but it also includes a hashed signature of that URL. This is how we can verify that the URL has not been tampered with. Like this. I could say if the request does not have a valid signature, then it's been tampered with. So why don't we abort with a 403, 400, 401, whatever you want. Otherwise, if the signature is valid, then return the welcome view. OK, let's give this a shot. If I load this in my browser and I don't include that hash, sure enough, I do get a 403 forbidden.
Temporary Signed Links2:00
OK, let's give this a shot. If I load this in my browser and I don't include that hash, sure enough, I do get a 403 forbidden. OK, but now let's use the full hash here. And it works. OK, so now we're starting to understand how this works. And keep in mind, if we tweak it at all, it fails. Now further, we could change this up and make it a temporary signed route. So I could say you can click on this link for the next hour, but after that, it expires. And again, very common for unsubscribed links or update links. So let's generate a temporary signed route for a total of a day.
How Signatures Validate2:34
And again, very common for unsubscribed links or update links. So let's generate a temporary signed route for a total of a day. You can click this for the next day. OK, so now we have our signature, but there's also an expiration key there. So basically what happens here is Laravel generates a signature based on the current URL. And I think it uses the hash HMAC, it uses SHA-256, that method. We then give it the full URL. And then finally, it's going to want your app key, and you should be able to find it here.
And then finally, it's going to want your app key, and you should be able to find it here. So notice, if I run hash_hmac, and again, that's just a php function that hashes a string using SHA-256, and notice here, matches up exactly with what you see here. So now what happens in your routes file is when you check if it has a valid signature, is Laravel basically does this again, and it checks if the current URL matches the original signature that it generated. And if it did, you're all set to go. If it doesn't, you get booted out with a 403. OK, so now that you understand the basics, let's create an option to update a User's
Email Change Confirmation Flow3:44
If it doesn't, you get booted out with a 403. OK, so now that you understand the basics, let's create an option to update a User's email address. Maybe you want that to be a bit more secure. For example, a User has an email, but we will also have a pending email. So you can imagine from your account settings, you want to change your email address. In order to change an email, you have to confirm that you own the original email address. You don't have to do this, but some people prefer it. So basically, before we commit this change, we email the original address, and we say, hey, just click this link to confirm it.
So basically, before we commit this change, we email the original address, and we say, hey, just click this link to confirm it. You've likely seen this before. We'll allow for that through a pending_email field. So let's go ahead and migrate my database. We now have a users table with a pending_email. Next, let's whip up a quick User. So app/Models/User.php, and give me a quick factory. All right, we got flow more. Next, I'm not going to create a form for this, so let's quickly find that User again.
All right, we got flow more. Next, I'm not going to create a form for this, so let's quickly find that User again. And then let's set a pendingEmail on that User. pendingEmail will be change at example.com. Okay, so now this would simulate flow, in this case, updating their account. And instead of immediately updating the current email, we just set it to pending. Then as part of that, you fire off a Mailable or a notification that includes a signed URL or a signed route. As soon as they click on that route, the process is complete and their email has been updated.
Building Confirm Email Endpoint5:22
As soon as they click on that route, the process is complete and their email has been updated. That's what we need to work on. Okay, let's see. We want this to be public, of course. So how about users, a specific User, and then maybe emailConfirm. Next, this will be something like, what's the controller? ConfirmEmailChangeController, something like that. Let's go ahead and create that. php artisan make:controller ConfirmEmailChangeController.
Let's go ahead and create that. php artisan, make me a controller called ConfirmEmailChangeController. And I just want to import that at the top, all right? This can be a standard invocable. invoke, this will accept the User in question. And yeah, at this point, assuming a valid signature, we commit the change. So we would just say $user->email will be what the $pendingEmail was. We can then clear out the $pendingEmail. And then we save and redirect somewhere. I'm just going to say email update complete, because we're short on time.
And then we save and redirect somewhere. I'm just going to say email update complete, because we're short on time. So let's give it a go. We're going to come back to our routes file. We need to give this a name. So we'll just say confirmEmailChange. And now think about it. We have a public endpoint, public meaning anyone can access it, that performs some kind of destructive action on an account. Now to protect ourselves, we want to confirm that signature.
that performs some kind of destructive action on an account. Now to protect ourselves, we want to confirm that signature. So we're going to do the same thing as we did before. And this time, if you want, we will inject the request. And we'll say, if the request does not have a valid signature, then once again, abort. Let's do 403 unauthorized this time. This is how we protect ourselves. All right, so let's give it a shot. The current URL is this.
All right, so let's give it a shot. The current URL is this. We'll give it a shot, signedroute.test/users/, I assume that user has an ID of 1. We give it a run and nope. We get a 403 forbidden. But now, let's create a signed URL, and we want a temporary, how about a temporary signed route to that named route, confirmEmailChange. This will be active for, how about, three hours.
to that named route, confirm email change. This will be active for, how about, three hours. Finally, we need to give it the route key. In this case, again, I'm hoping that user has an ID of one. I assume it does, because it's the first user in our system. But if we give it a run, there we go. So notice we have a full URL to users/1/email/confirm. But now we do have that hash. So I'm going to copy this full thing. And once again, if it's tampered with, we'll just change it there.
So I'm going to copy this full thing. And once again, if it's tampered with, we'll just change it there. Or, of course, maybe we're going to try to confirm the User with an ID of 5. Nope, it doesn't work. Because if you change that URL, that would then change the hash. And then when Laravel validates the hash, the new one will not match what you had here. That's how that works. So anyways, I'll paste it in again, and we get email update complete. So with any luck, if I now find that User,
So anyways, I'll paste it in again, and we get email update complete. So with any luck, if I now find that User, notice her email has been updated and we've reset the pending email. The process is now complete. And that's all you need to know about signed routes. So now the only remaining step would be in your AccountController, when the User submits that form, you would just check something like, if they have changed their email address, then fire off a Mailable. And as part of that Mailable's body, you would use something just like that and throw it into your email.
