Skip to content

fix: cookie not being deleted correctly when basepath set #2223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/client/helpers/get-access-token.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { normalizeWithBasePath } from "../../utils/pathUtils.js";
import { AccessTokenError } from "../../errors/index.js";
import { normalizeWithBasePath } from "../../utils/pathUtils.js";

type AccessTokenResponse = {
token: string;
Expand Down
2 changes: 1 addition & 1 deletion src/client/hooks/use-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import useSWR from "swr";

import { normalizeWithBasePath } from "../../utils/pathUtils.js";
import type { User } from "../../types/index.js";
import { normalizeWithBasePath } from "../../utils/pathUtils.js";

export function useUser() {
const { data, error, isLoading, mutate } = useSWR<User, Error, string>(
Expand Down
154 changes: 154 additions & 0 deletions src/server/auth-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2357,6 +2357,160 @@ ca/T0LLtgmbMmxSv/MmzIg==
"An error occured while trying to initiate the logout request."
);
});

it("should properly clear session cookies when base path is set", async () => {
// Set up base path
process.env.NEXT_PUBLIC_BASE_PATH = "/dashboard";

const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
secret,
cookieOptions: {
path: "/dashboard"
}
});
const sessionStore = new StatelessSessionStore({
secret,
cookieOptions: {
path: "/dashboard"
}
});
const authClient = new AuthClient({
transactionStore,
sessionStore,

domain: DEFAULT.domain,
clientId: DEFAULT.clientId,
clientSecret: DEFAULT.clientSecret,

secret,
appBaseUrl: DEFAULT.appBaseUrl,

fetch: getMockAuthorizationServer()
});

// set the session cookie with base path
const session: SessionData = {
user: { sub: DEFAULT.sub },
tokenSet: {
idToken: DEFAULT.idToken,
accessToken: DEFAULT.accessToken,
refreshToken: DEFAULT.refreshToken,
expiresAt: 123456
},
internal: {
sid: DEFAULT.sid,
createdAt: Math.floor(Date.now() / 1000)
}
};
const maxAge = 60 * 60; // 1 hour
const expiration = Math.floor(Date.now() / 1000 + maxAge);
const sessionCookie = await encrypt(session, secret, expiration);
const headers = new Headers();
headers.append("cookie", `__session=${sessionCookie}`);
const request = new NextRequest(
new URL("/dashboard/auth/logout", DEFAULT.appBaseUrl),
{
method: "GET",
headers
}
);

const response = await authClient.handleLogout(request);
expect(response.status).toEqual(307);
expect(response.headers.get("Location")).not.toBeNull();

const authorizationUrl = new URL(response.headers.get("Location")!);
expect(authorizationUrl.origin).toEqual(`https://${DEFAULT.domain}`);

// session cookie should be cleared with the correct path
const cookie = response.cookies.get("__session");
expect(cookie?.value).toEqual("");
expect(cookie?.maxAge).toEqual(0);
expect(cookie?.path).toEqual("/dashboard");

// Clean up
delete process.env.NEXT_PUBLIC_BASE_PATH;
});

it("should handle logout with base path and transaction cookies", async () => {
// Set up base path
process.env.NEXT_PUBLIC_BASE_PATH = "/dashboard";

const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
secret,
cookieOptions: {
path: "/dashboard"
}
});
const sessionStore = new StatelessSessionStore({
secret,
cookieOptions: {
path: "/dashboard"
}
});
const authClient = new AuthClient({
transactionStore,
sessionStore,

domain: DEFAULT.domain,
clientId: DEFAULT.clientId,
clientSecret: DEFAULT.clientSecret,

secret,
appBaseUrl: DEFAULT.appBaseUrl,

fetch: getMockAuthorizationServer()
});

// Create request with session and transaction cookies
const session: SessionData = {
user: { sub: DEFAULT.sub },
tokenSet: {
idToken: DEFAULT.idToken,
accessToken: DEFAULT.accessToken,
refreshToken: DEFAULT.refreshToken,
expiresAt: 123456
},
internal: {
sid: DEFAULT.sid,
createdAt: Math.floor(Date.now() / 1000)
}
};
const maxAge = 60 * 60; // 1 hour
const expiration = Math.floor(Date.now() / 1000 + maxAge);
const sessionCookie = await encrypt(session, secret, expiration);
const transactionCookie = await encrypt(
{ state: "test-state" },
secret,
expiration

const headers = new Headers();
headers.append(
"cookie",
`__session=${sessionCookie}; __txn_test-state=${transactionCookie}`
);
const request = new NextRequest(
new URL("/dashboard/auth/logout", DEFAULT.appBaseUrl),
{
method: "GET",
headers
}
);

const response = await authClient.handleLogout(request);
expect(response.status).toEqual(307);

// Both session and transaction cookies should be cleared with correct path
const sessionCookieResponse = response.cookies.get("__session");
expect(sessionCookieResponse?.value).toEqual("");
expect(sessionCookieResponse?.maxAge).toEqual(0);
expect(sessionCookieResponse?.path).toEqual("/dashboard");

// Clean up
delete process.env.NEXT_PUBLIC_BASE_PATH;
});
});

describe("handleProfile", async () => {
Expand Down
36 changes: 28 additions & 8 deletions src/server/auth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,7 @@ export interface AuthClientOptions {
}

function createRouteUrl(path: string, baseUrl: string) {
return new URL(
ensureNoLeadingSlash(normalizeWithBasePath(path)),
ensureTrailingSlash(baseUrl)
);
return new URL(ensureNoLeadingSlash(path), ensureTrailingSlash(baseUrl));
}

export class AuthClient {
Expand Down Expand Up @@ -286,7 +283,21 @@ export class AuthClient {

async handler(req: NextRequest): Promise<NextResponse> {
const { pathname } = req.nextUrl;
const sanitizedPathname = removeTrailingSlash(pathname);

// Strip base path from pathname if it exists
// This simulates what Next.js middleware does in a real application
let processedPathname = pathname;
const basePath = process.env.NEXT_PUBLIC_BASE_PATH;
if (basePath) {
const normalizedBasePath = basePath.startsWith("/")
? basePath
: `/${basePath}`;
if (pathname.startsWith(normalizedBasePath)) {
processedPathname = pathname.slice(normalizedBasePath.length) || "/";
}
}

const sanitizedPathname = removeTrailingSlash(processedPathname);
const method = req.method;

if (method === "GET" && sanitizedPathname === this.routes.login) {
Expand Down Expand Up @@ -331,7 +342,10 @@ export class AuthClient {
async startInteractiveLogin(
options: StartInteractiveLoginOptions = {}
): Promise<NextResponse> {
const redirectUri = createRouteUrl(this.routes.callback, this.appBaseUrl); // must be registed with the authorization server
const redirectUri = createRouteUrl(
normalizeWithBasePath(this.routes.callback),
this.appBaseUrl
); // must be registed with the authorization server
let returnTo = this.signInReturnToPath;

// Validate returnTo parameter
Expand Down Expand Up @@ -560,7 +574,10 @@ export class AuthClient {

let codeGrantResponse: Response;
try {
const redirectUri = createRouteUrl(this.routes.callback, this.appBaseUrl); // must be registed with the authorization server
const redirectUri = createRouteUrl(
normalizeWithBasePath(this.routes.callback),
this.appBaseUrl
); // must be registed with the authorization server
codeGrantResponse = await oauth.authorizationCodeGrantRequest(
authorizationServerMetadata,
this.clientMetadata,
Expand Down Expand Up @@ -906,7 +923,10 @@ export class AuthClient {
}

const res = NextResponse.redirect(
createRouteUrl(ctx.returnTo || "/", this.appBaseUrl)
createRouteUrl(
normalizeWithBasePath(ctx.returnTo || "/"),
this.appBaseUrl
)
);

return res;
Expand Down
Loading