UNPKG

@auth0/nextjs-auth0

Version:
205 lines (204 loc) 8.12 kB
import { NextRequest } from "next/server.js"; import * as jose from "jose"; import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { getDefaultRoutes } from "../test/defaults.js"; import { generateSecret } from "../test/utils.js"; import { AuthClient } from "./auth-client.js"; import { decrypt, encrypt } from "./cookies.js"; import { StatelessSessionStore } from "./session/stateless-session-store.js"; import { TransactionStore } from "./transaction-store.js"; /** * Test suite for the beforeSessionSaved hook. */ // Test constants const domain = "guabu.us.auth0.com"; const clientId = "client_123"; const clientSecret = "client-secret"; const appBaseUrl = "https://example.com"; const accessToken = "at_123"; const refreshToken = "rt_123"; const sub = "user_123"; const sid = "auth0-sid"; const alg = "RS256"; // Generate key pair for token signing let keyPair; // OIDC Discovery metadata const discoveryMetadata = { issuer: `https://${domain}/`, authorization_endpoint: `https://${domain}/authorize`, token_endpoint: `https://${domain}/oauth/token`, userinfo_endpoint: `https://${domain}/userinfo`, jwks_uri: `https://${domain}/.well-known/jwks.json`, end_session_endpoint: `https://${domain}/oidc/logout` }; /** * MSW handlers for OAuth2 endpoints. * These are declarative and can be customized per test using server.use() */ const handlers = [ // OIDC Discovery Endpoint http.get(`https://${domain}/.well-known/openid-configuration`, () => { return HttpResponse.json(discoveryMetadata); }), // JWKS Endpoint http.get(`https://${domain}/.well-known/jwks.json`, async () => { const jwk = await jose.exportJWK(keyPair.publicKey); return HttpResponse.json({ keys: [jwk] }); }), // Token Exchange Endpoint http.post(`https://${domain}/oauth/token`, async ({ request }) => { const body = await request.formData(); const grantType = body.get("grant_type"); // Authorization code grant (login flow) if (grantType === "authorization_code") { const idTokenJwt = await new jose.SignJWT({ sub, sid, nonce: "nonce-value", auth_time: Math.floor(Date.now() / 1000), iss: discoveryMetadata.issuer, aud: clientId }) .setProtectedHeader({ alg }) .setIssuedAt() .setExpirationTime("1h") .sign(keyPair.privateKey); return HttpResponse.json({ access_token: accessToken, refresh_token: refreshToken, id_token: idTokenJwt, token_type: "Bearer", expires_in: 86400 }); } // Refresh token grant (token refresh) if (grantType === "refresh_token") { const newAccessToken = "at_new_refreshed"; const idTokenJwt = await new jose.SignJWT({ sub, sid, auth_time: Math.floor(Date.now() / 1000), iss: discoveryMetadata.issuer, aud: clientId }) .setProtectedHeader({ alg }) .setIssuedAt() .setExpirationTime("1h") .sign(keyPair.privateKey); return HttpResponse.json({ access_token: newAccessToken, refresh_token: refreshToken, id_token: idTokenJwt, token_type: "Bearer", expires_in: 86400 }); } // Unknown grant type return HttpResponse.json({ error: "unsupported_grant_type" }, { status: 400 }); }) ]; // Setup MSW server for all tests in this suite const server = setupServer(...handlers); describe("AuthClient - beforeSessionSaved hook", async () => { beforeAll(async () => { // Initialize key pair and start MSW server keyPair = await jose.generateKeyPair(alg); server.listen(); }); afterEach(() => { // Reset any custom handlers added in individual tests server.resetHandlers(); }); afterAll(() => { // Clean up MSW server server.close(); }); it("should call beforeSessionSaved with updated tokens after token refresh (handleAccessToken)", async () => { const currentAccessToken = "at_old"; const newAccessToken = "at_new_refreshed"; const secret = await generateSecret(32); const transactionStore = new TransactionStore({ secret }); const sessionStore = new StatelessSessionStore({ secret }); // Track what the hook receives let hookReceivedAccessToken; let hookReceivedSession; const authClient = new AuthClient({ transactionStore, sessionStore, domain: domain, clientId: clientId, clientSecret: clientSecret, secret, appBaseUrl: appBaseUrl, routes: getDefaultRoutes(), beforeSessionSaved: async (session) => { // Capture what the hook receives hookReceivedAccessToken = session.tokenSet?.accessToken; hookReceivedSession = session; // Hook can modify the session return { ...session, user: { ...session.user, enriched: true } }; } }); // Create an expired session that needs token refresh const expiresAt = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60; // expired 10 days ago const originalSession = { user: { sub: sub, name: "John Doe", email: "john@example.com", picture: "https://example.com/john.jpg" }, tokenSet: { accessToken: currentAccessToken, scope: "openid profile email", refreshToken: refreshToken, expiresAt }, internal: { sid: 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(originalSession, secret, expiration); const headers = new Headers(); headers.append("cookie", `__session=${sessionCookie}`); const request = new NextRequest(new URL("/auth/access-token", appBaseUrl), { method: "GET", headers }); const response = await authClient.handleAccessToken(request); // Verify the response expect(response.status).toEqual(200); const responseBody = await response.json(); expect(responseBody.token).toEqual(newAccessToken); // The hook should have received the UPDATED access token (not the old one) expect(hookReceivedAccessToken).toEqual(newAccessToken); expect(hookReceivedAccessToken).not.toEqual(currentAccessToken); // The hook should have received the complete updated session expect(hookReceivedSession?.tokenSet?.accessToken).toEqual(newAccessToken); expect(hookReceivedSession?.tokenSet?.refreshToken).toEqual(refreshToken); // Verify the session cookie is updated with hook modifications const updatedSessionCookie = response.cookies.get("__session"); const { payload: updatedSession } = (await decrypt(updatedSessionCookie.value, secret)); // Hook modifications should be persisted expect(updatedSession.user).toEqual(expect.objectContaining({ enriched: true })); // Updated token should be in final session expect(updatedSession.tokenSet.accessToken).toEqual(newAccessToken); }); });