@auth0/nextjs-auth0
Version:
Auth0 Next.js SDK
751 lines (750 loc) • 33.3 kB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { generateSecret } from "../../test/utils.js";
import { decrypt, encrypt, RequestCookies, ResponseCookies, sign } from "../cookies.js";
import { LEGACY_COOKIE_NAME } from "./normalize-session.js";
import { StatefulSessionStore } from "./stateful-session-store.js";
describe("Stateful Session Store", async () => {
describe("get", async () => {
it("should call the store.get method with the session ID", async () => {
const sessionId = "ses_123";
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt: Math.floor(Date.now() / 1000)
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const maxAge = 60 * 60; // 1 hour in seconds
const expiration = Math.floor(Date.now() / 1000 + maxAge);
const encryptedCookieValue = await encrypt({
id: sessionId
}, secret, expiration);
const headers = new Headers();
headers.append("cookie", `__session=${encryptedCookieValue}`);
const requestCookies = new RequestCookies(headers);
const sessionStore = new StatefulSessionStore({
secret,
store
});
const sessionFromDb = await sessionStore.get(requestCookies);
expect(store.get).toHaveBeenCalledOnce();
expect(store.get).toHaveBeenCalledWith(sessionId);
expect(sessionFromDb).toEqual(session);
});
it("should return null if no session cookie exists", async () => {
const secret = await generateSecret(32);
const headers = new Headers();
const requestCookies = new RequestCookies(headers);
const store = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn()
};
const sessionStore = new StatefulSessionStore({
secret,
store
});
expect(await sessionStore.get(requestCookies)).toBeNull();
});
it("should return null if no matching session exists in the DB", async () => {
const sessionId = "ses_does_not_exist";
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt: Math.floor(Date.now() / 1000)
}
};
const store = {
get: vi.fn().mockImplementation(async (sessionId) => {
if (sessionId === "ses_123") {
return session;
}
return null;
}),
set: vi.fn(),
delete: vi.fn()
};
const maxAge = 60 * 60; // 1 hour in seconds
const expiration = Math.floor(Date.now() / 1000 + maxAge);
const encryptedCookieValue = await encrypt({
id: sessionId
}, secret, expiration);
const headers = new Headers();
headers.append("cookie", `__session=${encryptedCookieValue}`);
const requestCookies = new RequestCookies(headers);
const sessionStore = new StatefulSessionStore({
secret,
store
});
const sessionFromDb = await sessionStore.get(requestCookies);
expect(store.get).toHaveBeenCalledOnce();
expect(store.get).toHaveBeenCalledWith(sessionId);
expect(sessionFromDb).toBeNull();
});
describe("migrate legacy session", async () => {
it("should convert the legacy session to the new format", async () => {
const sessionId = "ses_123";
const secret = await generateSecret(32);
const legacySession = {
header: {
iat: Math.floor(Date.now() / 1000),
uat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000)
},
data: {
user: {
sub: "user_123",
sid: "auth0-sid"
},
accessToken: "at_123",
accessTokenScope: "openid profile email",
refreshToken: "rt_123",
accessTokenExpiresAt: 123456
}
};
const store = {
get: vi.fn().mockResolvedValue(legacySession),
set: vi.fn(),
delete: vi.fn()
};
const signedCookieValue = await sign("appSession", sessionId, secret);
const headers = new Headers();
headers.append("cookie", `appSession=${signedCookieValue}`);
const requestCookies = new RequestCookies(headers);
const sessionStore = new StatefulSessionStore({
secret,
store
});
const sessionFromDb = await sessionStore.get(requestCookies);
expect(store.get).toHaveBeenCalledOnce();
expect(store.get).toHaveBeenCalledWith(sessionId);
expect(sessionFromDb).toEqual({
user: { sub: "user_123", sid: "auth0-sid" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456,
scope: "openid profile email"
},
internal: {
sid: "auth0-sid",
createdAt: legacySession.header.iat
}
});
});
it("should discard any missing properties", async () => {
const sessionId = "ses_123";
const secret = await generateSecret(32);
const legacySession = {
header: {
iat: Math.floor(Date.now() / 1000),
uat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000)
},
data: {
user: {
sub: "user_123",
sid: "auth0-sid"
}
}
};
const store = {
get: vi.fn().mockResolvedValue(legacySession),
set: vi.fn(),
delete: vi.fn()
};
const signedCookieValue = await sign("appSession", sessionId, secret);
const headers = new Headers();
headers.append("cookie", `appSession=${signedCookieValue}`);
const requestCookies = new RequestCookies(headers);
const sessionStore = new StatefulSessionStore({
secret,
store
});
const sessionFromDb = await sessionStore.get(requestCookies);
expect(store.get).toHaveBeenCalledOnce();
expect(store.get).toHaveBeenCalledWith(sessionId);
expect(sessionFromDb).toEqual({
user: { sub: "user_123", sid: "auth0-sid" },
tokenSet: {},
internal: {
sid: "auth0-sid",
createdAt: legacySession.header.iat
}
});
});
it("should convert legacy sessions with custom cookie names", async () => {
const cookieName = "customSession";
const sessionId = "ses_123";
const secret = await generateSecret(32);
const legacySession = {
header: {
iat: Math.floor(Date.now() / 1000),
uat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000)
},
data: {
user: {
sub: "user_123",
sid: "auth0-sid"
},
accessToken: "at_123",
accessTokenScope: "openid profile email",
refreshToken: "rt_123",
accessTokenExpiresAt: 123456
}
};
const store = {
get: vi.fn().mockResolvedValue(legacySession),
set: vi.fn(),
delete: vi.fn()
};
const signedCookieValue = await sign(cookieName, sessionId, secret);
const headers = new Headers();
headers.append("cookie", `${cookieName}=${signedCookieValue}`);
const requestCookies = new RequestCookies(headers);
const sessionStore = new StatefulSessionStore({
secret,
store,
cookieOptions: {
name: cookieName
}
});
const sessionFromDb = await sessionStore.get(requestCookies);
expect(store.get).toHaveBeenCalledOnce();
expect(store.get).toHaveBeenCalledWith(sessionId);
expect(sessionFromDb).toEqual({
user: { sub: "user_123", sid: "auth0-sid" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456,
scope: "openid profile email"
},
internal: {
sid: "auth0-sid",
createdAt: legacySession.header.iat
}
});
});
});
});
describe("set", async () => {
describe("with rolling sessions enabled", async () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should extend the cookie lifetime by the inactivity duration", async () => {
const currentTime = Date.now();
const createdAt = Math.floor(currentTime / 1000);
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store,
rolling: true,
absoluteDuration: 3600,
inactivityDuration: 1800
});
await sessionStore.set(requestCookies, responseCookies, session);
const cookie = responseCookies.get("__session");
const { payload: cookieValue } = (await decrypt(cookie.value, secret));
expect(cookie).toBeDefined();
expect(cookieValue).toHaveProperty("id");
expect(cookie?.path).toEqual("/");
expect(cookie?.httpOnly).toEqual(true);
expect(cookie?.sameSite).toEqual("lax");
expect(cookie?.maxAge).toEqual(1800);
expect(cookie?.secure).toEqual(false);
expect(store.set).toHaveBeenCalledOnce();
expect(store.set).toHaveBeenCalledWith(cookieValue.id, session);
});
it("should not exceed the absolute timeout duration", async () => {
const currentTime = Date.now();
const createdAt = Math.floor(currentTime / 1000);
const secret = await generateSecret(32);
const session = {
createdAt,
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store,
rolling: true,
absoluteDuration: 3600,
inactivityDuration: 1800
});
// advance time by 2 hours - session should expire after 1 hour
vi.setSystemTime(currentTime + 2 * 3600 * 1000);
await sessionStore.set(requestCookies, responseCookies, session);
const cookie = responseCookies.get("__session");
const { payload: cookieValue } = (await decrypt(cookie.value, secret));
expect(cookie).toBeDefined();
expect(cookieValue).toHaveProperty("id");
expect(cookie?.path).toEqual("/");
expect(cookie?.httpOnly).toEqual(true);
expect(cookie?.sameSite).toEqual("lax");
expect(cookie?.maxAge).toEqual(0); // cookie should expire immedcreatedAtely
expect(cookie?.secure).toEqual(false);
expect(store.set).toHaveBeenCalledOnce();
expect(store.set).toHaveBeenCalledWith(cookieValue.id, session);
});
});
describe("with rolling sessions disabled", async () => {
it("should set the cookie with a maxAge of the absolute session duration and call store.set", async () => {
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt: Math.floor(Date.now() / 1000)
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store,
rolling: false,
absoluteDuration: 3600
});
await sessionStore.set(requestCookies, responseCookies, session);
const cookie = responseCookies.get("__session");
const { payload: cookieValue } = (await decrypt(cookie.value, secret));
expect(cookie).toBeDefined();
expect(cookieValue).toHaveProperty("id");
expect(cookie?.path).toEqual("/");
expect(cookie?.httpOnly).toEqual(true);
expect(cookie?.sameSite).toEqual("lax");
expect(cookie?.maxAge).toEqual(3600);
expect(cookie?.secure).toEqual(false);
expect(store.set).toHaveBeenCalledOnce();
expect(store.set).toHaveBeenCalledWith(cookieValue.id, session);
});
});
describe("session fixation", async () => {
it("should generate a new session ID if the session is new", async () => {
const sessionId = "ses_123";
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt: Math.floor(Date.now() / 1000)
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const maxAge = 60 * 60; // 1 hour in seconds
const expiration = Math.floor(Date.now() / 1000 + maxAge);
const encryptedCookieValue = await encrypt({
id: sessionId
}, secret, expiration);
const headers = new Headers();
headers.append("cookie", `__session=${encryptedCookieValue}`);
const requestCookies = new RequestCookies(headers);
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store,
rolling: false,
absoluteDuration: 3600
});
await sessionStore.set(requestCookies, responseCookies, session, true);
const cookie = responseCookies.get("__session");
const { payload: cookieValue } = (await decrypt(cookie.value, secret));
expect(cookie).toBeDefined();
expect(store.delete).toHaveBeenCalledWith(sessionId); // the old session should be deleted
expect(store.set).toHaveBeenCalledOnce();
expect(store.set).toHaveBeenCalledWith(cookieValue.id, session); // a new session ID should be generated
});
});
describe("with cookieOptions", async () => {
it("should apply the secure attribute to the cookie", async () => {
const currentTime = Date.now();
const createdAt = Math.floor(currentTime / 1000);
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store,
rolling: true,
absoluteDuration: 3600,
inactivityDuration: 1800,
cookieOptions: {
secure: true
}
});
await sessionStore.set(requestCookies, responseCookies, session);
const cookie = responseCookies.get("__session");
const { payload: cookieValue } = (await decrypt(cookie.value, secret));
expect(cookie).toBeDefined();
expect(cookieValue).toHaveProperty("id");
expect(cookie?.path).toEqual("/");
expect(cookie?.httpOnly).toEqual(true);
expect(cookie?.sameSite).toEqual("lax");
expect(cookie?.maxAge).toEqual(1800);
expect(cookie?.secure).toEqual(true);
});
it("should apply the sameSite attribute to the cookie", async () => {
const currentTime = Date.now();
const createdAt = Math.floor(currentTime / 1000);
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store,
rolling: true,
absoluteDuration: 3600,
inactivityDuration: 1800,
cookieOptions: {
sameSite: "strict"
}
});
await sessionStore.set(requestCookies, responseCookies, session);
const cookie = responseCookies.get("__session");
const { payload: cookieValue } = (await decrypt(cookie.value, secret));
expect(cookie).toBeDefined();
expect(cookieValue).toHaveProperty("id");
expect(cookie?.path).toEqual("/");
expect(cookie?.httpOnly).toEqual(true);
expect(cookie?.sameSite).toEqual("strict");
expect(cookie?.maxAge).toEqual(1800);
expect(cookie?.secure).toEqual(false);
});
it("should apply the path to the cookie", async () => {
const currentTime = Date.now();
const createdAt = Math.floor(currentTime / 1000);
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store,
rolling: true,
absoluteDuration: 3600,
inactivityDuration: 1800,
cookieOptions: {
path: "/custom-path"
}
});
await sessionStore.set(requestCookies, responseCookies, session);
const cookie = responseCookies.get("__session");
expect(cookie).toBeDefined();
expect(cookie?.path).toEqual("/custom-path");
});
it("should apply the cookie name", async () => {
const currentTime = Date.now();
const createdAt = Math.floor(currentTime / 1000);
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store,
rolling: true,
absoluteDuration: 3600,
inactivityDuration: 1800,
cookieOptions: {
name: "my-session"
}
});
await sessionStore.set(requestCookies, responseCookies, session);
const cookie = responseCookies.get("my-session");
const { payload: cookieValue } = (await decrypt(cookie.value, secret));
expect(cookie).toBeDefined();
expect(cookieValue).toHaveProperty("id");
expect(cookie?.path).toEqual("/");
expect(cookie?.httpOnly).toEqual(true);
expect(cookie?.sameSite).toEqual("lax");
expect(cookie?.maxAge).toEqual(1800);
expect(cookie?.secure).toEqual(false);
});
});
it("should remove the legacy cookie if it exists", async () => {
const currentTime = Date.now();
const createdAt = Math.floor(currentTime / 1000);
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt
}
};
const store = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store
});
vi.spyOn(requestCookies, "has").mockReturnValue(true);
vi.spyOn(responseCookies, "set");
await sessionStore.set(requestCookies, responseCookies, session);
expect(responseCookies.set).toHaveBeenCalledWith(LEGACY_COOKIE_NAME, "", {
maxAge: 0,
path: "/"
});
});
it("should not delete the legacy cookie if session cookie name matches LEGACY_COOKIE_NAME", async () => {
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt: Math.floor(Date.now() / 1000)
}
};
const store = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
// Pretend the legacy cookie is already present
vi.spyOn(requestCookies, "has").mockReturnValue(true);
const deleteSpy = vi.spyOn(responseCookies, "delete");
const sessionStore = new StatefulSessionStore({
secret,
store,
cookieOptions: { name: LEGACY_COOKIE_NAME } // 👈 simulate legacy name
});
await sessionStore.set(requestCookies, responseCookies, session);
expect(deleteSpy).not.toHaveBeenCalled();
});
});
describe("delete", async () => {
it("should remove the cookie and call store.delete with the session ID", async () => {
const sessionId = "ses_123";
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt: Math.floor(Date.now() / 1000)
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const maxAge = 60 * 60; // 1 hour in seconds
const expiration = Math.floor(Date.now() / 1000 + maxAge);
const encryptedCookieValue = await encrypt({
id: sessionId
}, secret, expiration);
const headers = new Headers();
headers.append("cookie", `__session=${encryptedCookieValue}`);
const requestCookies = new RequestCookies(headers);
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store
});
await sessionStore.set(requestCookies, responseCookies, session);
expect(responseCookies.get("__session")).toBeDefined();
await sessionStore.delete(requestCookies, responseCookies);
const cookie = responseCookies.get("__session");
expect(cookie?.value).toEqual("");
expect(cookie?.maxAge).toEqual(0);
expect(store.delete).toHaveBeenCalledOnce();
expect(store.delete).toHaveBeenCalledWith(sessionId);
});
it("should not throw an error if the cookie does not exist", async () => {
const secret = await generateSecret(32);
const session = {
user: { sub: "user_123" },
tokenSet: {
accessToken: "at_123",
refreshToken: "rt_123",
expiresAt: 123456
},
internal: {
sid: "auth0-sid",
createdAt: Math.floor(Date.now() / 1000)
}
};
const store = {
get: vi.fn().mockResolvedValue(session),
set: vi.fn(),
delete: vi.fn()
};
const requestCookies = new RequestCookies(new Headers());
const responseCookies = new ResponseCookies(new Headers());
const sessionStore = new StatefulSessionStore({
secret,
store
});
await sessionStore.delete(requestCookies, responseCookies);
const cookie = responseCookies.get("__session");
expect(cookie?.value).toEqual("");
expect(cookie?.maxAge).toEqual(0);
expect(store.delete).not.toHaveBeenCalled();
});
});
});