Skip to content

Add breached webhook blog post #171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DocsDevREADME.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Here are some guidelines to follow when writing documentation.
For blog posts:
- Indent all code with two spaces per level.
- Blog post headers should have only the first word and any proper nouns are capitalized.
- For site navigation, use double quotes: Navigate to "Tenants" and then to the "Password" tab.

For lists:
- Capitalize the first word.
Expand Down
4 changes: 4 additions & 0 deletions site/_data/exampleapps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,7 @@
name: django
description: Social signin with django and FusionAuth
language: python
- url: https://github.com/FusionAuth/fusionauth-example-php-webhook
name: Webhooks
description: Handling a breached password detection webhook with PHP
language: php
259 changes: 259 additions & 0 deletions site/_posts/2020-08-07-locking-an-account-with-breached-password.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
---
layout: blog-post
title: ! 'Breached Password Detection: How to Lock User Accounts with a Webhook'
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.
author: Dan Moore
image: blogs/breached-password-webhook/breached-password-detection-how-to-lock-user-accounts-with-a-webhook.png
category: blog
tags: feature-breached-password-detection client-php feature-webhooks
excerpt_separator: "<!--more-->"
---

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?

<!--more-->

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](/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.

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](/docs/v1/tech/events-webhooks/events) for which FusionAuth can fire a webhook.

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).

*Note: breached password detection is a [paid edition feature](/pricing).*

## Prerequisites

* A modern PHP (tested with PHP 7.3)
* FusionAuth installed (see the [5 minute setup guide](/docs/v1/tech/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](https://github.com/FusionAuth/fusionauth-example-php-webhook) 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](/docs/v1/tech/reactor).

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:

{% 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 %}

## 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".

{% 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 %}

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](/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/).

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:

{% include _image.liquid src="/assets/img/blogs/breached-password-webhook/webhook-settings-url.png" alt="The webhook configuration screen." class="img-fluid" figure=false %}

{:style="margin-bottom:30px"}
Scroll down and make sure the `user.password.breach` event is enabled:

{% 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 %}

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:

{% 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 %}

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](/docs/v1/tech/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](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:

```php
<?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.

```php
// ...
require __DIR__. '/config.php';
require __DIR__ . '/vendor/autoload.php';
// ...
```

First, there are some required libraries and files.

```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;
}
//...
```

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.

```php
//...
$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.

```php
//...
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.

```php
//...
$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](/docs/v1/tech/apis/actioning-users) for display in the administrative interface or to be queried via the API
* Putting the user in a [group](/docs/v1/tech/core-concepts/groups) 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.

```php
//...
http_response_code(500);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an aside, we do actually create an error event log when a webhook fails. So if you the webhook wanted to return JSON in a response body it would get logged in the event log for record.

I may open an issue to better define expected return status codes. It would be kind of nice to allow the webhook to return a status code that says fail the request, but don't log an error. So maybe we could document that 500 is an error on your end, and a 4xx is a fail, but only log it if debug is enabled for the webhook.

FusionAuth/fusionauth-issues#814

//...
```

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](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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this message is correct... assuming this is the error you're seeing when the webhook fails? This may be a bug. I can take a look.

You don't need to hold up the blog, we can always revise with a new screenshot if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, that is the error I'm seeing. Would you like me to file a bug?

Copy link
Member

@robotdan robotdan Aug 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just checked , we have a test for this, so not sure how this is happening. You should see something like One or more webhooks returned an invalid response or were unreachable. Based on your transaction configuration, your action cannot be completed - and that message is theme-able.

I can try click testing it as well, perhaps the test is missing some edge case. Feel free to open a bug.


{% 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 %}

On their second login, they'll see the normal "your account has been locked" error message:

{% 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 %}

In a production system, you'd typically customize or localize these messages. [Themes](/docs/v1/tech/themes/) allow you to do so.

This user will also be deactivated in the administrative user interface:

{% 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 %}

## 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.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion site/docs/v1/tech/events-webhooks/writing-a-webhook.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Additional headers may be added to the request by adding headers to the Webhook

=== Responses

Your Webhook must handle the RESTful request described above and send back an appropriate status code. Your Webhook must send back to FusionAuth an HTTP response code that indicates whether or not the event was successfully handled or not. If your Webhook handled the event properly, it must send back an HTTP response status code of `200`. If there was any type of error or failure, your Webhook must send back an HTTP response status code of `500`.
Your Webhook must handle the RESTful request described above and send back an appropriate status code. Your Webhook must send back to FusionAuth an HTTP response code that indicates whether or not the event was successfully handled or not. If your Webhook handled the event properly, it must send back an HTTP response status code of `2xx`. If there was any type of error or failure, your Webhook must send back a non `2xx` HTTP response status.

=== Configuration

Expand Down