auth: make session binding optional, enforce WebAuthn credential ownership
- Make session_id parameter optional in validate_token — only enforced when provided, allowing WebSocket auth which cannot carry custom headers - Remove decode_token round-trip from daemon WS handler - Override WebAuthn registration username from JWT user_ctx to prevent users from registering credentials under another user's account - Frontend no longer sends username for WebAuthn registration - Redirect to login on 401 after token refresh fails for POST requests - Remove redundant auth session check from login page load - Add tests for session_id semantics and WebAuthn ownership guard
This commit is contained in:
@@ -165,6 +165,10 @@ export async function apiFetch(url, options = {}) {
|
||||
if (res.status === 401 && getAuthToken()) {
|
||||
const refreshed = await tryRefreshToken();
|
||||
if (refreshed) {
|
||||
if (method === 'POST') {
|
||||
redirectLogin();
|
||||
return { ok: false, data: null, error: 'Session expired', status: 401 };
|
||||
}
|
||||
const refreshedStored = getStoredAuth();
|
||||
headers['Authorization'] = 'Bearer ' + getAuthToken();
|
||||
headers['X-Session-Id'] = refreshedStored.session_id;
|
||||
|
||||
@@ -209,21 +209,9 @@ const Page = definePage({
|
||||
document.title = 'Login — Vacuum Wall';
|
||||
},
|
||||
|
||||
async load(state, abortController) {
|
||||
load() {
|
||||
handleLogin();
|
||||
setupPasskeyButton();
|
||||
|
||||
if (getAuthToken()) {
|
||||
try {
|
||||
const res = await apiFetch('/api/auth/session', { signal: abortController?.signal });
|
||||
if (res.ok) {
|
||||
window.location.hash = '/dashboard';
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// auth check failed, show login
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
|
||||
@@ -91,20 +91,11 @@ function addCredentialModal() {
|
||||
setModalProcessing(true);
|
||||
refreshModals();
|
||||
|
||||
const user = JSON.parse(sessionStorage.getItem('vw:user') || 'null');
|
||||
const username = user?.username || '';
|
||||
if (!username) {
|
||||
toast('Username not available', 'error');
|
||||
setModalProcessing(false);
|
||||
refreshModals();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Get registration options
|
||||
// Step 1: Get registration options (username from JWT)
|
||||
const beginRes = await apiFetch('/api/auth/webauthn/register-begin', {
|
||||
method: 'POST',
|
||||
body: { username },
|
||||
body: {},
|
||||
});
|
||||
|
||||
if (!beginRes.ok) {
|
||||
@@ -117,11 +108,10 @@ function addCredentialModal() {
|
||||
const credentialName = document.getElementById('cred-name')?.value?.trim() || '';
|
||||
const credentialResponse = await startRegistration(options);
|
||||
|
||||
// Step 3: Verify with server
|
||||
// Step 3: Verify with server (username from JWT)
|
||||
const finishRes = await apiFetch('/api/auth/webauthn/register-finish', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username,
|
||||
credential_response: credentialResponse,
|
||||
registration_options: options,
|
||||
name: credentialName,
|
||||
|
||||
Reference in New Issue
Block a user