From 64bbe67912a375b42066a95260593aca7dd82c3a Mon Sep 17 00:00:00 2001 From: aekaisato <35282290+aekaisato@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:09:32 -0400 Subject: [PATCH] Create "remember me" functionality for basic auth, which sets an unexpiring jwt and refreshes the cookie during requests (#1132) * Create "remember me" functionality for basic auth, which sets an unexpiring jwt and refreshes the cookie during requests --- server/crypto.ts | 11 +++++----- server/http_server.ts | 50 +++++++++++++++++++++++++++++++++---------- web/auth.html | 4 ++++ 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/server/crypto.ts b/server/crypto.ts index 094c3295..1f87ddf4 100644 --- a/server/crypto.ts +++ b/server/crypto.ts @@ -60,12 +60,13 @@ export class JWTIssuer { createJWT( payload: Record, - expirySeconds: number, + expirySeconds?: number, ): Promise { - return create({ alg: "HS512", typ: "JWT" }, { - ...payload, - exp: getNumericDate(expirySeconds), - }, this.key); + const jwtPayload = { ...payload }; + if (expirySeconds) { + jwtPayload.exp = getNumericDate(expirySeconds); + } + return create({ alg: "HS512", typ: "JWT" }, jwtPayload, this.key); } verifyAndDecodeJWT(jwt: string): Promise> { diff --git a/server/http_server.ts b/server/http_server.ts index 1407342e..96d6356b 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -319,6 +319,7 @@ export class HttpServer { this.app.get("/.logout", (c) => { const url = new URL(c.req.url); deleteCookie(c, authCookieName(url.host)); + deleteCookie(c, "refreshLogin"); return c.redirect("/.auth"); }); @@ -331,20 +332,22 @@ export class HttpServer { validator("form", (value, c) => { const username = value["username"]; const password = value["password"]; + const rememberMe = value["rememberMe"]; if ( !username || typeof username !== "string" || - !password || typeof password !== "string" + !password || typeof password !== "string" || + (rememberMe && typeof rememberMe !== "string") ) { return c.redirect("/.auth?error=0"); } - return { username, password }; + return { username, password, rememberMe }; }), async (c) => { const req = c.req; const url = new URL(c.req.url); - const { username, password } = req.valid("form"); + const { username, password, rememberMe } = req.valid("form"); const { user: expectedUser, @@ -353,24 +356,30 @@ export class HttpServer { if (username === expectedUser && password === expectedPassword) { // Generate a JWT and set it as a cookie - const jwt = await this.spaceServer.jwtIssuer.createJWT( - { username }, - authenticationExpirySeconds, - ); + const jwt = rememberMe + ? await this.spaceServer.jwtIssuer.createJWT({ username }) + : await this.spaceServer.jwtIssuer.createJWT( + { username }, + authenticationExpirySeconds, + ); console.log("Successful auth"); + const inAWeek = new Date( + Date.now() + authenticationExpirySeconds * 1000, + ); setCookie(c, authCookieName(url.host), jwt, { - expires: new Date( - Date.now() + authenticationExpirySeconds * 1000, - ), // in a week + expires: inAWeek, // sameSite: "Strict", // httpOnly: true, }); + if (rememberMe) { + setCookie(c, "refreshLogin", "true", { expires: inAWeek }); + } const values = await c.req.parseBody(); const from = values["from"]; return c.redirect(typeof from === "string" ? from : "/"); } else { console.error("Authentication failed, redirecting to auth page."); - return c.redirect("/.auth?error=1", 401); + return c.redirect("/.auth?error=1"); } }, ).all((c) => { @@ -404,6 +413,7 @@ export class HttpServer { const authToken = authHeader.slice("Bearer ".length); if (authToken === this.spaceServer.authToken) { // All good, let's proceed + this.refreshLogin(c, host); return next(); } else { console.log( @@ -435,10 +445,28 @@ export class HttpServer { return redirectToAuth(); } } + this.refreshLogin(c, host); return next(); }); } + private refreshLogin(c: Context, host: string) { + if (getCookie(c, "refreshLogin")) { + const inAWeek = new Date( + Date.now() + authenticationExpirySeconds * 1000, + ); + const jwt = getCookie(c, authCookieName(host)); + if (jwt) { + setCookie(c, authCookieName(host), jwt, { + expires: inAWeek, + // sameSite: "Strict", + // httpOnly: true, + }); + setCookie(c, "refreshLogin", "true", { expires: inAWeek }); + } + } + } + private addFsRoutes() { this.app.use( "*", diff --git a/web/auth.html b/web/auth.html index 6ea9def2..d471c0e9 100644 --- a/web/auth.html +++ b/web/auth.html @@ -95,6 +95,10 @@ placeholder="Password" /> +
+ + +