Suppose you have an application to which access is so sensitive that if any user’s password is found to be breached, the account should immediately be locked; the user should not be able to sign in. While you can force a user to change their password out of the box, an outright lock option isn’t built-in. What can you do?
An auth system is rarely sufficient on its own. After all, users want to authenticate in order to use an application’s functionality, not for the fun of entering their username and password. User management systems provide forgot password flows, breached password detection and admin user interfaces. But they also typically integrate into one or more applications, whether custom, open source, or SaaS software.
FusionAuth gives you options. You can integrate using JWTs or the APIs. However, when events happen, you may want to notify other parts of your system so they can take action. Webhooks can help. You may also use webhooks to extend the functionality of FusionAuth, listening to events and then calling back into FusionAuth using APIs.
In this tutorial, we’ll extend FusionAuth to lock an account if a user signs in with a compromised password.
Due to the use case, this post focuses on the breached password detection event, but the integration principles apply for any of the over fifteen events for which FusionAuth can fire a webhook.
This is different from locking an account based on login attempts. In this case, you are relying on the breached password detection feature.
Note: breached password detection is a paid edition feature.
Prerequisites
- A modern PHP (tested with PHP 7.3)
- FusionAuth installed (see the 5 minute setup guide if you don’t have it)
- A FusionAuth license
If you’d like to jump ahead to the code, here’s the GitHub repo that you can download and explore.
Set up users, license, and API keys
Once you’re in the administrative interface, create a user with a horrible password, one that is compromised. I suggest password
as a tried and true option.
Next, activate your license.
Create an API key by navigating to the “Settings” tab and then to “API Keys”. At a minimum configure the following permission for this key: DELETE
on the /api/user
endpoint. Note the API key for later use.
Configure breached password detection
Navigate to the “Tenants” section and then to the default tenant. Go to the “Passwords” tab. Take the following steps:
- Enable breached password detection.
- Set the “On login” option to “Only record the result, take no action.”
Your settings should look like this:
Configure tenant webhook settings
Now, you need to configure the webhook at the tenant level. This will ensure the tenant emits the event and waits for the webhook’s success. Navigate to the “Webhooks” tab for the default tenant. Enable the user.password.breach
event and set the “Transaction setting” to “All the Webhooks must succeed”.
Click the “Save” button to persist the tenant configuration. Now that you have configured the tenant to send events, you need to configure a webhook to listen.
Configure the webhook
While a bit more complicated, separately configuring the tenant to emit an event and the webhook to receive it provides flexibility. You can create global webhooks and then have tenants specify which events are sent. For example, if you are private labeling an application with FusionAuth’s multi-tenancy functionality, you could set up one tenant to emit new user registration events and another to send failed user logins. If you want to emit the same event in different tenants, you can also configure the webhook to listen to specific applications. See the docs for more information.
Navigate to the “Settings” section, and then to “Webhooks”. You may need to scroll to see this section. You’re now setting up the webhook to receive the breached password detection event.
Now, create the webhook in FusionAuth. Set the URL to http://localhost:8000/webhook.php
. For this example, using the http
protocol is fine, but for production, please use TLS. Add a description, and you should end up with a screen like this:
Scroll down and make sure the user.password.breach
event is enabled:
It’s a good idea to secure your webhook so no unauthorized POSTs are processed. You can do that with a header, basic auth, or at the network layer. For this application, let’s configure FusionAuth to send a header value when the webhook is called:
Finally, click the “Save” button, as you are done configuring the webhook.
Write the webhook code
Now that FusionAuth is properly set up, let’s look at some code. We’ll be using PHP because it’s a performant language that handles JSON well. You could use any of the client libraries or call the APIs directly. I suppose you could write the webhook in bash, IF YOU DARE.
But we’ll use PHP.
The code is available here if you want to check it out. Here’s the webhook.php
file, the heart of the example:
<?php
require __DIR__. '/config.php';
require __DIR__. '/vendor/autoload.php';
$headers = getallheaders();
if (!$headers) {
error_log("Invalid authorization header.");
return;
}
$authorization_value = $headers['Authorization'];
if ($authorization_value !== $authorization_header_value) {
error_log("Invalid authorization header value found: ".$authorization_value);
return;
}
$input = file_get_contents('php://input');
$obj = json_decode($input);
if (!$obj) {
error_log("Invalid JSON");
return;
}
$type = $obj->event->type;
if ($type !== "user.password.breach") {
error_log("Sorry, we only handle breached password events.");
return;
}
$user_id = $obj->event->user->id;
if (!$user_id) {
error_log("No user id");
return;
}
$client = new FusionAuth\FusionAuthClient($api_key, $fa_url);
$response = $client->deactivateUser($user_id);
if (!$response->wasSuccessful()) {
// uh oh
error_log("Response wasn't successful:");
error_log(var_export($response, TRUE));
return;
}
http_response_code(500);
?>
Let’s walk through this code, line by line.
// ...
require __DIR__. '/config.php';
require __DIR__. '/vendor/autoload.php';
// ...
First, there are some required libraries and files.
//...
$headers = getallheaders();
if (!$headers) {
error_log("Invalid authorization header.");
return;
}
$authorization_value = $headers['Authorization'];
if ($authorization_value !== $authorization_header_value) {
error_log("Invalid authorization header value found: ".$authorization_value);
return;
}
//...
Then, the code checks the Authorization
header. This ensures that only FusionAuth calls this webhook. For production, you would definitely want to use TLS as well.
//...
$input = file_get_contents('php://input');
$obj = json_decode($input);
//...
In these lines, the entire contents of the webhook payload are converted into a string. The string is then decoded into a JSON object for easier handling.
//...
if (!$obj) {
error_log("Invalid JSON");
return;
}
type = $obj->event->type;
if ($type !== "user.password.breach") {
error_log("Sorry, we only handle breached password events.");
return;
}
$user_id = $obj->event->user->id;
if (!$user_id) {
error_log("No user id");
return;
}
//...
Next, validate the payload. If you don’t get valid JSON, a password breach event, and a user id, simply log an error and return. Doing so allows the webhook and the login event to succeed. Check for the event type just in case there’s a misconfiguration and our webhook is notified of other types of events.
//...
$client = new FusionAuth\FusionAuthClient($api_key, $fa_url);
$response = $client->deactivateUser($user_id);
if (!$response->wasSuccessful()) {
error_log("Response wasn't successful:");
error_log(var_export($response, TRUE));
return;
}
//...
Finally! This is where the action is. This code creates a new FusionAuth client. It then deactivates the user who logged in with a password found to be compromised. You could take other steps here, too.
You could do more within FusionAuth, by, for example:
- Adding a date of deactivation to the
user.data
field - Actioning the user for display in the administrative interface or to be queried via the API
- Putting the user in a group for future processing
You could also integrate with other systems.
- You could fire off an API call to another service that needs to know about this security violation.
- The system could add an event to a streaming service, such as Kafka, for future analysis.
- Your application could send an email to the user and their boss about the situation. Wouldn’t be cool, but you could do it.
//...
http_response_code(500);
//...
Finally, the code returns a 500
HTTP status code. This stops the login process. Because you configured this tenant to require all webhooks to succeed before processing the event, if any don’t, the event doesn’t complete. This means the user is not logged in.
If this webhook didn’t fail, the user would be logged in. The account would be deactivated; they’d be unable to login later. But their current session would be active for the duration of the token. We don’t want that to happen, so that’s why the code returns a 500
.
Results
If you install the webhook, follow the instructions in the repository, and login as a user with a breached password, the user will see this screen on their first failed login:
On their second login, they’ll see the normal “your account has been locked” error message:
In a production system, you’d typically customize or localize these messages. Themes allow you to do so.
This user will also be deactivated in the administrative user interface:
Conclusion
Webhooks allow you to extend FusionAuth in all kinds of interesting ways.
Whether you are pushing data to an external system or calling back into FusionAuth to take custom actions, you can leverage webhooks to make FusionAuth work the way you want.
Top comments (0)