Skip to content

✨ Prefill Keycloak login and registration fields from voting links - #2905

Open
edulix wants to merge 14 commits into
mainfrom
feat/meta-12617/main
Open

✨ Prefill Keycloak login and registration fields from voting links#2905
edulix wants to merge 14 commits into
mainfrom
feat/meta-12617/main

Conversation

@edulix

@edulix edulix commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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

  • Add and test the Voting Portal login-hint contract.
  • Forward validated hints through keycloak-js without weakening OIDC state or PKCE handling.
  • Add and test the stock Keycloak registration prefill action.
  • Add and test the deferred registration IGNORE / ACCEPT policy.
  • Cover the redirect-to-registration flow.
  • Expose dynamic Windmill notification variables and add url_encode.
  • Add the full unit and browser test matrix.
  • Document notification templates and Keycloak realm configuration in Docusaurus.
  • Update the active next-release notes with migration steps.
  • Complete an overall security and architecture review.

Architecture And Security Decisions

  • Use login_hint__<field> to avoid collisions with Voting Portal controls and OIDC parameters. Mirror only login_hint__username to standard OIDC login_hint for stock login forms.
  • Bound each link to five scalar hints, 128-character field names, and 255-character values. This fits Keycloak 26.6.1 custom authorization-parameter defaults without deployment-wide changes.
  • Treat hints as editable, untrusted input. They never authenticate a user or verify an attribute; Keycloak registration metadata and normal validators remain authoritative.
  • Reject malformed or polluted hint sets atomically instead of partially forwarding them.
  • Percent encoding protects URL structure, not confidentiality or authenticity. The feature must not log hint values.
  • Run native Java and Rust checks with local tools or isolated one-shot containers; do not require the full devcontainer stack.

Implementation Log

2026-07-23: Voting Portal hint contract

  • Commit: c398861466
  • Added a typed parser/URL builder, dedicated translated invalid-link error, and Jest/SWC harness.
  • Validation: 11 focused Jest tests, ESLint, and Prettier passed. The production Webpack build completed with 112 pre-existing ui-essentials/asset-size warnings and no errors.

2026-07-23: Keycloak JS forwarding

  • Commit: 12363fa805
  • Capture validated hints only on the initial /login or /enroll route, 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.
  • Use createLoginUrl and createRegisterUrl so keycloak-js continues to own state, nonce, callback storage, redirect URI, locale, and PKCE. Mirror username to standard login_hint, then append the validated namespace through URLSearchParams.
  • Validation: focused Jest suite, ESLint, and Prettier passed; production Webpack build completed with the same 112 pre-existing warnings and no errors. The URL test explicitly preserves state, nonce, PKCE challenge, redirect URI, and rejects query injection.

2026-07-23: Stock Keycloak registration prefill

  • Commit: 56f10afde5
  • Added an auto-registered stock registration FormAction that reads bounded namespaced authorization client notes only on the initial GET render. Invalid sets are ignored atomically so submitted POST data always wins.
  • Intersect hints with explicit managed writable registration metadata and exclude credential, unmanaged, read-only, and configured verification attributes without logging values.
  • Validation: clean Maven 17 reactor compile, all 8 voter-enrollment tests, focused 4-test prefill suite, Spotless, editor diagnostics, and git diff --check passed in an isolated one-shot container.

2026-07-23: Deferred registration prefill policy

  • Commit: 838a1a331c
  • Added list-valued IGNORE / ACCEPT configuration with IGNORE as the backward-compatible default. Only explicit ACCEPT applies hints, and only on the initial GET render.
  • Build a disposable registration profile to determine managed writable fields instead of reusing 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.
  • Validation: clean Maven 17 reactor compile, all 10 voter-enrollment tests, focused 6-test deferred suite, Spotless, editor diagnostics, and git diff --check passed in an isolated one-shot container.

2026-07-23: Redirect-to-registration coverage

  • Commit: 132d71f9c4
  • Added regression coverage proving the authenticator redirects to Keycloak registration with the same client ID and authentication-session tab ID while leaving namespaced login-hint client notes unchanged. The existing production authenticator required no change.
  • Validation: clean Maven 17 reactor compile, focused redirect test, all 21 conditional-authenticators tests, Spotless across 28 Java files, editor diagnostics, and git diff --check passed in an isolated one-shot container.

2026-07-23: Dynamic notification variables and URL encoding

  • Commit: f4e9d4bcae
  • Windmill notification variables now expose each non-empty dynamic Keycloak attribute at user.<attribute> using its first value, while preserving complete arrays under user.attributes.<attribute>. Standard user fields retain precedence over colliding dynamic names.
  • Added the shared url_encode Handlebars helper so templates can percent-encode each dynamic query value without allowing data to alter query structure.
  • Validation: focused Windmill and sequent-core tests passed (1 test each), cargo fmt --all -- --check passed, both touched crates passed Clippy, editor diagnostics and git diff --check were clean. Windmill Clippy required allowing the unrelated existing Rust 1.96 clippy::deprecated_semver finding in windmill/src/services/users.rs:1583.

2026-07-23: Unit and browser test matrix

  • Commit: e0b7957cbb
  • Added a five-scenario Nightwatch matrix that starts at Voting Portal /login or /enroll URLs and covers stock username login, direct stock registration, login redirected to registration, deferred IGNORE, and deferred ACCEPT. Provisioned URLs are supplied by documented environment variables; absent scenarios are explicitly skipped.
  • Added Java coverage for atomic stock-form rejection and generated FormActionFactory provider registration/requirements.
  • Validation: Nightwatch discovered and loaded the matrix with all five unconfigured scenarios skipped; 11 portal hint tests passed; portal ESLint, Prettier, and production build passed; all 12 Keycloak extension modules passed mvn -B clean verify, including 15 voter-enrollment and 22 conditional-authenticator tests; Spotless, editor diagnostics, and staged diff checks passed.
  • Environment limits: the five live browser flows require provisioned realm/event URLs and were not executed here. The final Dockerfile.keycloak image 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

  • Commit: 4ab6ec58b7
  • Documented notification variables, multivalued attributes, url_encode, bounded login_hint__<field> links for Voting Portal /login and /enroll, and the editable/untrusted hint security boundary.
  • Documented stock registration action ordering, deferred IGNORE/ACCEPT policy, default-realm-template rollout, and existing-realm migration steps.
  • Validation: Docusaurus production build generated static files successfully in 41.75 seconds; editor diagnostics and git diff --check passed. 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

  • Commit: 69db7c892f
  • Added opt-in migration steps for notification-template links, stock registration action ordering, deferred registration default IGNORE policy, default and existing realm rollout, and verification of the supported flows and trust boundary.
  • Validation: Docusaurus production build generated static files successfully in 9.14 seconds; editor diagnostics and git diff --check passed. 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

  • Commit: 5f095d589f
  • Review remediations: excluded hidden declarative-profile fields; rejected malformed hint encoding; made prototype-like names bounded and scrubbed; scrubbed invalid hint sets before the error page; normalized trailing-slash login/enroll routes; removed rendered content, recipients, and helper arguments from tracing, transport, and electoral-log payloads; documented Handlebars lookup for dotted names.
  • Architecture verification: bounded atomic parsing, OIDC state/nonce/PKCE preservation, authentication-session retention across registration redirects, initial-GET-only prefill, POST precedence, deferred default IGNORE, managed/writable field intersection, credential/hidden/verification exclusions, and per-value URL encoding were reviewed with no actionable findings remaining.
  • Validation: 21 portal tests passed; portal ESLint, Prettier, and production build passed; full 12-module Maven clean verify passed 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 -- --check and the established Windmill Clippy gate passed; Docusaurus generated static files successfully; editor diagnostics and git diff --check were clean for touched files.
  • Residual environment limits: live Nightwatch flows remain skipped without provisioned PREFILL_*_URL realms, 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

  • Commit: 566bb5a172
  • Running the feature against a live Voting Portal and Keycloak (rather than unit tests alone) found three defects that each independently prevented any prefill from reaching the login form.
  • parseLoginHints iterated URLSearchParams.entries() and Map.keys() with for ... of. The portal compiles with target: "es5" and no downlevelIteration, so both became indexed loops over an iterator whose length is undefined and 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 with forEach. The existing Jest suite could not detect this because @swc/jest builds the tests for es2022.
  • createLoginUrl and createRegisterUrl return Promise<string> in keycloak-js 26 and were passed unawaited to appendLoginHints, which threw TypeError: Failed to construct 'URL': Invalid URL and aborted the redirect. TypeScript reports this as TS2345 twice, but the Webpack build runs ts-loader with transpileOnly: true, so no type checking occurs at build time.
  • The Voting Portal login template omitted stock Keycloak's value="${(login.username!'')}", so the username field rendered empty even after Keycloak resolved login_hint. Confirmed independently of the portal by requesting the authorization endpoint with login_hint=foo and inspecting the rendered input.
  • Validation: 21 portal Jest tests, portal ESLint and Prettier passed. Browser verification: the authorization URL now carries both login_hint and login_hint__username, the redirect_uri is scrubbed of hint parameters, and the Keycloak username field is prefilled.

2026-07-29: Per-attribute prefill policy annotation

  • Commit: c68bd8dbb2
  • Registration fields now declare how they accept a prefilled value through the loginHintPrefillPolicy user profile attribute annotation: EDITABLE (applied when the annotation is absent, so existing realms are unaffected), READ_ONLY, and IGNORE. An unrecognised value falls back to IGNORE and logs a warning, so a typo can never silently prefill a field.
  • READ_ONLY is enforced on the server because the rendered attribute is only a browser affordance: a submitted value that differs from the hint is rejected with loginHintReadOnlyFieldModified, 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.
  • Text inputs render readonly rather than disabled so 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.
  • Both prefill paths share one resolution helper, so the annotation applies to stock and deferred registration alike.
  • Validation: 12 LoginHintPrefillTest tests (7 new) and the full 12-module Maven reactor passed; Spotless clean. Browser verification against a realm with the prefill action configured: EDITABLE prefills and stays editable, IGNORE does not prefill, and READ_ONLY prefills and locks — removing readonly in 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

  • Commit: 45a94d3107
  • The login page is not rendered from the user profile, so login.ftl cannot read the loginHintPrefillPolicy annotation. Dumping the FreeMarker data model of the rendered page confirms that neither the profile bean nor the authentication-session client notes are available to it; only login.username, login.rememberMe, and realm.attributes are reachable.
  • The template therefore renders 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 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.
  • This login lock is presentation-only. Keycloak still authenticates whichever username is submitted, which is the same guarantee an unprefilled login page provides: the password decides which account is entered. Registration stores the value, so READ_ONLY remains server-enforced there.
  • Validation: new LoginTemplateTest pins both the prefill and the lock; the full 12-module Maven reactor and Spotless passed. Browser verification against the stock Username Password Form: READ_ONLY renders the field readOnly: true, and EDITABLE renders it readOnly: 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:

  • Add the Sequent: Login hint registration prefill action to the registration flow, ordered before registration user creation.
  • Set the realm attribute loginHintUsernamePolicy to READ_ONLY to lock a prefilled username on the login page.

Known gaps left out of scope: sequent.admin-portal/login/login.ftl has the same missing value="${(login.username!'')}", so login_hint and remember-me prefill are broken there too; and loginHintReadOnlyFieldModified is 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.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://docs.sequentech.io/docusaurus/pr-preview/pr-2905/

Built to branch doc-previews at 2026-07-29 17:17 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@edulix
edulix marked this pull request as ready for review July 23, 2026 12:59
Copilot AI review requested due to automatic review settings July 23, 2026 12:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 /login and /enroll URLs, keep validated hints in-memory, and forward them via keycloak-js URLs (while mirroring username to OIDC login_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_encode helper, 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());
edulix added 4 commits July 29, 2026 13:55
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants