✨ Prefill Keycloak login and registration fields from voting links - #2905
Open
edulix wants to merge 14 commits into
Open
✨ Prefill Keycloak login and registration fields from voting links#2905edulix wants to merge 14 commits into
edulix wants to merge 14 commits into
Conversation
|
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an end-to-end “login hint” prefill capability across Voting Portal, Keycloak extensions, notification templating, tests, and operator/developer documentation, enabling bounded login_hint__<field> parameters in voting links to prefill login/registration fields without weakening OIDC/PKCE guarantees.
Changes:
- Voting Portal: parse/validate/strip
login_hint__*from/loginand/enrollURLs, keep validated hints in-memory, and forward them viakeycloak-jsURLs (while mirroringusernameto OIDClogin_hint). - Keycloak extensions: add stock registration prefill action and an opt-in deferred registration prefill policy (
IGNORE/ACCEPT), applying hints only on initial GET render and only to managed writable attributes. - Notifications & docs/tests: expose dynamic user attributes to templates, add
url_encodehelper, expand unit + Nightwatch matrix coverage, and document rollout/migration steps.
Reviewed changes
Copilot reviewed 26 out of 28 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/yarn.lock | Locks @swc/jest resolution for Voting Portal Jest/SWC setup. |
| packages/windmill/src/tasks/send_template.rs | Exposes dynamic Keycloak attributes to templates and reduces audit/log PII footprint; adds unit tests. |
| packages/windmill/src/services/providers/sms_sender.rs | Stops logging SMS receiver/message content via tracing instrumentation. |
| packages/windmill/src/services/providers/email_sender.rs | Stops logging email recipients/body content via tracing instrumentation. |
| packages/voting-portal/test/readme.md | Documents env vars and how to run the login-hint Nightwatch matrix. |
| packages/voting-portal/test/e2e/login_hints.test.ts | Adds Nightwatch matrix for /login and /enroll hint-prefill scenarios. |
| packages/voting-portal/src/utils/loginHints.ts | Adds bounded parser/sanitizer and URL builder for login_hint__* parameters. |
| packages/voting-portal/src/utils/loginHints.test.ts | Adds unit tests for parsing/validation/encoding/scrubbing behavior. |
| packages/voting-portal/src/translations/en.ts | Adds user-facing “invalid voting link” messaging for invalid hint sets. |
| packages/voting-portal/src/services/VotingPortalError.ts | Adds INVALID_LOGIN_HINT_PARAMETERS error type. |
| packages/voting-portal/src/routes/ErrorPage.tsx | Renders a dedicated error page section for invalid hint parameters. |
| packages/voting-portal/src/providers/AuthContextProvider.tsx | Forwards validated hints through keycloak-js redirect URL construction without replacing OIDC internals. |
| packages/voting-portal/src/App.tsx | Captures/validates hints on initial route load, strips them from history, and passes them into auth provider state. |
| packages/voting-portal/package.json | Adds a test script and @swc/jest dev dependency for Jest execution. |
| packages/voting-portal/jest.config.cjs | Introduces SWC-backed Jest config for TS tests in Voting Portal. |
| packages/sequent-core/src/services/reports.rs | Adds shared Handlebars url_encode helper; reduces helper error logging verbosity; adds helper tests. |
| packages/sequent-core/Cargo.toml | Adds optional urlencoding dependency gated under reports feature. |
| packages/keycloak-extensions/voter-enrollment/src/test/java/sequent/keycloak/voter_enrollment/LoginHintPrefillTest.java | Adds unit tests for stock registration prefill behavior and filtering rules. |
| packages/keycloak-extensions/voter-enrollment/src/test/java/sequent/keycloak/voter_enrollment/DeferredRegistrationUserCreationTest.java | Adds coverage for deferred prefill policy default/behavior. |
| packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LoginHintRegistrationPrefill.java | Adds stock registration FormAction to prefill managed writable registration fields from bounded hints. |
| packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LoginHintPrefill.java | Adds bounded extraction + filtering logic for login-hint client notes. |
| packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/DeferredRegistrationUserCreation.java | Adds IGNORE/ACCEPT prefill policy and prefill application on initial GET only. |
| packages/keycloak-extensions/voter-enrollment/pom.xml | Adds Mockito dependency needed for new tests. |
| packages/keycloak-extensions/conditional-authenticators/src/test/java/sequent/keycloak/conditional_authenticators/RedirectToRegisterAuthenticatorTest.java | Adds regression test ensuring redirect-to-registration preserves auth session + client notes. |
| packages/Cargo.lock | Locks urlencoding crate in workspace dependency graph. |
| docs/docusaurus/docs/08-releases/08-release-9.5/release-9.5.0.md | Adds release notes and migration/rollout guidance for login-hint prefill and template usage. |
| docs/docusaurus/docs/07-developers/08-windmill/developers_windmill.md | Documents login-hint flow, Keycloak configuration, and trust boundary for operators/developers. |
| docs/docusaurus/docs/02-election_managers/02-reference/02-election-event/13-02-election_management_election-event_templates.md | Documents template variables, dynamic attributes, and url_encode usage for prefilled links. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+63
to
+68
| const url = new URL(configuredUrl) | ||
| if (!url.pathname.endsWith(`/${scenario.route}`)) { | ||
| throw new Error( | ||
| `${scenario.environmentVariable} must be a Voting Portal /${scenario.route} URL` | ||
| ) | ||
| } |
Comment on lines
+139
to
+146
| new LoginHintRegistrationPrefill().buildPage(context, form); | ||
|
|
||
| verify(form).setFormData(ArgumentMatchers.<MultivaluedMap<String, String>>any()); | ||
|
|
||
| when(request.getHttpMethod()).thenReturn("POST"); | ||
| new LoginHintRegistrationPrefill().buildPage(context, form); | ||
|
|
||
| verify(form).setFormData(ArgumentMatchers.<MultivaluedMap<String, String>>any()); |
Three independent defects each stopped `login_hint__*` parameters from
prefilling the Keycloak username field.
`parseLoginHints` iterated `URLSearchParams.entries()` and `Map.keys()`
with `for ... of`. The portal compiles with `target: "es5"` and no
`downlevelIteration`, so both loops became indexed loops over an iterator
whose `length` is `undefined`, and never ran a single iteration. Hints
were therefore always empty, so the portal took the plain login path and
left the hints in the redirect URI. The unit tests did not catch this
because `@swc/jest` builds them for `es2022`.
`createLoginUrl` and `createRegisterUrl` return `Promise<string>` in
keycloak-js 26, and were passed unawaited to `appendLoginHints`, which
threw `Invalid URL` and aborted the redirect. TypeScript reports this,
but the webpack build runs `ts-loader` with `transpileOnly`.
The voting portal login template dropped stock Keycloak's
`value="${(login.username!'')}"`, so the username field rendered empty
even when Keycloak had resolved the hint.
Registration fields now decide how they accept a prefilled value through the `loginHintPrefillPolicy` user profile annotation: `EDITABLE` (the default when unannotated), `READ_ONLY` or `IGNORE`. An unrecognised value falls back to `IGNORE`, so a typo never prefills a field. `READ_ONLY` renders the field read-only and rejects a registration whose submitted value differs from the hint, since the rendered attribute is only a browser affordance. The redisplayed form restores the hint value, otherwise the voter is left with a rejected value in a field they can no longer edit. Text inputs use `readonly` rather than `disabled` so the value is still submitted; selects, radios and checkboxes are disabled and mirror their value in a hidden input. Both prefill paths share the same resolution, so the annotation applies to stock and deferred registration alike.
The login page is not rendered from the user profile, so login.ftl cannot read the loginHintPrefillPolicy attribute annotation: neither the profile bean nor the authentication session client notes are in its data model. Render a prefilled username read-only when the realm attribute loginHintUsernamePolicy is READ_ONLY, defaulting to EDITABLE. A username restored by remember me stays editable so the voter can still sign in as somebody else. This is a presentation-only lock. Keycloak still authenticates whichever username is submitted, which is the same guarantee as an unprefilled login page: the password decides which account is entered. Registration stores the value, so READ_ONLY stays enforced on the server there.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parent issue: https://github.com/sequentech/meta/issues/12617
Description
Add bounded
login_hint__<field>parameters to Voting Portal login and enrollment links, securely forward them through Keycloak, prefill writable registration fields, and expose URL-safe dynamic voter attributes to notification templates.This is a draft. Implementation is proceeding one parent-issue checklist item per validated commit.
Tasks
keycloak-jswithout weakening OIDC state or PKCE handling.IGNORE/ACCEPTpolicy.url_encode.Architecture And Security Decisions
login_hint__<field>to avoid collisions with Voting Portal controls and OIDC parameters. Mirror onlylogin_hint__usernameto standard OIDClogin_hintfor stock login forms.Implementation Log
2026-07-23: Voting Portal hint contract
ui-essentials/asset-size warnings and no errors.2026-07-23: Keycloak JS forwarding
/loginor/enrollroute, keep them in memory, and replace the visible route before constructing the Keycloak redirect. Provider state is cleared when another route or event is configured so hints cannot leak across elections.createLoginUrlandcreateRegisterUrlsokeycloak-jscontinues to own state, nonce, callback storage, redirect URI, locale, and PKCE. Mirror username to standardlogin_hint, then append the validated namespace throughURLSearchParams.2026-07-23: Stock Keycloak registration prefill
FormActionthat reads bounded namespaced authorization client notes only on the initial GET render. Invalid sets are ignored atomically so submitted POST data always wins.git diff --checkpassed in an isolated one-shot container.2026-07-23: Deferred registration prefill policy
IGNORE/ACCEPTconfiguration withIGNOREas the backward-compatible default. Only explicitACCEPTapplies hints, and only on the initial GET render.UP_REGISTER; this prevents hint data from surviving into validation or overriding submitted edits. Hidden profile attributes, credentials, unmanaged/read-only fields, and the configured verification-status attribute are excluded.git diff --checkpassed in an isolated one-shot container.2026-07-23: Redirect-to-registration coverage
git diff --checkpassed in an isolated one-shot container.2026-07-23: Dynamic notification variables and URL encoding
user.<attribute>using its first value, while preserving complete arrays underuser.attributes.<attribute>. Standard user fields retain precedence over colliding dynamic names.url_encodeHandlebars helper so templates can percent-encode each dynamic query value without allowing data to alter query structure.cargo fmt --all -- --checkpassed, both touched crates passed Clippy, editor diagnostics andgit diff --checkwere clean. Windmill Clippy required allowing the unrelated existing Rust 1.96clippy::deprecated_semverfinding inwindmill/src/services/users.rs:1583.2026-07-23: Unit and browser test matrix
/loginor/enrollURLs and covers stock username login, direct stock registration, login redirected to registration, deferredIGNORE, and deferredACCEPT. Provisioned URLs are supplied by documented environment variables; absent scenarios are explicitly skipped.FormActionFactoryprovider registration/requirements.mvn -B clean verify, including 15 voter-enrollment and 22 conditional-authenticator tests; Spotless, editor diagnostics, and staged diff checks passed.Dockerfile.keycloakimage layer was blocked by Docker Desktop content-store I/O errors, while its complete Maven provider-build stage passed locally with standalone Apache Maven.2026-07-23: Docusaurus notification and realm configuration documentation
url_encode, boundedlogin_hint__<field>links for Voting Portal/loginand/enroll, and the editable/untrusted hint security boundary.IGNORE/ACCEPTpolicy, default-realm-template rollout, and existing-realm migration steps.git diff --checkpassed. The build reported only pre-existing broken links in the IVR, Velvet, and system-introduction pages, with no warning from either edited page.2026-07-23: Release 9.5 migration notes
IGNOREpolicy, default and existing realm rollout, and verification of the supported flows and trust boundary.git diff --checkpassed. The build reported only the known unrelated broken links in the IVR, Velvet, and system-introduction pages.2026-07-23: Cross-module security and architecture review
lookupfor dotted names.IGNORE, managed/writable field intersection, credential/hidden/verification exclusions, and per-value URL encoding were reviewed with no actionable findings remaining.clean verifypassed in 1:31, including 15 voter-enrollment and 22 conditional-authenticator tests; 2 focused Windmill tests and 2 sequent-core helper tests passed;cargo fmt --all -- --checkand the established Windmill Clippy gate passed; Docusaurus generated static files successfully; editor diagnostics andgit diff --checkwere clean for touched files.PREFILL_*_URLrealms, and the final Keycloak image layer remains blocked by Docker Desktop content-store I/O corruption. The complete Maven provider reactor passed locally. Docusaurus reports only pre-existing broken links outside edited pages; the portal build retains its existing 112 UI export warnings.2026-07-29: End-to-end verification and prefill defect fixes
parseLoginHintsiteratedURLSearchParams.entries()andMap.keys()withfor ... of. The portal compiles withtarget: "es5"and nodownlevelIteration, so both became indexed loops over an iterator whoselengthisundefinedand never executed a single iteration; hints were always empty and the portal fell through to the plain login path, leaving the hint in the redirect URI. Replaced withforEach. The existing Jest suite could not detect this because@swc/jestbuilds the tests fores2022.createLoginUrlandcreateRegisterUrlreturnPromise<string>inkeycloak-js26 and were passed unawaited toappendLoginHints, which threwTypeError: Failed to construct 'URL': Invalid URLand aborted the redirect. TypeScript reports this asTS2345twice, but the Webpack build runsts-loaderwithtranspileOnly: true, so no type checking occurs at build time.value="${(login.username!'')}", so the username field rendered empty even after Keycloak resolvedlogin_hint. Confirmed independently of the portal by requesting the authorization endpoint withlogin_hint=fooand inspecting the rendered input.login_hintandlogin_hint__username, theredirect_uriis scrubbed of hint parameters, and the Keycloak username field is prefilled.2026-07-29: Per-attribute prefill policy annotation
loginHintPrefillPolicyuser profile attribute annotation:EDITABLE(applied when the annotation is absent, so existing realms are unaffected),READ_ONLY, andIGNORE. An unrecognised value falls back toIGNOREand logs a warning, so a typo can never silently prefill a field.READ_ONLYis enforced on the server because the rendered attribute is only a browser affordance: a submitted value that differs from the hint is rejected withloginHintReadOnlyFieldModified, and the redisplayed form restores the hint value so the voter is not stranded with a rejected value in a field they can no longer edit.readonlyrather thandisabledso the locked value is still submitted; selects, radio buttons, and checkboxes are disabled and mirror their value in a hidden input. The read-only marker is set on every render, including validation-error redisplays.LoginHintPrefillTesttests (7 new) and the full 12-module Maven reactor passed; Spotless clean. Browser verification against a realm with the prefill action configured:EDITABLEprefills and stays editable,IGNOREdoes not prefill, andREAD_ONLYprefills and locks — removingreadonlyin the DOM and submitting another value is rejected with "This field was prefilled and cannot be changed" while the field is restored to the hint value and stays locked.2026-07-29: Login page username lock
login.ftlcannot read theloginHintPrefillPolicyannotation. Dumping the FreeMarker data model of the rendered page confirms that neither theprofilebean nor the authentication-session client notes are available to it; onlylogin.username,login.rememberMe, andrealm.attributesare reachable.loginHintUsernamePolicyisREAD_ONLY, defaulting toEDITABLE. A username restored by remember-me stays editable so a voter can still sign in as somebody else. No new authenticator and no browser-flow change are required; the stock Username Password Form is unchanged.READ_ONLYremains server-enforced there.LoginTemplateTestpins both the prefill and the lock; the full 12-module Maven reactor and Spotless passed. Browser verification against the stock Username Password Form:READ_ONLYrenders the fieldreadOnly: true, andEDITABLErenders itreadOnly: false.2026-07-29: Realm configuration required
Neither prefill entry point is enabled by the election-event realm template, so both remain opt-in per realm and are worth an explicit decision before merge:
loginHintUsernamePolicytoREAD_ONLYto lock a prefilled username on the login page.Known gaps left out of scope:
sequent.admin-portal/login/login.ftlhas the same missingvalue="${(login.username!'')}", sologin_hintand remember-me prefill are broken there too; andloginHintReadOnlyFieldModifiedis currently English only, with the other seven locales falling back to English.Testing
Validation evidence will be appended here after each completed task. Final coverage will include TypeScript, Java, Rust, browser flows, provider packaging, and Docusaurus build checks.
Further Comments
Stable v10 draft PR: #2907
The v10 branch replays the nine stable-relevant validated commits onto
release/10.0; the main-only 9.5 release-note commit is omitted because that page does not exist on the stable branch. Portal, Maven, Rust, Docusaurus, patch-hygiene, and editor-diagnostic gates passed on the v10 branch.