|
| 1 | +--- |
| 2 | +layout: blog-post |
| 3 | +title: ! 'Breached Password Detection: How to Lock User Accounts with a Webhook' |
| 4 | +description: Webhooks let you take actions based on events in FusionAuth. For instance, if you've detected that someone tried logging in with a compromised password, you can immediately lock their account. |
| 5 | +author: Dan Moore |
| 6 | +image: blogs/breached-password-webhook/breached-password-detection-how-to-lock-user-accounts-with-a-webhook.png |
| 7 | +category: blog |
| 8 | +tags: feature-breached-password-detection client-php feature-webhooks |
| 9 | +excerpt_separator: "<!--more-->" |
| 10 | +--- |
| 11 | + |
| 12 | +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? |
| 13 | + |
| 14 | +<!--more--> |
| 15 | + |
| 16 | +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. |
| 17 | + |
| 18 | +FusionAuth gives you options. You can integrate using [JWTs](/docs/v1/tech/oauth/tokens) or the [APIs](/docs/v1/tech/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. |
| 19 | + |
| 20 | +In this tutorial, we'll extend FusionAuth to lock an account if a user signs in with a compromised password. |
| 21 | + |
| 22 | +> 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](/docs/v1/tech/events-webhooks/events) for which FusionAuth can fire a webhook. |
| 23 | +
|
| 24 | +This is different from [locking an account](/docs/v1/tech/tutorials/setting-up-user-account-lockout) based on login attempts. In this case, you are relying on the [breached password detection feature](/blog/2020/07/22/breached-password-detection). |
| 25 | + |
| 26 | +*Note: breached password detection is a [paid edition feature](/pricing).* |
| 27 | + |
| 28 | +## Prerequisites |
| 29 | + |
| 30 | +* A modern PHP (tested with PHP 7.3) |
| 31 | +* FusionAuth installed (see the [5 minute setup guide](/docs/v1/tech/5-minute-setup-guide) if you don't have it) |
| 32 | +* A FusionAuth license |
| 33 | + |
| 34 | +If you'd like to jump ahead to the code, here's the [GitHub repo](https://github.com/FusionAuth/fusionauth-example-php-webhook) that you can download and explore. |
| 35 | + |
| 36 | +## Set up users, license, and API keys |
| 37 | + |
| 38 | +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. |
| 39 | + |
| 40 | +Next, [activate your license](/docs/v1/tech/reactor). |
| 41 | + |
| 42 | +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. |
| 43 | + |
| 44 | +## Configure breached password detection |
| 45 | + |
| 46 | +Navigate to the "Tenants" section and then to the default tenant. Go to the "Passwords" tab. Take the following steps: |
| 47 | + |
| 48 | +* Enable breached password detection. |
| 49 | +* Set the "On login" option to "Only record the result, take no action." |
| 50 | + |
| 51 | +Your settings should look like this: |
| 52 | + |
| 53 | +{% include _image.liquid src="/assets/img/blogs/breached-password-webhook/tenant-settings-password-tab.png" alt="Setting up breached password detection." class="img-fluid" figure=false %} |
| 54 | + |
| 55 | +## Configure tenant webhook settings |
| 56 | + |
| 57 | +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". |
| 58 | + |
| 59 | +{% include _image.liquid src="/assets/img/blogs/breached-password-webhook/tenant-settings-webhooks-tab.png" alt="Setting up webhook tenant settings." class="img-fluid" figure=false %} |
| 60 | + |
| 61 | +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. |
| 62 | + |
| 63 | +## Configure the webhook |
| 64 | + |
| 65 | +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](/blog/2020/06/30/private-labeling-with-multi-tenant), 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](/docs/v1/tech/events-webhooks/). |
| 66 | + |
| 67 | +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. |
| 68 | + |
| 69 | +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: |
| 70 | + |
| 71 | +{% include _image.liquid src="/assets/img/blogs/breached-password-webhook/webhook-settings-url.png" alt="The webhook configuration screen." class="img-fluid" figure=false %} |
| 72 | + |
| 73 | +{:style="margin-bottom:30px"} |
| 74 | +Scroll down and make sure the `user.password.breach` event is enabled: |
| 75 | + |
| 76 | +{% include _image.liquid src="/assets/img/blogs/breached-password-webhook/webhook-settings-event-choice.png" alt="Configuring the received webhook events." class="img-fluid" figure=false %} |
| 77 | + |
| 78 | +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](/docs/v1/tech/events-webhooks/securing). For this application, let's configure FusionAuth to send a header value when the webhook is called: |
| 79 | + |
| 80 | +{% include _image.liquid src="/assets/img/blogs/breached-password-webhook/webhook-settings-add-headers.png" alt="Configuring the webhook to receive an Authorization header." class="img-fluid" figure=false %} |
| 81 | + |
| 82 | +Finally, click the "Save" button, as you are done configuring the webhook. |
| 83 | + |
| 84 | +## Write the webhook code |
| 85 | + |
| 86 | +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](/docs/v1/tech/client-libraries/) or call the APIs directly. I suppose you could write the webhook in bash, IF YOU DARE. |
| 87 | + |
| 88 | +But we'll use PHP. |
| 89 | + |
| 90 | +The code is [available here](https://github.com/FusionAuth/fusionauth-example-php-webhook) if you want to check it out. Here's the `webhook.php` file, the heart of the example: |
| 91 | + |
| 92 | +```php |
| 93 | +<?php |
| 94 | +require __DIR__. '/config.php'; |
| 95 | +require __DIR__ . '/vendor/autoload.php'; |
| 96 | + |
| 97 | +$headers = getallheaders(); |
| 98 | +if (!$headers) { |
| 99 | + error_log("Invalid authorization header."); |
| 100 | + return; |
| 101 | +} |
| 102 | + |
| 103 | +$authorization_value = $headers['Authorization']; |
| 104 | +if ($authorization_value !== $authorization_header_value) { |
| 105 | + error_log("Invalid authorization header value found: ".$authorization_value); |
| 106 | + return; |
| 107 | +} |
| 108 | + |
| 109 | +$input = file_get_contents('php://input'); |
| 110 | + |
| 111 | +$obj = json_decode($input); |
| 112 | +if (!$obj) { |
| 113 | + error_log("Invalid JSON"); |
| 114 | + return; |
| 115 | +} |
| 116 | + |
| 117 | +$type = $obj->event->type; |
| 118 | +if ($type !== "user.password.breach") { |
| 119 | + error_log("Sorry, we only handle breached password events."); |
| 120 | + return; |
| 121 | +} |
| 122 | + |
| 123 | +$user_id = $obj->event->user->id; |
| 124 | +if (!$user_id) { |
| 125 | + error_log("No user id"); |
| 126 | + return; |
| 127 | +} |
| 128 | +$client = new FusionAuth\FusionAuthClient($api_key, $fa_url); |
| 129 | +$response = $client->deactivateUser($user_id); |
| 130 | +if (!$response->wasSuccessful()) { |
| 131 | + // uh oh |
| 132 | + error_log("Response wasn't successful:"); |
| 133 | + error_log(var_export($response, TRUE)); |
| 134 | + return; |
| 135 | +} |
| 136 | +http_response_code(500); |
| 137 | +?> |
| 138 | +``` |
| 139 | + |
| 140 | +Let's walk through this code, line by line. |
| 141 | + |
| 142 | +```php |
| 143 | +// ... |
| 144 | +require __DIR__. '/config.php'; |
| 145 | +require __DIR__ . '/vendor/autoload.php'; |
| 146 | +// ... |
| 147 | +``` |
| 148 | + |
| 149 | +First, there are some required libraries and files. |
| 150 | + |
| 151 | +```php |
| 152 | +//... |
| 153 | +$headers = getallheaders(); |
| 154 | +if (!$headers) { |
| 155 | + error_log("Invalid authorization header."); |
| 156 | + return; |
| 157 | +} |
| 158 | + |
| 159 | +$authorization_value = $headers['Authorization']; |
| 160 | +if ($authorization_value !== $authorization_header_value) { |
| 161 | + error_log("Invalid authorization header value found: ".$authorization_value); |
| 162 | + return; |
| 163 | +} |
| 164 | +//... |
| 165 | +``` |
| 166 | + |
| 167 | +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. |
| 168 | + |
| 169 | +```php |
| 170 | +//... |
| 171 | +$input = file_get_contents('php://input'); |
| 172 | + |
| 173 | +$obj = json_decode($input); |
| 174 | +//... |
| 175 | +``` |
| 176 | + |
| 177 | +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. |
| 178 | + |
| 179 | +```php |
| 180 | +//... |
| 181 | +if (!$obj) { |
| 182 | + error_log("Invalid JSON"); |
| 183 | + return; |
| 184 | +} |
| 185 | + |
| 186 | +type = $obj->event->type; |
| 187 | +if ($type !== "user.password.breach") { |
| 188 | + error_log("Sorry, we only handle breached password events."); |
| 189 | + return; |
| 190 | +} |
| 191 | + |
| 192 | +$user_id = $obj->event->user->id; |
| 193 | +if (!$user_id) { |
| 194 | + error_log("No user id"); |
| 195 | + return; |
| 196 | +} |
| 197 | +//... |
| 198 | +``` |
| 199 | + |
| 200 | +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. |
| 201 | + |
| 202 | +```php |
| 203 | +//... |
| 204 | +$client = new FusionAuth\FusionAuthClient($api_key, $fa_url); |
| 205 | +$response = $client->deactivateUser($user_id); |
| 206 | +if (!$response->wasSuccessful()) { |
| 207 | + error_log("Response wasn't successful:"); |
| 208 | + error_log(var_export($response, TRUE)); |
| 209 | + return; |
| 210 | +} |
| 211 | +//... |
| 212 | +``` |
| 213 | + |
| 214 | +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. |
| 215 | + |
| 216 | +You could do more within FusionAuth, by, for example: |
| 217 | + |
| 218 | +* Adding a date of deactivation to the `user.data` field |
| 219 | +* [Actioning the user](/docs/v1/tech/apis/actioning-users) for display in the administrative interface or to be queried via the API |
| 220 | +* Putting the user in a [group](/docs/v1/tech/core-concepts/groups) for future processing |
| 221 | + |
| 222 | +You could also integrate with other systems. |
| 223 | + |
| 224 | +* You could fire off an API call to another service that needs to know about this security violation. |
| 225 | +* The system could add an event to a streaming service, such as Kafka, for future analysis. |
| 226 | +* Your application could send an email to the user and their boss about the situation. Wouldn't be cool, but you could do it. |
| 227 | + |
| 228 | +```php |
| 229 | +//... |
| 230 | +http_response_code(500); |
| 231 | +//... |
| 232 | +``` |
| 233 | + |
| 234 | +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. |
| 235 | + |
| 236 | +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`. |
| 237 | + |
| 238 | +## Results |
| 239 | + |
| 240 | +If you install the webhook, follow the instructions in [the repository](https://github.com/FusionAuth/fusionauth-example-php-webhook/blob/master/README.md), and login as a user with a breached password, the user will see this screen on their first failed login: |
| 241 | + |
| 242 | +{% include _image.liquid src="/assets/img/blogs/breached-password-webhook/first-attempt-login-after-lock.png" alt="Login screen after first failed login attempt." class="img-fluid" figure=false %} |
| 243 | + |
| 244 | +On their second login, they'll see the normal "your account has been locked" error message: |
| 245 | + |
| 246 | +{% include _image.liquid src="/assets/img/blogs/breached-password-webhook/subsequent-attempts-login-after-lock.png" alt="Login screen after subsequent failed login attempts." class="img-fluid" figure=false %} |
| 247 | + |
| 248 | +In a production system, you'd typically customize or localize these messages. [Themes](/docs/v1/tech/themes/) allow you to do so. |
| 249 | + |
| 250 | +This user will also be deactivated in the administrative user interface: |
| 251 | + |
| 252 | +{% include _image.liquid src="/assets/img/blogs/breached-password-webhook/admin-view-user-locked.png" alt="Administrative user interface view of locked user." class="img-fluid" figure=false %} |
| 253 | + |
| 254 | +## Conclusion |
| 255 | + |
| 256 | +Webhooks allow you to extend FusionAuth in all kinds of interesting ways. |
| 257 | + |
| 258 | +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. |
| 259 | + |
0 commit comments