Redirect to initial url after authentication (#893)

pull/897/head
MrMugame 2024-06-22 12:45:23 +02:00 committed by GitHub
parent bd3e91ba65
commit b6d95ec632
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 168 additions and 130 deletions

View File

@ -10,5 +10,5 @@ export async function cleanCommand() {
} }
await editor.flashNotification("Now wiping all state and logging out..."); await editor.flashNotification("Now wiping all state and logging out...");
await debug.cleanup(); await debug.cleanup();
await editor.openUrl("/.auth?logout", true); await editor.openUrl("/.logout", true);
} }

View File

@ -1,6 +1,6 @@
import { deleteCookie, getCookie, setCookie } from "hono/helper.ts"; import { deleteCookie, getCookie, setCookie } from "hono/helper.ts";
import { cors } from "hono/middleware.ts"; import { cors } from "hono/middleware.ts";
import { type Context, Hono, type HonoRequest } from "hono/mod.ts"; import { type Context, Hono, type HonoRequest, validator } from "hono/mod.ts";
import { AssetBundle } from "$lib/asset_bundle/bundle.ts"; import { AssetBundle } from "$lib/asset_bundle/bundle.ts";
import { FileMeta } from "$sb/types.ts"; import { FileMeta } from "$sb/types.ts";
import { ShellRequest } from "$type/rpc.ts"; import { ShellRequest } from "$type/rpc.ts";
@ -286,25 +286,44 @@ export class HttpServer {
"/.auth", "/.auth",
]; ];
// Middleware handling the /.auth page and flow // TODO: This should probably be a POST request
this.app.all("/.auth", async (c) => { this.app.get("/.logout", async (c) => {
const url = new URL(c.req.url); const url = new URL(c.req.url);
const req = c.req; deleteCookie(c, authCookieName(url.host));
const host = url.host; // e.g. localhost:3000
if (url.search === "?logout") { return c.redirect("/.auth");
deleteCookie(c, authCookieName(host)); });
}
if (req.method === "GET") { this.app.get("/.auth", async (c) => {
return c.html( const html = this.clientAssetBundle.readTextFileSync(".client/auth.html");
this.clientAssetBundle.readTextFileSync(".client/auth.html"),
); return c.html(html);
} else if (req.method === "POST") { }).post(
const values = await c.req.parseBody(); validator("form", (value, c) => {
const username = values["username"]; const username = value["username"];
const password = values["password"]; const password = value["password"];
if (
!username || typeof username !== "string" ||
!password || typeof password !== "string"
) {
return c.redirect("/.auth?error=0");
}
return { username, password };
}),
async (c) => {
const req = c.req;
const url = new URL(c.req.url);
const { username, password } = req.valid("form");
const spaceServer = await this.ensureSpaceServer(req); const spaceServer = await this.ensureSpaceServer(req);
const { user: expectedUser, pass: expectedPassword } = spaceServer
.auth!; const {
user: expectedUser,
pass: expectedPassword,
} = spaceServer.auth!;
if (username === expectedUser && password === expectedPassword) { if (username === expectedUser && password === expectedPassword) {
// Generate a JWT and set it as a cookie // Generate a JWT and set it as a cookie
const jwt = await spaceServer.jwtIssuer.createJWT( const jwt = await spaceServer.jwtIssuer.createJWT(
@ -312,21 +331,23 @@ export class HttpServer {
authenticationExpirySeconds, authenticationExpirySeconds,
); );
console.log("Successful auth"); console.log("Successful auth");
setCookie(c, authCookieName(host), jwt, { setCookie(c, authCookieName(url.host), jwt, {
expires: new Date( expires: new Date(
Date.now() + authenticationExpirySeconds * 1000, Date.now() + authenticationExpirySeconds * 1000,
), // in a week ), // in a week
// sameSite: "Strict", // sameSite: "Strict",
// httpOnly: true, // httpOnly: true,
}); });
return c.redirect("/"); const values = await c.req.parseBody();
const from = values["from"];
return c.redirect(typeof from === "string" ? from : "/");
} else { } else {
console.error("Authentication failed, redirecting to auth page."); console.error("Authentication failed, redirecting to auth page.");
return c.redirect("/.auth?error=1"); return c.redirect("/.auth?error=1");
} }
} else { },
return c.redirect("/.auth"); ).all(async (c) => {
} return c.redirect("/.auth");
}); });
// Check auth // Check auth
@ -339,6 +360,14 @@ export class HttpServer {
} }
const url = new URL(req.url); const url = new URL(req.url);
const host = url.host; const host = url.host;
const redirectToAuth = () => {
// Try filtering api paths
if (req.path.startsWith("/.") || req.path.endsWith(".md")) {
return c.redirect("/.auth");
} else {
return c.redirect(`/.auth?from=${req.path}`);
}
};
if (!excludedPaths.includes(url.pathname)) { if (!excludedPaths.includes(url.pathname)) {
const authCookie = getCookie(c, authCookieName(host)); const authCookie = getCookie(c, authCookieName(host));
@ -360,7 +389,7 @@ export class HttpServer {
} }
if (!authCookie) { if (!authCookie) {
console.log("Unauthorized access, redirecting to auth page"); console.log("Unauthorized access, redirecting to auth page");
return c.redirect("/.auth"); return redirectToAuth();
} }
const { user: expectedUser } = spaceServer.auth!; const { user: expectedUser } = spaceServer.auth!;
@ -376,7 +405,7 @@ export class HttpServer {
"Error verifying JWT, redirecting to auth page", "Error verifying JWT, redirecting to auth page",
e.message, e.message,
); );
return c.redirect("/.auth"); return redirectToAuth();
} }
} }
return next(); return next();
@ -395,24 +424,21 @@ export class HttpServer {
); );
// File list // File list
this.app.get( this.app.get("/index.json", async (c) => {
"/index.json", const req = c.req;
async (c) => { const spaceServer = await this.ensureSpaceServer(req);
const req = c.req; if (req.header("X-Sync-Mode")) {
const spaceServer = await this.ensureSpaceServer(req); // Only handle direct requests for a JSON representation of the file list
if (req.header("X-Sync-Mode")) { const files = await spaceServer.spacePrimitives.fetchFileList();
// Only handle direct requests for a JSON representation of the file list return c.json(files, 200, {
const files = await spaceServer.spacePrimitives.fetchFileList(); "X-Space-Path": spaceServer.pagesPath,
return c.json(files, 200, { });
"X-Space-Path": spaceServer.pagesPath, } else {
}); // Otherwise, redirect to the UI
} else { // The reason to do this is to handle authentication systems like Authelia nicely
// Otherwise, redirect to the UI return c.redirect("/");
// The reason to do this is to handle authentication systems like Authelia nicely }
return c.redirect("/"); });
}
},
);
// RPC shell // RPC shell
this.app.post("/.rpc/shell", async (c) => { this.app.post("/.rpc/shell", async (c) => {
@ -466,97 +492,92 @@ export class HttpServer {
const filePathRegex = "/:path{[^!].*\\.[a-zA-Z]+}"; const filePathRegex = "/:path{[^!].*\\.[a-zA-Z]+}";
const mdExt = ".md"; const mdExt = ".md";
this.app.get( this.app.get(filePathRegex, async (c) => {
filePathRegex, const req = c.req;
async (c) => { const name = req.param("path")!;
const req = c.req; const spaceServer = await this.ensureSpaceServer(req);
const name = req.param("path")!; console.log("Requested file", name);
const spaceServer = await this.ensureSpaceServer(req);
console.log( if (
"Requested file", name.endsWith(mdExt) &&
name, // This header signififies the requests comes directly from the http_space_primitives client (not the browser)
!req.header("X-Sync-Mode") &&
// This Accept header is used by federation to still work with CORS
req.header("Accept") !==
"application/octet-stream" &&
req.header("sec-fetch-mode") !== "cors"
) {
// It can happen that during a sync, authentication expires, this may result in a redirect to the login page and then back to this particular file. This particular file may be an .md file, which isn't great to show so we're redirecting to the associated SB UI page.
console.warn(
"Request was without X-Sync-Mode nor a CORS request, redirecting to page",
); );
if ( return c.redirect(`/${name.slice(0, -mdExt.length)}`, 401);
name.endsWith(mdExt) && }
// This header signififies the requests comes directly from the http_space_primitives client (not the browser) if (name.startsWith(".")) {
!req.header("X-Sync-Mode") && // Don't expose hidden files
// This Accept header is used by federation to still work with CORS return c.notFound();
req.header("Accept") !== }
"application/octet-stream" && // Handle federated links through a simple redirect, only used for attachments loads with service workers disabled
req.header("sec-fetch-mode") !== "cors" if (name.startsWith("!")) {
) { let url = name.slice(1);
// It can happen that during a sync, authentication expires, this may result in a redirect to the login page and then back to this particular file. This particular file may be an .md file, which isn't great to show so we're redirecting to the associated SB UI page. console.log("Handling this as a federated link", url);
console.warn( if (url.startsWith("localhost")) {
"Request was without X-Sync-Mode nor a CORS request, redirecting to page", url = `http://${url}`;
); } else {
return c.redirect(`/${name.slice(0, -mdExt.length)}`, 401); url = `https://${url}`;
} }
if (name.startsWith(".")) {
// Don't expose hidden files
return c.notFound();
}
// Handle federated links through a simple redirect, only used for attachments loads with service workers disabled
if (name.startsWith("!")) {
let url = name.slice(1);
console.log("Handling this as a federated link", url);
if (url.startsWith("localhost")) {
url = `http://${url}`;
} else {
url = `https://${url}`;
}
try {
const req = await fetch(url);
// Override X-Permssion header to always be "ro"
const newHeaders = new Headers();
for (const [key, value] of req.headers.entries()) {
newHeaders.set(key, value);
}
newHeaders.set("X-Permission", "ro");
return new Response(req.body, {
status: req.status,
headers: newHeaders,
});
} catch (e: any) {
console.error("Error fetching federated link", e);
return c.text(e.message, 500);
}
}
const filename = path.posix.basename(name, mdExt);
if (filename.trim() !== filename) {
const newName = path.posix.join(
path.posix.dirname(name),
filename.trim(),
);
return c.redirect(`/${newName}`);
}
try { try {
if (req.header("X-Get-Meta")) { const req = await fetch(url);
// Getting meta via GET request // Override X-Permssion header to always be "ro"
const fileData = await spaceServer.spacePrimitives.getFileMeta( const newHeaders = new Headers();
name, for (const [key, value] of req.headers.entries()) {
); newHeaders.set(key, value);
return c.text("", 200, this.fileMetaToHeaders(fileData));
} }
const fileData = await spaceServer.spacePrimitives.readFile(name); newHeaders.set("X-Permission", "ro");
const lastModifiedHeader = new Date(fileData.meta.lastModified) return new Response(req.body, {
.toUTCString(); status: req.status,
if ( headers: newHeaders,
req.header("If-Modified-Since") === lastModifiedHeader
) {
return c.body(null, 304);
}
return c.body(fileData.data, 200, {
...this.fileMetaToHeaders(fileData.meta),
"Last-Modified": lastModifiedHeader,
}); });
} catch (e: any) { } catch (e: any) {
console.error("Error GETting file", name, e.message); console.error("Error fetching federated link", e);
return c.notFound(); return c.text(e.message, 500);
} }
}, }
).put(
const filename = path.posix.basename(name, mdExt);
if (filename.trim() !== filename) {
const newName = path.posix.join(
path.posix.dirname(name),
filename.trim(),
);
return c.redirect(`/${newName}`);
}
try {
if (req.header("X-Get-Meta")) {
// Getting meta via GET request
const fileData = await spaceServer.spacePrimitives.getFileMeta(
name,
);
return c.text("", 200, this.fileMetaToHeaders(fileData));
}
const fileData = await spaceServer.spacePrimitives.readFile(name);
const lastModifiedHeader = new Date(fileData.meta.lastModified)
.toUTCString();
if (
req.header("If-Modified-Since") === lastModifiedHeader
) {
return c.body(null, 304);
}
return c.body(fileData.data, 200, {
...this.fileMetaToHeaders(fileData.meta),
"Last-Modified": lastModifiedHeader,
});
} catch (e: any) {
console.error("Error GETting file", name, e.message);
return c.notFound();
}
}).put(
async (c) => { async (c) => {
const req = c.req; const req = c.req;
const name = req.param("path")!; const name = req.param("path")!;

View File

@ -76,10 +76,23 @@
<script> <script>
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const error = params.get('error'); const error = params.get('error');
if (error === "1") { if (error === "0") {
document.querySelector('.error-message').innerText = "The sent data was invalid";
} else if (error === "1") {
document.querySelector('.error-message').innerText = "Invalid username or password"; document.querySelector('.error-message').innerText = "Invalid username or password";
} }
const from = params.get("from");
if (from) {
var input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", "from");
input.setAttribute("value", from);
document.getElementById("login").appendChild(input);
}
</script> </script>
</body> </body>

View File

@ -97,7 +97,11 @@ self.addEventListener("fetch", (event: any) => {
const pathname = requestUrl.pathname; const pathname = requestUrl.pathname;
if (pathname === "/.auth" || pathname === "/index.json") { if (
pathname === "/.auth" ||
pathname === "/.logout" ||
pathname === "/index.json"
) {
return fetch(request); return fetch(request);
} else if (/\/.+\.[a-zA-Z]+$/.test(pathname)) { } else if (/\/.+\.[a-zA-Z]+$/.test(pathname)) {
// If this is a /*.* request, this can either be a plug worker load or an attachment load // If this is a /*.* request, this can either be a plug worker load or an attachment load