Record the human onboarding run and fix password-setup usability
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 5s
Identity provider journey acceptance / provider (push) Successful in 15s
Build and Publish identity-provisioner / build-and-push (push) Successful in 7s

- NK-WP-0036 finished: native onboarding journey completed by the operator.
- NK-WP-0037-T02 waits on key-cape: Authelia 4.38 rejects every human
  prompt=login flow (auth_time precedes request registration).
- identity-provisioner: read-only autocomplete=username field on the setup
  form (submitted value ignored) and an HTTPS sign-in link on completion.
- NK-WP-0041 tracks the fixes and routes Authelia/user-engine findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 299762@bnt-lap001
Assistant-Session: d3d3cea1-869c-44f1-be2a-3d6d3550e72e
This commit is contained in:
tegwick 2026-09-23 21:48:29 +02:00
parent 15939d00af
commit 6c4fcaf9ae
7 changed files with 215 additions and 12 deletions

View file

@ -14,6 +14,7 @@ class Handler(BaseHTTPRequestHandler):
provisioner: LLDAPProvisioner
password_setups: PasswordSetupGrants
service_token: str
signin_url: str = ""
def do_GET(self):
path = urlsplit(self.path)
@ -34,7 +35,7 @@ class Handler(BaseHTTPRequestHandler):
token = parse_qs(path.query).get("token", [""])[0]
if not self.password_setups.valid(token):
return self._html(400, _expired_page())
return self._html(200, _setup_page(token))
return self._html(200, _setup_page(token, self.password_setups.login_name(token)))
self._send(404, {"error": "not_found"})
def do_POST(self):
@ -71,15 +72,16 @@ class Handler(BaseHTTPRequestHandler):
token = body.get("token", [""])[0]
password = body.get("password", [""])[0]
confirmation = body.get("confirmation", [""])[0]
login_name = self.password_setups.login_name(token)
if password != confirmation:
return self._html(400, _setup_page(token, "Passwords do not match."))
return self._html(400, _setup_page(token, login_name, "Passwords do not match."))
try:
return_to = self.password_setups.consume(token, password)
except ValueError as exc:
return self._html(400, _setup_page(token, str(exc)))
return self._html(400, _setup_page(token, login_name, str(exc)))
except (DependencyFailure, RuntimeError):
return self._html(503, _failed_page())
return self._html(200, _complete_page(return_to))
return self._html(200, _complete_page(return_to, self.signin_url))
def _send(self, status: int, payload: dict):
body = json.dumps(payload, separators=(",", ":")).encode()
@ -115,6 +117,7 @@ def main():
Handler.service_token = os.environ["PROVISIONER_SERVICE_TOKEN"].strip()
if not Handler.service_token:
raise ValueError("PROVISIONER_SERVICE_TOKEN must not be empty")
Handler.signin_url = _https_or_empty(os.environ.get("PASSWORD_SETUP_SIGNIN_URL", ""))
Handler.password_setups = PasswordSetupGrants(
public_url=os.environ["PASSWORD_SETUP_PUBLIC_URL"],
setter=LLDAPPasswordSetter(
@ -137,12 +140,27 @@ input,button{{padding:.7rem}}.error{{color:#a00}}
</style><main><h1>{escape(title)}</h1>{content}</main></html>"""
def _setup_page(token: str, error: str = "") -> str:
def _https_or_empty(url: str) -> str:
url = url.strip()
if url and not url.startswith("https://"):
raise ValueError("PASSWORD_SETUP_SIGNIN_URL must be an HTTPS URL")
return url
def _setup_page(token: str, login_name: str = "", error: str = "") -> str:
message = f'<p class="error">{escape(error)}</p>' if error else ""
# A read-only username field lets password managers store the new
# password against the directory login name, not against nothing.
username = (
f'<label>Login name<input type="text" name="username" value="{escape(login_name, quote=True)}" '
'readonly autocomplete="username"></label>'
if login_name else ""
)
return _page("Set up your password", f"""{message}
<p>This single-use link expires shortly. Choose a password of at least 12 characters.</p>
<form method="post" action="/setup/password" autocomplete="off">
<form method="post" action="/setup/password">
<input type="hidden" name="token" value="{escape(token)}">
{username}
<label>New password<input type="password" name="password" minlength="12" required autocomplete="new-password"></label>
<label>Confirm password<input type="password" name="confirmation" minlength="12" required autocomplete="new-password"></label>
<button type="submit">Set password</button></form>""")
@ -156,10 +174,12 @@ def _failed_page() -> str:
return _page("Setup unavailable", "<p>Password setup could not be completed. Request a new link and try again.</p>")
def _complete_page(return_to: str = "") -> str:
def _complete_page(return_to: str = "", signin_url: str = "") -> str:
content = "<p>Your password is ready. Sign in with your own account to continue.</p>"
if return_to:
content += f'<p><a rel="noreferrer" href="{escape(return_to, quote=True)}">Continue to your company</a></p>'
elif signin_url:
content += f'<p><a rel="noreferrer" href="{escape(signin_url, quote=True)}">Sign in</a></p>'
else:
content += "<p>Return to the application to sign in and enroll MFA.</p>"
return _page("Password set", content)