Files
Project-SDE-WP-Suite/html/login.js
Matt Mabrey 72b10283fc T10.5: login becomes an Okta redirect, not a form
server/app.py:
- okta_login(): validates and stashes ?next= (same-site path only) in the
  OAuth-state session before redirecting to Okta, so a deep link an assignment
  email carried (X1/CR-011/CR-014) survives the round trip instead of always
  landing on /index.html.
- okta_callback(): reads that stashed next= back (re-validated on the way out
  too - belt and suspenders against a crafted value) and redirects there on
  success. The two failure paths that used to raise a raw HTTPException -
  OAuthError (sign-in cancelled/failed) and a locally-disabled account - now
  redirect to /login.html?error=... instead: this route is reached by a full
  page browser navigation from Okta, not a fetch() call, so a JSON error body
  just looks like a broken page to whoever is signing in.

html/login.html + html/login.js: rebuilt as a single "Sign in with Okta" link,
replacing the username/password form and the forgot/reset-password views
(gone entirely - no local password exists to reset, per D15/D16/T10.4). Kept
the accessible error/ok banner pattern (role=alert / role=status) byte-for-
byte, since CLAUDE.md names this file as the reference other pages copy for
that pattern. login.js reads ?next= off its own URL (auth-guard.js's
goToLogin() already builds this, unchanged) and forwards it to
/api/auth/okta/login, and shows a plain-language message for ?error=disabled
/ ?error=cancelled, clearing the code from the address bar once shown. Sign-
out (auth-guard.js's wpLogout()) already redirected to login.html - untouched,
already satisfied "lands back on the app's own login page."

Uses a real <a href> rather than a JS-driven navigation, so it's a working
link even before login.js runs, and needs no keyboard/touch handling beyond
what a link gets for free (C1 accessibility).

Also fixed in passing (not a separate commit - this is what exposed it):
_safe_next_path() on the server and safeNext() in login.js enforce the exact
same rule (same-site path only, reject '//' and scheme URLs) so a crafted
?next= can't become an open redirect through a real Okta sign-in.

Verified: a fake-Okta-client round trip against the real app (SessionMiddleware
fix from the prior commit) confirms next= is honored end to end, a malicious
next= is rejected and falls back to /index.html, OAuthError redirects to
?error=cancelled, and a disabled account redirects to ?error=disabled. The
JS-side safeNext() was checked against the same cases directly in Node and
matches the server's validation exactly. login.js passes `node --check`;
login.html parses cleanly. Live 390px/1440px screenshots were NOT captured
this session - the environment's browser pane isn't signed in to view a
published preview of it, so that check needs to happen when this branch is
actually run and opened by a signed-in browser; the layout risk is low since
.card/.brand/.error/.ok/.foot are unchanged from the already-shipped file and
the only new CSS is one simple full-width block link.

wave-10.md T10.5 / D15 / D16
2026-09-03 11:27:26 -07:00

56 lines
2.1 KiB
JavaScript

/* Login page logic for the Work Package Suite.
One action: sign in with Okta. There is no local password anymore (D15/D16,
T10.4) — this page's only job is building the link to /api/auth/okta/login
(carrying ?next=, if there was one) and showing a plain-language message for
the failure states server/app.py's okta_callback() sends back here (T10.5). */
(function () {
'use strict';
var errorBox = document.getElementById('error');
var okBox = document.getElementById('ok');
var signinLink = document.getElementById('okta-signin');
function byId(id) { return document.getElementById(id); }
function showError(msg) {
okBox.classList.remove('show');
errorBox.textContent = msg;
errorBox.classList.add('show');
}
// Same-site path only — mirrors the check server/app.py's _safe_next_path()
// makes again on the way back, so a crafted ?next= can't become an open
// redirect even if this client-side check were somehow bypassed.
function safeNext() {
try {
var next = new URLSearchParams(location.search).get('next') || '';
if (next && next.charAt(0) === '/' && next.charAt(1) !== '/') return next;
} catch (e) {}
return '';
}
if (signinLink) {
var next = safeNext();
if (next) signinLink.href = '/api/auth/okta/login?next=' + encodeURIComponent(next);
}
var ERROR_MESSAGES = {
disabled: 'Your account has been disabled. Contact an administrator.',
cancelled: 'Sign-in was not completed. Select the button below to try again.'
};
(function showErrorFromQuery() {
try {
var code = new URLSearchParams(location.search).get('error') || '';
if (!code) return;
showError(ERROR_MESSAGES[code] || 'Sign-in was not completed. Select the button below to try again.');
// Out of the address bar once shown — an error code has no reason to
// survive a refresh or get copied along with the link.
var url = new URL(location.href);
url.searchParams.delete('error');
history.replaceState(null, '', url.pathname + url.search);
} catch (e) {}
})();
})();