feat(security): HttpOnly cookies + CSP headers + CSRF double-submit protection

- Migrate auth tokens from localStorage to HttpOnly SameSite=Lax cookies
- Add cookie_auth.rs: helpers for setting/clearing auth + CSRF cookies
- Update auth middleware: 3-method auth (Bearer → Basic → Cookie)
- Add 5 security headers: CSP, X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, Permissions-Policy
- Implement CSRF double-submit cookie pattern (csrf.rs middleware)
- Set CSRF cookie on login/refresh/oidc-exchange, clear on logout
- CookieAuthenticated marker skips CSRF for Bearer/Basic clients
- Frontend: strip all localStorage token refs from 14 JS files
- Frontend: csrf.js utility + all 52 mutating fetch/XHR calls protected
- 121 tests passing, 0 warnings
This commit is contained in:
Dionisio
2026-03-03 01:10:50 +01:00
parent 7b2a8577a9
commit d2c08d31ba
27 changed files with 579 additions and 455 deletions
+7 -27
View File
@@ -137,6 +137,7 @@
<div id="status-error" class="status error" id="status-error-msg"></div>
</div>
<script src="/js/core/csrf.js"></script>
<script>
const API_BASE = window.location.origin;
const codeInput = document.getElementById('user-code');
@@ -175,14 +176,13 @@
async function lookupCode(code) {
try {
const token = getAuthToken();
if (!token) {
const resp = await fetch(`${API_BASE}/api/auth/device/verify?code=${encodeURIComponent(code)}`, {
credentials: 'same-origin'
});
if (resp.status === 401) {
showError('You must be logged in to authorize a device. Please log in first.');
return;
}
const resp = await fetch(`${API_BASE}/api/auth/device/verify?code=${encodeURIComponent(code)}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!resp.ok) throw new Error('Lookup failed');
const data = await resp.json();
@@ -210,13 +210,10 @@
btnDeny.disabled = true;
try {
const token = getAuthToken();
const resp = await fetch(`${API_BASE}/api/auth/device/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() },
body: JSON.stringify({ user_code: currentCode, action: action })
});
@@ -242,23 +239,6 @@
errorText.textContent = msg;
errorText.style.display = 'block';
}
function getAuthToken() {
// Try localStorage (OxiCloud frontend stores tokens there)
try {
const stored = localStorage.getItem('auth_token')
|| localStorage.getItem('access_token')
|| localStorage.getItem('oxicloud_token');
if (stored) return stored;
// Try parsing a JSON auth object
const authData = localStorage.getItem('auth');
if (authData) {
const parsed = JSON.parse(authData);
return parsed.access_token || parsed.token;
}
} catch {}
return null;
}
</script>
</body>
</html>