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
56 lines
2.1 KiB
JavaScript
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) {}
|
|
})();
|
|
})();
|