Add security improvements: refresh tokens, passkeys, CSP, audit logging, TOTP encryption
- Shorten access token to 30min with 7-day refresh token flow - Add WebAuthn passkey registration and login with TOTP fallback - Add Content-Security-Policy header - Add audit logging middleware to /volume1/docker/nas-dashboard/audit.log - Block /volume1/docker/ in files endpoint - Encrypt TOTP secret at rest with Fernet (derived from SECRET_KEY) - New deps: py_webauthn, cryptography
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { post } from "../lib/api.js";
|
||||
import { post, get } from "../lib/api.js";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let currentPassword = $state("");
|
||||
let newPassword = $state("");
|
||||
@@ -8,6 +9,76 @@
|
||||
let error = $state("");
|
||||
let saving = $state(false);
|
||||
|
||||
let passkeyCount = $state(0);
|
||||
let passkeyMsg = $state("");
|
||||
let passkeyErr = $state("");
|
||||
let passkeyLoading = $state(false);
|
||||
|
||||
onMount(loadPasskeys);
|
||||
|
||||
async function loadPasskeys() {
|
||||
try {
|
||||
const res = await get("/auth/passkey/list");
|
||||
passkeyCount = res.count;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function base64urlToBuffer(b64) {
|
||||
const s = b64.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const raw = atob(s);
|
||||
return Uint8Array.from(raw, c => c.charCodeAt(0)).buffer;
|
||||
}
|
||||
|
||||
function bufferToBase64url(buf) {
|
||||
const bytes = new Uint8Array(buf);
|
||||
let s = '';
|
||||
bytes.forEach(b => s += String.fromCharCode(b));
|
||||
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
async function registerPasskey() {
|
||||
passkeyLoading = true;
|
||||
passkeyMsg = ""; passkeyErr = "";
|
||||
try {
|
||||
const opts = await post("/auth/passkey/register/options");
|
||||
opts.challenge = base64urlToBuffer(opts.challenge);
|
||||
opts.user.id = base64urlToBuffer(opts.user.id);
|
||||
if (opts.excludeCredentials) {
|
||||
opts.excludeCredentials = opts.excludeCredentials.map(c => ({ ...c, id: base64urlToBuffer(c.id) }));
|
||||
}
|
||||
const cred = await navigator.credentials.create({ publicKey: opts });
|
||||
const body = {
|
||||
id: cred.id,
|
||||
rawId: bufferToBase64url(cred.rawId),
|
||||
type: cred.type,
|
||||
response: {
|
||||
attestationObject: bufferToBase64url(cred.response.attestationObject),
|
||||
clientDataJSON: bufferToBase64url(cred.response.clientDataJSON),
|
||||
},
|
||||
};
|
||||
await post("/auth/passkey/register/verify", body);
|
||||
passkeyMsg = "Passkey registered successfully";
|
||||
await loadPasskeys();
|
||||
} catch (e) {
|
||||
passkeyErr = e.body?.detail || e.message || "Failed to register passkey";
|
||||
}
|
||||
passkeyLoading = false;
|
||||
}
|
||||
|
||||
async function clearPasskeys() {
|
||||
if (!confirm("Remove all passkeys?")) return;
|
||||
passkeyLoading = true;
|
||||
passkeyMsg = ""; passkeyErr = "";
|
||||
try {
|
||||
await post("/auth/passkey/clear");
|
||||
passkeyMsg = "All passkeys removed";
|
||||
passkeyCount = 0;
|
||||
} catch (e) {
|
||||
passkeyErr = e.body?.detail || "Failed to remove passkeys";
|
||||
}
|
||||
passkeyLoading = false;
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
error = "";
|
||||
message = "";
|
||||
@@ -59,4 +130,28 @@
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl border border-surface-200 p-6 shadow-sm max-w-md">
|
||||
<h3 class="text-sm font-semibold text-surface-700 mb-4">Passkeys</h3>
|
||||
|
||||
{#if passkeyMsg}
|
||||
<div class="text-sm text-emerald-600 bg-emerald-50 px-3 py-2 rounded-lg mb-4">{passkeyMsg}</div>
|
||||
{/if}
|
||||
{#if passkeyErr}
|
||||
<div class="text-sm text-rose-600 bg-rose-50 px-3 py-2 rounded-lg mb-4">{passkeyErr}</div>
|
||||
{/if}
|
||||
|
||||
<p class="text-sm text-surface-500 mb-4">{passkeyCount} passkey{passkeyCount !== 1 ? 's' : ''} registered</p>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button onclick={registerPasskey} disabled={passkeyLoading} class="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors disabled:opacity-50">
|
||||
Register new passkey
|
||||
</button>
|
||||
{#if passkeyCount > 0}
|
||||
<button onclick={clearPasskeys} disabled={passkeyLoading} class="px-4 py-2 text-sm font-medium text-rose-600 bg-rose-50 hover:bg-rose-100 rounded-lg transition-colors disabled:opacity-50">
|
||||
Remove all
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user