# Keycloak extension

Add the step to your registration flow, then check on your dashboard that it works.

The web page with this text is https://signupbouncer.com/docs/keycloak. Link to the web page when you cite it.

## Install in Keycloak

SignupBouncer for Keycloak is a step for the registration form of Keycloak 26 that checks each new signup's email address and IP address before Keycloak creates the account. It's open source under the MIT license. It works on the Free plan, which includes 1,000 checks a month.

1. Sign up at [signupbouncer.com/signup](https://signupbouncer.com/signup), then create an API key on your app's Integration tab.
2. Download `keycloak-signupbouncer-0.1.0.jar` from [github.com/signupbouncer/signupbouncer-keycloak/releases/latest](https://github.com/signupbouncer/signupbouncer-keycloak/releases/latest) and copy it into the `providers` folder of your Keycloak 26 server. Run `bin/kc.sh build`, then restart Keycloak so it loads the step. A server you started with `start-dev` needs the restart only.
3. In the Keycloak admin console, open your realm's Authentication page and duplicate the `registration` flow.
4. In the copy, add a step to the "registration form" sub-flow. Choose "SignupBouncer signup check" and set it to Required.
5. Click the gear icon next to the step. Give its settings an alias and paste your API key. Leave "Refuse signups marked challenge" off, "Send the visitor's IP address" on and "App ID (older keys only)" empty, then save.
6. From the flow's Action menu, choose "Bind flow" and pick "Registration flow".

### Behind a proxy or load balancer

Keycloak sees the address of whatever connects to it. Behind a proxy, that's the proxy, so every signup reaches us with the proxy's IP address. Our IP checks and burst checks then treat all your signups as coming from the same address.

To pass on the visitor's address, make your proxy overwrite the `X-Forwarded-For` header with it. A proxy that adds to the header instead keeps whatever the visitor sent. Then start Keycloak with `--proxy-headers xforwarded` and `--proxy-trusted-addresses` set to your proxy's addresses. If your proxy sets the standard `Forwarded` header, use `--proxy-headers forwarded` instead.

If someone can reach Keycloak without going through the proxy, or the proxy adds to the header, the person signing up can choose the IP address we check. When you can't rule that out, turn off "Send the visitor's IP address" in the step's settings. We then check the email address only.

### Check that it works

If your realm's login page has no Register link, turn on User registration under Realm settings, Login.

Register with a test address. Keycloak creates the account, and the signup appears on your dashboard's Checks tab within seconds.

Your SignupBouncer app starts in shadow mode, so we log each signup and refuse nothing. When the Checks tab looks right, switch the app to enforce mode on the Rules tab.

If no signup appears, look for a line starting with `SignupBouncer:` in Keycloak's log. A wrong or revoked API key lets every signup through unchecked, and the line says so.

The label in the Result column says what the score marked the signup: Allow, Challenge or Block. A line under the label says when we let a signup marked Block or Challenge through. In shadow mode, a signup from mailinator.com shows Block with "Let through, Shadow" under it, and Keycloak creates the account.

A reason with a dashed border counts only on a higher plan, so it adds no points to your score.

On the Free plan, after 1,000 checks in a month, the Result column shows Not checked, with "Over quota" under it. Keycloak creates those accounts without a check until the month ends.

On a monthly paid plan, signups go through unchecked once you reach your cap on extra checks. On a yearly plan, they go through unchecked once you've used the allowance.

### What your users see

In enforce mode, we refuse a signup marked block. Keycloak shows your block message under the email field and creates no account. While you keep our default message, we word it for the reason, as in the [Supabase hook](https://signupbouncer.com/docs/supabase#block-message).

To use your own words in Keycloak, and translate them, open Realm settings, Localization, Realm overrides. Add the key `signupbouncerRefused` for each language. Your text replaces our message for every reason. Put `{0}` in it where you want our message to appear.

A challenge is a signup we find suspicious but not clear enough to refuse, such as the third spelling of the same inbox within 7 days. By default, a signup marked challenge goes through, and Keycloak still asks for email verification if you turned it on.

To refuse those signups too, turn on "Refuse signups marked challenge" in the step's settings. We still let through a challenge that comes from burst points, as the Supabase and Ory hooks do. The Challenges setting on our Rules tab applies to those hooks only.

### If SignupBouncer is down

The step waits up to 1.5 seconds for our answer. When the call fails or times out, the signup goes through unchecked and Keycloak logs why. A check we finished late still shows on your Checks tab.

If Keycloak reaches the internet through a proxy, the call to SignupBouncer goes through that proxy too. Keycloak's [outgoing HTTP guide](https://www.keycloak.org/server/outgoinghttp) has the details.

### Which signups we check

We check signups that come through Keycloak's registration form. Accounts that people create through an identity provider such as Google, or that an admin creates, skip the form, so we don't check them. If your form has no email field, we check the IP address alone, and Keycloak shows a refusal above the form.

Keycloak runs every step of the form each time someone sends it, even when another step finds a mistake. So a form sent back for a password typo and sent again uses two checks from your monthly allowance. On the Free plan, after 1,000 checks in a month, we let signups through unchecked until the month ends.

We tested the step with Keycloak 26.8.1. It also passed our tests on Keycloak 25.0.6, which has no `--proxy-trusted-addresses` option.

## Turn the step off

1. In the Keycloak admin console, open the Authentication page of each realm that uses SignupBouncer, then the flow whose "Used by" column says "Registration flow".
2. Set the SignupBouncer step's Requirement to Disabled. Keycloak skips a disabled step and runs the rest of your flow, so a CAPTCHA or any other step you added keeps working.
3. Register with a test address. Keycloak creates the account, and your Checks tab shows no new check.

To check signups again, open the flow whose "Used by" column says "Registration flow".

1. If that flow is the built-in `registration` flow, go back to the Authentication page and open the copy you made at install. From its Action menu, choose "Bind flow", pick "Registration flow" and click Save.
2. In the flow now marked "Registration flow", set the SignupBouncer step's Requirement to Required. If that flow has no SignupBouncer step, add one as you did at install.
3. Register with a test address. Your Checks tab shows a new check within seconds. If no check appears, look for a line starting with `SignupBouncer:` in Keycloak's log.

Keycloak never created accounts for the signups we refused, so turning the step off doesn't restore them. Those people have to register again.

### Remove the extension

Once the jar is gone, Keycloak can't show a registration form whose flow has a Required SignupBouncer step.

1. In each realm, open every flow on the Authentication page without a "Built-in" label.
2. In each flow, click the Delete icon in every SignupBouncer step's row, then Delete in the window that opens.
3. Delete `keycloak-signupbouncer-0.1.0.jar` from the `providers` folder, run `bin/kc.sh build` and restart Keycloak.

## Next

- [Shadow and enforce](https://signupbouncer.com/docs#modes): When your Checks tab looks right, you switch the app to enforce.
- [Reason codes](https://signupbouncer.com/docs/reasons): What each reason on your Checks tab means.
- [Check settings](https://signupbouncer.com/docs/rules#check-settings): You choose what each check does on your app.
