-
Notifications
You must be signed in to change notification settings - Fork 4
✨ Keycloak/Admin Portal: Encrypted voter attributes for outputs, exports and login #3143
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| --- | ||
| id: admin_portal_tutorials_multi_attribute_password_login | ||
| title: Logging In Without a Username (Attribute + Password) | ||
| title: Logging In Without a Username (Attribute + Password or Secret) | ||
| --- | ||
|
|
||
| <!-- | ||
|
|
@@ -14,11 +14,13 @@ By default, voters log in with a username and password. Some elections instead i | |
| attributes they already know - a date of birth, a national ID - without asking them to remember a | ||
| separate username. This tutorial explains how to configure the **Multi-Attribute + Password Form** | ||
| authenticator so voters log in with one or more configured user attributes plus a password, | ||
| instead of a username. | ||
| instead of a username. The password box can verify either the normal Keycloak password (the | ||
| default) or an explicitly configured encrypted voter attribute. | ||
|
|
||
| The authenticator finds every user whose configured attribute(s) match the submitted value(s) - | ||
| **all** configured attributes must match the same user - then checks the submitted password | ||
| against that candidate. Login succeeds only when **exactly one** candidate's password matches. | ||
| against those candidates. With the default `REJECT_AMBIGUOUS` match policy, login succeeds only | ||
| when **exactly one** candidate's credential matches and that account is enabled and not locked out. | ||
|
|
||
| A single attribute like date of birth is not unique on its own (many voters share a birth date). | ||
| This still works: the authenticator collects every user with that birth date as candidates and | ||
|
|
@@ -30,6 +32,65 @@ second identifying attribute is available. | |
|
|
||
| ## Prerequisites | ||
|
|
||
| ### Optional: use an encrypted voter attribute as the credential | ||
|
|
||
| The default **Credential verification policy** is `PASSWORD`; existing flows do not change. | ||
| To authenticate with a secret voter field instead of a Keycloak password: | ||
|
|
||
| 1. Configure a custom User Profile attribute, such as `mysecret`, with `sequent.secret=true`, | ||
| then populate it using the authorized voter editor or CSV importer. Do not enter plaintext | ||
| directly into Keycloak's attribute editor. | ||
| 2. Provision the same 64-character hex master key used by Harvest/Windmill in Keycloak's | ||
| `MASTER_SECRET` environment variable. The supplied Compose configurations pass the single | ||
| `MASTER_SECRET` deployment variable to all three services. If Harvest/Windmill use an external | ||
| vault, inject that vault's existing master key into Keycloak, not a newly generated key. | ||
| Recreate Keycloak after provisioning the environment variable. Receiving the key does not | ||
| enable encrypted-attribute login by itself; ordinary `PASSWORD` verification remains the default. | ||
| 3. On this authenticator's configuration, set **Credential verification policy** to | ||
| `SECRET_ATTRIBUTE` and **Encrypted credential attribute** to `mysecret`. Keep ordinary | ||
| identifying attributes in **User attributes to match**, not the secret attribute. | ||
| 4. Keep **Multiple-candidate match policy** at `REJECT_AMBIGUOUS`. `FIRST_MATCH` is rejected in | ||
| this mode. For IVR, set the same two options on **Multi-Attribute + Password Direct Grant**; | ||
| its existing single `kind=secret`/`maps_to=password` input carries this credential. | ||
|
|
||
| For example, to ask for a date of birth and use `mysecret` as the password: | ||
|
|
||
| | Authenticator setting | Value | | ||
| |---|---| | ||
| | User attributes to match | `dateOfBirth` | | ||
| | Credential verification policy | `SECRET_ATTRIBUTE` | | ||
| | Encrypted credential attribute | `mysecret` | | ||
| | Multiple-candidate match policy | `REJECT_AMBIGUOUS` | | ||
|
|
||
| Use the exact attribute **name**, not its display label. The secret is the credential, not a lookup | ||
| attribute: do not add `mysecret` to **User attributes to match**. The | ||
| [secret-attribute setup guide](./99-admin_portal_tutorials_add-user-attributes-to-keycloak.md#protecting-a-voter-attribute-as-secret) | ||
| explains the required annotation, permissions and validator restrictions. | ||
|
|
||
| The form remains `login.ftl`, and voters enter the credential in the existing `password` field. | ||
| Credential placement, masking/show-hide, autofocus, and `credential-input-policy: "pattern"` keep | ||
| their existing behavior. Pattern formatting is presentation: store the exact submitted credential | ||
| (for digit patterns, the digits, not visual separators). Do not add a second input. A Keycloak | ||
| password is not required or consulted in this mode; password-reset and password-policy operations | ||
| still manage the separate Keycloak password and do not replace this encrypted attribute. | ||
|
|
||
| Verification decrypts only the selected attribute of bounded candidates, binds each envelope to | ||
| the tenant/event/voter/attribute, compares fixed-size SHA-256 digests in constant time, and examines | ||
| all stored values. It does not trim or change credential case. Missing, malformed or tampered | ||
| envelopes, missing/wrong keys, disabled/locked accounts and ambiguous matches fail closed without | ||
| password fallback. At most 100 stored values per attribute and 150 UTF-8 bytes per submitted | ||
| credential are accepted. Existing tuple throttling and Keycloak account brute-force protection | ||
| remain active; guessed credentials are never included in the tuple throttle key. | ||
|
|
||
| Treat this field as a login credential: anyone authorized to reveal or export it can impersonate | ||
| its voter. Choose high-entropy values, restrict secret-read/write access, enable realm brute-force | ||
| protection, and avoid sending it in report previews, logs or untrusted message transports. | ||
| Email/SMS delivery remains readable; declaring a secret does not encrypt the message. The explicit | ||
| [Console test transport](../02-reference/user-manual/templates/admin_portal_reference_user-manual_templates.md#test-only-console-delivery) | ||
| also prints decrypted values, so use only synthetic credentials when that transport is enabled. | ||
|
|
||
| ### Standard prerequisites | ||
|
|
||
| - Access to the Keycloak Admin Console. | ||
| - The `sequent.message-otp-authenticator.jar` extension deployed in Keycloak's `providers/` | ||
| directory (included in the Sequent Keycloak Docker image by default). | ||
|
|
@@ -84,7 +145,7 @@ second identifying attribute is available. | |
| 7. Leave **Existing browser session policy** at `KEEP` for normal login flows. For a shared-device | ||
| flow where each visit must allow a different user to sign in, select `TERMINATE_BEFORE_LOGIN`. | ||
| Opening that login form then ends the user session attached to the browser before credentials | ||
| are submitted. | ||
| are submitted. This policy works with both `PASSWORD` and `SECRET_ATTRIBUTE` credential verification. | ||
| 8. Click **Save**. | ||
|
|
||
| --- | ||
|
|
@@ -127,23 +188,25 @@ and explains where realm attributes are edited. | |
| | No user matches all configured attributes | Generic "invalid credentials" error. | | ||
| | Exactly one candidate, correct password | Login succeeds. | | ||
| | Exactly one candidate, wrong password | Generic "invalid credentials" error - this attempt **is** counted toward that account's Brute Force Detection lockout, same as a standard login. | | ||
| | Exactly one candidate, currently locked out by Brute Force Detection | "Account temporarily/permanently disabled" - no password check is even attempted. | | ||
| | Exactly one candidate, currently locked out by Brute Force Detection | Generic "invalid credentials" error; the locked account cannot log in. | | ||
| | Multiple candidates share the configured attribute(s), and the password matches exactly one | Login succeeds as that user. | | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Correct the multi-candidate success condition. In 🤖 Prompt for AI Agents |
||
| | Multiple candidates match the password (or none do) | Generic "invalid credentials" error - see the brute-force note below. | | ||
|
|
||
| The error is always the same generic message regardless of cause, so a failed attempt never | ||
| reveals which attribute, or the password, was wrong. Every "no match" outcome above (blank field, | ||
| no candidates, wrong password) takes the same time to respond, including a real password-hash | ||
| computation on paths that never actually found a candidate to check - so response time doesn't | ||
| reveal whether any account has the submitted attribute value. | ||
| Authentication failures use a generic message rather than identifying the incorrect attribute or | ||
| credential. Dummy password-hash work on early rejection paths helps reduce timing differences; | ||
| it does **not** guarantee identical response times for an entire login request. In | ||
| `SECRET_ATTRIBUTE` mode, fixed-size digest comparison is constant-time, while database lookup, | ||
| the number of candidates and decryption still contribute to total request time. | ||
|
|
||
| > **Note on brute-force protection:** Keycloak's built-in per-account lockout only engages once | ||
| > resolution narrows to a single candidate - the same account that ends up locked out is also the | ||
| > one whose failed attempts get counted, matching how the standard username/password form behaves. | ||
| > When more than one candidate still shares the configured attribute(s), there is no single | ||
| > account a failed attempt can honestly be attributed to, so the counter can't engage for that | ||
| > specific request (a locked-out account among several ambiguous candidates still can't have its | ||
| > password probed, though - it's excluded from consideration before any password is checked). | ||
| > specific request. In `PASSWORD` mode, locked accounts are excluded before password verification. | ||
| > In `SECRET_ATTRIBUTE` mode, enabled but locked accounts still participate in ambiguity detection: | ||
| > locking one account must not make a shared secret authenticate as another voter. A locked | ||
| > account itself can never authenticate. | ||
| > Configuring more attributes narrows the candidate set before the password check, making the | ||
| > single-candidate (fully protected) case the common one; keep **Brute Force Detection** enabled | ||
| > at the realm level regardless. | ||
|
|
@@ -200,6 +263,9 @@ touching hash strength. | |
| When more than one candidate shares the configured attribute value(s) (e.g. several voters born on | ||
| the same date), **Multiple-candidate match policy** governs how the submitted password picks one: | ||
|
|
||
| `SECRET_ATTRIBUTE` mode requires `REJECT_AMBIGUOUS`; the `FIRST_MATCH` option below applies only | ||
| to ordinary `PASSWORD` verification. | ||
|
|
||
| - **`REJECT_AMBIGUOUS`** (default): checks every candidate's password. Only succeeds if the | ||
| submitted password matches **exactly one** of them; if it matches more than one, the request | ||
| fails generically, the same as if none had matched. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🤖 get_repo_knowledge executed:
get_repo_knowledge sequentech/step /tmp/coderabbit-repo-knowledge/sequentech-step-58f97436/conventionsLength of output: 2570
🏁 Script executed:
Repository: sequentech/step
Length of output: 45304
🏁 Script executed:
Repository: sequentech/step
Length of output: 21150
🌐 Web query:
site:keycloak.org/docs/24.0.5 server importing realm --import-realm existing realm💡 Result:
Keycloak supports importing realms at startup using the --import-realm option [1]. To import a realm file, you can place the realm configuration file (typically in JSON format) into the data/import directory of your Keycloak distribution. When you start the server, Keycloak will automatically detect and import the realms found in that directory [1]. If you are attempting to overwrite an existing realm, be aware that the standard startup import process is primarily intended for initial provisioning. For managing existing realms or updating their configurations, it is often recommended to use the Admin REST API or the Admin Console [2][3]. The Admin REST API allows for programmatic updates, imports, and exports of realm configurations [4][5]. For detailed usage and to verify the specific configuration for your version, you can run the following command to view the help documentation: bin/kc.[sh|bat] start --help
Citations:
🌐 Web query:
site:keycloak.org/docs/26.6.1/server/server-configuring-import-export "import-realm" existing realm💡 Result:
Keycloak’s
--import-realmdoes not overwrite an existing realm. If the realm already exists, the startup import is skipped. Use it only when creating a realm on a fresh database.For updating an existing realm, use the Admin REST API/CLI or delete the realm first, then restart with
--import-realm. In Keycloak 26.6.1, a notable exception is themasterrealm: it can be imported when it does not yet exist. [1][2][1] (keycloak.org)
[2] (forum.keycloak.org)
Citations:
🏁 Script executed:
Repository: sequentech/step
Length of output: 50372
Add an idempotent Keycloak realm migration.
Keycloak 26.6.1 skips
--import-realmwhen the tenant realm already exists. The compose services only start Keycloak with this option, so existing tenants will not receivevoter-secret-attribute-read,voter-secret-attribute-write, or theiradmingroup assignments. Add or document an Admin REST API or CLI migration before relying on these definitions for upgrades.🤖 Prompt for AI Agents