2 Commits
v1.1.0 ... main

Author SHA1 Message Date
Raul Lugo
04f00993db 1.1.1 2026-01-29 17:43:53 +01:00
Raul Lugo
81239dc6ab fix: safe redirect if return_to is protected after logout 2026-01-29 17:43:47 +01:00
6 changed files with 64 additions and 11 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@resuely/astro-oidc-rp", "name": "@resuely/astro-oidc-rp",
"version": "1.1.0", "version": "1.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@resuely/astro-oidc-rp", "name": "@resuely/astro-oidc-rp",
"version": "1.1.0", "version": "1.1.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jose": "^5.0.0" "jose": "^5.0.0"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@resuely/astro-oidc-rp", "name": "@resuely/astro-oidc-rp",
"version": "1.1.0", "version": "1.1.1",
"description": "Astro integration providing OIDC relying-party routes, middleware, and types.", "description": "Astro integration providing OIDC relying-party routes, middleware, and types.",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -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,
};
} }
} }

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;
} }
} }
} }