fix: safe redirect if return_to is protected after logout
This commit is contained in:
@@ -21,6 +21,11 @@ function escapeRegex(str: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function globToRegExp(pattern: string): RegExp {
|
function globToRegExp(pattern: string): RegExp {
|
||||||
|
if (pattern.endsWith("/*")) {
|
||||||
|
const base = pattern.slice(0, -2);
|
||||||
|
const baseEscaped = escapeRegex(base);
|
||||||
|
return new RegExp(`^${baseEscaped}(?:/.*)?$`);
|
||||||
|
}
|
||||||
const escaped = pattern.split("*").map(escapeRegex).join(".*");
|
const escaped = pattern.split("*").map(escapeRegex).join(".*");
|
||||||
return new RegExp(`^${escaped}$`);
|
return new RegExp(`^${escaped}$`);
|
||||||
}
|
}
|
||||||
@@ -38,12 +43,17 @@ export const onRequest: MiddlewareHandler = async (context, next) => {
|
|||||||
|
|
||||||
locals.user = null;
|
locals.user = null;
|
||||||
if (token) {
|
if (token) {
|
||||||
const res = await verifyAndDecode<{ sub: string; email?: string }>(
|
const res = await verifyAndDecode<{
|
||||||
token,
|
sub: string;
|
||||||
options.cookie.signingSecret,
|
email?: string;
|
||||||
);
|
firstName?: string;
|
||||||
|
}>(token, options.cookie.signingSecret);
|
||||||
if (res.valid && res.payload && typeof res.payload.sub === "string") {
|
if (res.valid && res.payload && typeof res.payload.sub === "string") {
|
||||||
locals.user = { sub: res.payload.sub, email: res.payload.email };
|
locals.user = {
|
||||||
|
sub: res.payload.sub,
|
||||||
|
email: res.payload.email,
|
||||||
|
firstName: res.payload.firstName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,9 +127,13 @@ export async function GET(ctx: APIContext) {
|
|||||||
audience: options.clientId,
|
audience: options.clientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const givenName =
|
||||||
|
typeof payload["given_name"] === "string" ? payload["given_name"] : undefined;
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
sub: String(payload.sub),
|
sub: String(payload.sub),
|
||||||
email: typeof payload.email === "string" ? payload.email : undefined,
|
email: typeof payload.email === "string" ? payload.email : undefined,
|
||||||
|
firstName: givenName,
|
||||||
};
|
};
|
||||||
const signed = await signPayload(session, options.cookie.signingSecret);
|
const signed = await signPayload(session, options.cookie.signingSecret);
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,39 @@ function safeReturnTo(reqUrl: URL, returnTo: string | undefined): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeRegex(str: string): string {
|
||||||
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function globToRegExp(pattern: string): RegExp {
|
||||||
|
if (pattern.endsWith("/*")) {
|
||||||
|
const base = pattern.slice(0, -2);
|
||||||
|
const baseEscaped = escapeRegex(base);
|
||||||
|
return new RegExp(`^${baseEscaped}(?:/.*)?$`);
|
||||||
|
}
|
||||||
|
const escaped = pattern.split("*").map(escapeRegex).join(".*");
|
||||||
|
return new RegExp(`^${escaped}$`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isProtected(pathname: string, patterns: string[]): boolean {
|
||||||
|
return patterns.some((p) => globToRegExp(p).test(pathname));
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizePostLogoutReturnTo(
|
||||||
|
options: ReturnType<typeof getOptions>,
|
||||||
|
reqUrl: URL,
|
||||||
|
returnTo: string | undefined,
|
||||||
|
): string {
|
||||||
|
const safe = safeReturnTo(reqUrl, returnTo);
|
||||||
|
try {
|
||||||
|
const u = new URL(safe, reqUrl);
|
||||||
|
if (isProtected(u.pathname, options.protected)) return new URL("/", reqUrl).toString();
|
||||||
|
return u.toString();
|
||||||
|
} catch {
|
||||||
|
return new URL("/", reqUrl).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function inferPostLogoutRedirectUri(
|
function inferPostLogoutRedirectUri(
|
||||||
options: ReturnType<typeof getOptions>,
|
options: ReturnType<typeof getOptions>,
|
||||||
reqUrl: URL,
|
reqUrl: URL,
|
||||||
@@ -88,7 +121,9 @@ export async function GET(ctx: APIContext) {
|
|||||||
const parsed = cookie ? parseLogoutCookie(cookie) : null;
|
const parsed = cookie ? parseLogoutCookie(cookie) : null;
|
||||||
|
|
||||||
const ok = parsed && parsed.state === state;
|
const ok = parsed && parsed.state === state;
|
||||||
const location = ok ? safeReturnTo(url, parsed.returnTo) : "/";
|
const location = ok
|
||||||
|
? finalizePostLogoutReturnTo(options, url, parsed.returnTo)
|
||||||
|
: "/";
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set("Location", location);
|
headers.set("Location", location);
|
||||||
@@ -115,7 +150,11 @@ export async function GET(ctx: APIContext) {
|
|||||||
disco.end_session_endpoint || new URL("/logout", options.issuer).toString();
|
disco.end_session_endpoint || new URL("/logout", options.issuer).toString();
|
||||||
|
|
||||||
const state = generateState();
|
const state = generateState();
|
||||||
const returnTo = url.searchParams.get("return_to") || undefined;
|
const returnTo = finalizePostLogoutReturnTo(
|
||||||
|
options,
|
||||||
|
url,
|
||||||
|
url.searchParams.get("return_to") || undefined,
|
||||||
|
);
|
||||||
const cookieValue = JSON.stringify({ state, returnTo });
|
const cookieValue = JSON.stringify({ state, returnTo });
|
||||||
const setLogout = serializeCookie(logoutCookieName, cookieValue, {
|
const setLogout = serializeCookie(logoutCookieName, cookieValue, {
|
||||||
path: options.cookie.path,
|
path: options.cookie.path,
|
||||||
|
|||||||
2
types/astro.locals.d.ts
vendored
2
types/astro.locals.d.ts
vendored
@@ -1,7 +1,7 @@
|
|||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
interface Locals {
|
interface Locals {
|
||||||
user?: { sub: string; email?: string } | null;
|
user?: { sub: string; email?: string; firstName?: string } | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user