Skip to content
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
39 changes: 27 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ This produces three jars (each plugin lives in its own folder so it can be built
independently):

- `captcha/target/helpwave-captcha-<v>.jar`
- `privacy/target/helpwave-privacy-<v>.jar`
- `policy-acceptance/target/helpwave-policy-acceptance-<v>.jar`
- `picture/target/helpwave-picture-<v>.jar` (shaded with AWS SDK + Thumbnailator)

Drop all three (alongside the theme jar) into Keycloak's `providers/` directory and run
Expand Down Expand Up @@ -91,8 +91,9 @@ For nixos users, see [docs/nixos.md](docs/nixos.md) for nix-shell setup instruct
- Custom login, register, and forgot password pages
- Field-level validation matching hightide patterns
- **Cloudflare Turnstile** CAPTCHA on signup (`helpwave-turnstile` FormAction SPI)
- **Privacy policy** checkbox on signup with acceptance metadata stored on the user
(`helpwave-privacy` FormAction SPI)
- **Versioned policy consents** (privacy + future forms) via a reusable RequiredAction
SPI (`helpwave-policy-acceptance`). Bumping the version on the realm re-prompts every
user on next login; acceptance metadata is persisted on the user account.
- **Profile picture upload** with server-side scaling to multiple sizes and storage in any
S3-compatible bucket (`helpwave-picture` Realm Resource SPI)

Expand All @@ -106,28 +107,42 @@ The release workflow publishes the following jars on every version bump in `pack
|--------------------------------------------------|--------------------------------------------------|
| `keycloak-theme-for-kc-26.2-and-above.jar` | The login/account theme |
| `helpwave-captcha-<v>.jar` | Cloudflare Turnstile registration form action |
| `helpwave-privacy-<v>.jar` | Privacy acceptance form action + attribute store |
| `helpwave-policy-acceptance-<v>.jar` | Versioned policy consents (privacy + future) |
| `helpwave-picture-<v>.jar` | Profile picture REST endpoint + R2/S3 upload |

Copy all jars into Keycloak's `providers/` directory (or mount them into the container)
and run `kc.sh build` to rebuild the runtime, then start Keycloak normally.

### 1. Enable the Cloudflare Turnstile and Privacy form actions
### 1. Enable the Cloudflare Turnstile form action

1. Open the Keycloak admin console.
2. Go to **Authentication** → **Flows** and duplicate the built-in **registration** flow.
3. In your new copy, add two executions to the *registration form*:
3. In your new copy, add an execution to the *registration form*:
- `Cloudflare Turnstile (helpwave)` — set to **Required**
- `Privacy Policy Acceptance (helpwave)` — set to **Required**
4. Click the gear on each execution to configure it:
4. Click the gear on the execution to configure it:
- **Turnstile**: set the `Turnstile site key` (public) and `Turnstile secret` (private).
Get these from <https://dash.cloudflare.com/?to=/:account/turnstile>.
- **Privacy**: set the `Privacy policy URL` (defaults to `https://helpwave.de/privacy`)
and an optional `Privacy policy version` string. Both are persisted on the user as
`privacy_policy_accepted_at` and `privacy_policy_version` attributes.
5. Set this flow as the realm's **Registration flow** binding.

### 2. Configure the profile picture storage
### 2. Enable the policy-acceptance required actions

1. **Authentication** → **Required actions** → enable
**Privacy Policy Acceptance (helpwave)**.
2. **Realm settings** → **General** → **Attributes**: set
- `helpwave.policy.privacy.url` (defaults to `https://helpwave.de/privacy`)
- `helpwave.policy.privacy.version` (defaults to `2024-01`)
3. On first login after registration, users are prompted to accept the policy. Their
acceptance is stored on the user as `privacy_policy_accepted`,
`privacy_policy_accepted_at` and `privacy_policy_version`. Bumping the realm version
re-prompts every user on their next login.

Add more consent forms (e.g. terms of service, data-processing agreement) by adding a new
`AbstractPolicyAcceptanceRequiredActionFactory` subclass to
`keycloak-extensions/policy-acceptance/` and registering it in the `META-INF/services/`
file. The React `Terms.tsx` page renders the policy variant automatically when a
`policyId` attribute is set.

### 3. Configure the profile picture storage

The profile picture SPI accepts standard AWS S3 or Cloudflare R2 (any S3-compatible
backend). It exposes itself at:
Expand Down
69 changes: 48 additions & 21 deletions docs/deployment-nixos.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ Every release attaches the following artifacts to the GitHub release:
|--------------------------------------------|----------------------------------------|-----------------------------------------------------------|
| `keycloak-theme-for-kc-26.2-and-above.jar` | (root, built by Keycloakify) | Login + account theme `helpwave-id`. |
| `helpwave-captcha-<VER>.jar` | `captcha/` | `FormAction` SPI: Cloudflare Turnstile CAPTCHA on signup. |
| `helpwave-privacy-<VER>.jar` | `privacy/` | `FormAction` SPI: privacy checkbox + acceptance attrs. |
| `helpwave-policy-acceptance-<VER>.jar` | `policy-acceptance/` | `RequiredAction` SPI: versioned policy consents (privacy + future forms). |
| `helpwave-picture-<VER>.jar` | `picture/` | `RealmResourceProvider` SPI: avatar upload to S3 / R2. |

`<VER>` is the SPI Maven version (`keycloak-extensions/pom.xml`, currently `0.2.0`),
`<VER>` is the SPI Maven version (`keycloak-extensions/pom.xml`, currently `0.3.0`),
which is independent from the theme/npm version in `package.json`. All four jars go into
Keycloak's `providers/` directory — `services.keycloak.plugins` does that for you.

Expand All @@ -28,8 +28,8 @@ Keycloak's `providers/` directory — `services.keycloak.plugins` does that for
let
domain = "id.helpwave.de";

themeVersion = "0.5.0"; # ⇄ package.json version → release tag v0.5.0
spiVersion = "0.2.0"; # ⇄ keycloak-extensions/pom.xml
themeVersion = "0.6.0"; # ⇄ package.json version → release tag v0.6.0
spiVersion = "0.3.0"; # ⇄ keycloak-extensions/pom.xml

release = file: sha:
pkgs.fetchurl {
Expand All @@ -42,7 +42,7 @@ let
"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
captchaPlugin = release "helpwave-captcha-${spiVersion}.jar"
"sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
privacyPlugin = release "helpwave-privacy-${spiVersion}.jar"
policyPlugin = release "helpwave-policy-acceptance-${spiVersion}.jar"
"sha256-CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=";
picturePlugin = release "helpwave-picture-${spiVersion}.jar"
"sha256-DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=";
Expand Down Expand Up @@ -72,7 +72,7 @@ in
plugins = [
themePlugin
captchaPlugin
privacyPlugin
policyPlugin
picturePlugin
];

Expand Down Expand Up @@ -125,15 +125,34 @@ in
> Or just run `nixos-rebuild switch` once with all four `sha256-AAA…` placeholders, copy
> each `got: sha256-…` line out of the error message, and paste it back.

## 3. Wiring the auth flow (Turnstile + privacy)
## 3. Wiring the auth flow (Turnstile + policy consents)

The captcha and privacy SPIs plug into the *registration form* flow as `FormAction`s.
Their config is **not** read from `keycloak.conf` — it's per-flow so different realms
can have different keys.
### Turnstile (registration FormAction)

Turnstile plugs into the *registration form* flow as a `FormAction`. Its config is **not**
read from `keycloak.conf` — it's per-flow so different realms can have different keys.

### Policy acceptance (RequiredAction)

The privacy consent (and any future policy form) is a `RequiredAction`, not a registration
form field. The provider auto-evaluates on every login: if the user's stored policy
version is missing or differs from the version configured on the realm, the user is
prompted to re-accept before the login completes. Bumping a policy version is therefore a
one-line realm-attribute change — no migration, no flow surgery.

User attributes the provider writes on success:

```
<id>_policy_accepted = "true"
<id>_policy_accepted_at = ISO-8601 timestamp
<id>_policy_version = the version the user accepted
```

…where `<id>` is the policy id (`privacy` for now).

### Option A — Realm export (preferred, declarative)

Ship the flow as part of a realm JSON and load it via
Ship the flow + required-action enablement as part of a realm JSON and load it via
`services.keycloak.realmFiles = [ ./customer-realm.json ];`. The interesting bits:

```json
Expand All @@ -160,9 +179,7 @@ Ship the flow as part of a realm JSON and load it via
{ "authenticator": "registration-user-creation", "requirement": "REQUIRED" },
{ "authenticator": "registration-password-action", "requirement": "REQUIRED" },
{ "authenticator": "helpwave-turnstile", "requirement": "REQUIRED",
"authenticatorConfig": "turnstile-config" },
{ "authenticator": "helpwave-privacy-acceptance", "requirement": "REQUIRED",
"authenticatorConfig": "privacy-config" }
"authenticatorConfig": "turnstile-config" }
]
}
],
Expand All @@ -173,15 +190,21 @@ Ship the flow as part of a realm JSON and load it via
"turnstile.site.key": "0x4AAAAAAAxxxxxxxxxxxxxxxx",
"turnstile.secret": "$${env.TURNSTILE_SECRET}"
}
},
}
],
"requiredActions": [
{
"alias": "privacy-config",
"config": {
"privacy.policy.url": "https://helpwave.de/privacy",
"privacy.policy.version": "2024-01"
}
"alias": "helpwave-privacy-acceptance",
"name": "Privacy Policy Acceptance (helpwave)",
"providerId": "helpwave-privacy-acceptance",
"enabled": true,
"defaultAction": false
}
],
"attributes": {
"helpwave.policy.privacy.url": "https://helpwave.de/privacy",
"helpwave.policy.privacy.version": "2024-01"
},
"registrationFlow": "registration-helpwave"
}
```
Expand All @@ -192,7 +215,11 @@ disk in cleartext or in the Nix store.

### Option B — Click through the admin console

Same steps as the manual setup section in [README.md](../README.md#1-enable-the-cloudflare-turnstile-and-privacy-form-actions).
1. `Authentication → Required actions →` enable **Privacy Policy Acceptance (helpwave)**.
2. `Realm settings → General → Attributes` (or via the admin API): set
`helpwave.policy.privacy.url` and `helpwave.policy.privacy.version`.

See also the manual setup section in [README.md](../README.md#1-enable-the-cloudflare-turnstile-and-privacy-form-actions).

## 4. Example `.env` (for local dev with `docker compose`)

Expand Down
2 changes: 1 addition & 1 deletion keycloak-extensions/captcha/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>de.helpwave.keycloak</groupId>
<artifactId>helpwave-keycloak-extensions</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</parent>

<artifactId>helpwave-captcha</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion keycloak-extensions/picture/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>de.helpwave.keycloak</groupId>
<artifactId>helpwave-keycloak-extensions</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</parent>

<artifactId>helpwave-picture</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
<parent>
<groupId>de.helpwave.keycloak</groupId>
<artifactId>helpwave-keycloak-extensions</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</parent>

<artifactId>helpwave-privacy</artifactId>
<artifactId>helpwave-policy-acceptance</artifactId>
<packaging>jar</packaging>
<name>helpwave Privacy Acceptance Form Action</name>
<name>helpwave Policy Acceptance (Required Action)</name>
<description>
Reusable Keycloak Required Action SPI that asks the user to accept a versioned
policy document (privacy, terms, data processing, ...). Stores acceptance metadata
on the user account and re-prompts when the version configured on the realm changes.
</description>

<dependencies>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package de.helpwave.keycloak.policy;

import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;

/**
* Base factory for a single concrete policy. Add a new consent by extending this class,
* defining a {@link PolicyDefinition} constant, and registering the subclass in
* {@code META-INF/services/org.keycloak.authentication.RequiredActionFactory}.
*/
public abstract class AbstractPolicyAcceptanceRequiredActionFactory implements RequiredActionFactory {

protected abstract PolicyDefinition policy();

@Override
public RequiredActionProvider create(KeycloakSession session) {
return new PolicyAcceptanceRequiredAction(policy());
}

@Override
public String getId() {
return policy().providerId();
}

@Override
public String getDisplayText() {
return policy().displayText();
}

@Override
public boolean isOneTimeAction() {
return false;
}

@Override
public void init(Config.Scope config) { }

@Override
public void postInit(KeycloakSessionFactory factory) { }

@Override
public void close() { }
}
Loading
Loading