diff --git a/README.md b/README.md index a895697..9d394a4 100644 --- a/README.md +++ b/README.md @@ -301,27 +301,36 @@ In the above example the `/admin` page will require a user to be signed in, wher `unauthenticatedPaths` uses the same glob logic as the [Next.js matcher](https://nextjs.org/docs/pages/building-your-application/routing/middleware#matcher). -### Retrieve session in middleware +### Composing middleware -Sometimes it's useful to check the user session if you want to compose custom middleware. The `getSession` helper method will retrieve the session from the cookie and verify the access token. +If you don't want to use `authkitMiddleware` and instead want to compose your own middleware, you can use the `authkit` method. In this mode you are responsible to handling what to do when there's no session on a protected route. ```ts -import { authkitMiddleware, getSession } from '@workos-inc/authkit-nextjs'; -import { NextRequest, NextFetchEvent } from 'next/server'; +export default async function middleware(request: NextRequest) { + // Perform logic before or after AuthKit -export default async function middleware(request: NextRequest, event: NextFetchEvent) { - // authkitMiddleware will handle refreshing the session if the access token has expired - const response = await authkitMiddleware()(request, event); + // Auth object contains the session, response headers and an auhorization URL in the case that the session isn't valid + // This method will automatically handle setting the cookie and refreshing the session + const { session, headers, authorizationUrl } = await authkit(request, { + debug: true, + }); - // If session is undefined, the user is not authenticated - const session = await getSession(response); + // Control of what to do when there's no session on a protected route is left to the developer + if (request.url.includes('/account') && !session.user) { + console.log('No session on protected path'); + return NextResponse.redirect(authorizationUrl); - // ...add additional middleware logic here + // Alternatively you could redirect to your own login page, for example if you want to use your own UI instead of hosted AuthKit + return NextResponse.redirect('/login'); + } - return response; + // Headers from the authkit response need to be included in every non-redirect response to ensure that `withAuth` works as expected + return NextResponse.next({ + headers: headers, + }); } -// Match against pages that require auth +// Match against the pages export const config = { matcher: ['/', '/account/:path*'] }; ``` diff --git a/__tests__/session.spec.ts b/__tests__/session.spec.ts index 10c23b2..5425137 100644 --- a/__tests__/session.spec.ts +++ b/__tests__/session.spec.ts @@ -1,11 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; import { cookies, headers } from 'next/headers'; import { redirect } from 'next/navigation'; -import { withAuth, updateSession, refreshSession, getSession, terminateSession } from '../src/session.js'; +import { generateTestToken } from './test-helpers.js'; +import { withAuth, updateSession, refreshSession, terminateSession, updateSessionMiddleware } from '../src/session.js'; import { workos } from '../src/workos.js'; import * as envVariables from '../src/env-variables.js'; -import { jwtVerify, SignJWT } from 'jose'; +import { jwtVerify } from 'jose'; import { sealData } from 'iron-session'; import { User } from '@workos-inc/node'; @@ -147,14 +148,14 @@ describe('session.ts', () => { }); }); - describe('updateSession', () => { + describe('updateSessionMiddleware', () => { it('should throw an error if the redirect URI is not set', async () => { const originalWorkosRedirectUri = envVariables.WORKOS_REDIRECT_URI; jest.replaceProperty(envVariables, 'WORKOS_REDIRECT_URI', ''); await expect(async () => { - await updateSession( + await updateSessionMiddleware( new NextRequest(new URL('http://example.com')), false, { @@ -175,7 +176,7 @@ describe('session.ts', () => { jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', ''); await expect(async () => { - await updateSession( + await updateSessionMiddleware( new NextRequest(new URL('http://example.com')), false, { @@ -198,7 +199,7 @@ describe('session.ts', () => { jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', 'short'); await expect(async () => { - await updateSession( + await updateSessionMiddleware( new NextRequest(new URL('http://example.com')), false, { @@ -217,7 +218,7 @@ describe('session.ts', () => { it('should return early if there is no session', async () => { const request = new NextRequest(new URL('http://example.com')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, false, { @@ -246,7 +247,7 @@ describe('session.ts', () => { }); const request = new NextRequest(new URL('http://example.com')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, true, { @@ -259,7 +260,6 @@ describe('session.ts', () => { expect(result).toBeInstanceOf(NextResponse); expect(result.status).toBe(200); - expect(console.log).toHaveBeenCalledWith('Session is valid'); }); it('should attempt to refresh the session when the access token is invalid', async () => { @@ -283,7 +283,7 @@ describe('session.ts', () => { const request = new NextRequest(new URL('http://example.com')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, true, { @@ -298,7 +298,7 @@ describe('session.ts', () => { expect(console.log).toHaveBeenCalledWith( `Session invalid. Refreshing access token that ends in ${mockSession.accessToken.slice(-10)}`, ); - expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Refresh successful. New access token ends in')); + expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Session successfully refreshed')); }); it('should delete the cookie when refreshing fails', async () => { @@ -322,7 +322,7 @@ describe('session.ts', () => { const request = new NextRequest(new URL('http://example.com')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, true, { @@ -352,7 +352,7 @@ describe('session.ts', () => { jest.spyOn(console, 'log').mockImplementation(() => {}); const request = new NextRequest(new URL('http://example.com/protected')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, true, { @@ -374,7 +374,7 @@ describe('session.ts', () => { (NextResponse as Partial).redirect = undefined; const request = new NextRequest(new URL('http://example.com/protected')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, false, { @@ -393,7 +393,7 @@ describe('session.ts', () => { it('should automatically add the redirect URI to unauthenticatedPaths when middleware is enabled', async () => { const request = new NextRequest(new URL('http://example.com/protected')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, false, { @@ -409,7 +409,7 @@ describe('session.ts', () => { it('should redirect unauthenticated users to sign up page on protected routes included in signUpPaths', async () => { const request = new NextRequest(new URL('http://example.com/protected-signup')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, false, { @@ -424,9 +424,27 @@ describe('session.ts', () => { expect(result.headers.get('Location')).toContain('screen_hint=sign-up'); }); + it('should set the sign up paths in the headers', async () => { + const request = new NextRequest(new URL('http://example.com/protected-signup')); + const result = await updateSessionMiddleware( + request, + false, + { + enabled: false, + unauthenticatedPaths: [], + }, + process.env.NEXT_PUBLIC_WORKOS_REDIRECT_URI as string, + ['/protected-signup'], + ); + + console.log('result headers:', result.headers); + + expect(result.headers.get('x-middleware-request-x-sign-up-paths')).toBe('/protected-signup'); + }); + it('should allow logged out users on unauthenticated paths', async () => { const request = new NextRequest(new URL('http://example.com/unauthenticated')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, false, { @@ -443,7 +461,7 @@ describe('session.ts', () => { it('should throw an error if the provided regex is invalid', async () => { const request = new NextRequest(new URL('http://example.com/invalid-regex')); await expect(async () => { - await updateSession( + await updateSessionMiddleware( request, false, { @@ -467,12 +485,12 @@ describe('session.ts', () => { }); // Import session after setting up the spy - const { updateSession } = await import('../src/session.js'); + const { updateSessionMiddleware } = await import('../src/session.js'); const request = new NextRequest(new URL('http://example.com/invalid-regex')); await expect(async () => { - await updateSession( + await updateSessionMiddleware( request, false, { @@ -490,7 +508,7 @@ describe('session.ts', () => { it('should default to the WORKOS_REDIRECT_URI environment variable if no redirect URI is provided', async () => { const request = new NextRequest(new URL('http://example.com/protected')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, false, { @@ -525,7 +543,7 @@ describe('session.ts', () => { const request = new NextRequest(new URL('http://example.com')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, true, { @@ -549,14 +567,17 @@ describe('session.ts', () => { new Error('Failed to refresh'), ); - expect(console.log).toHaveBeenNthCalledWith(3, 'Redirecting to AuthKit to log in again.'); + expect(console.log).toHaveBeenNthCalledWith( + 3, + 'Unauthenticated user on protected route http://example.com/, redirecting to AuthKit', + ); }); describe('sign up paths', () => { it('should redirect to sign up when unauthenticated user is on a sign up path', async () => { const request = new NextRequest(new URL('http://example.com/signup')); - const result = await updateSession( + const result = await updateSessionMiddleware( request, false, { @@ -584,6 +605,92 @@ describe('session.ts', () => { }); }); + describe('updateSession', () => { + it('should return an authorization url if the session is invalid', async () => { + const result = await updateSession(new NextRequest(new URL('http://example.com/protected')), { + debug: true, + screenHint: 'sign-up', + }); + + expect(result.authorizationUrl).toBeDefined(); + expect(result.authorizationUrl).toContain('screen_hint=sign-up'); + expect(result.session.user).toBeNull(); + expect(console.log).toHaveBeenCalledWith('No session found from cookie'); + }); + + it('should return a session if the session is valid', async () => { + const nextCookies = await cookies(); + nextCookies.set( + 'wos-session', + await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }), + ); + + const result = await updateSession(new NextRequest(new URL('http://example.com/protected'))); + + expect(result.session).toBeDefined(); + }); + + it('should attempt to refresh an invalid session', async () => { + // Setup invalid session + mockSession.accessToken = await generateTestToken({}, true); + + const nextCookies = await cookies(); + nextCookies.set( + 'wos-session', + await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }), + ); + + // Mock token verification to fail + (jwtVerify as jest.Mock).mockImplementation(() => { + throw new Error('Invalid token'); + }); + + // Mock successful refresh + jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({ + accessToken: await generateTestToken(), + refreshToken: 'new-refresh-token', + user: mockSession.user, + }); + + const result = await updateSession(new NextRequest(new URL('http://example.com/protected')), { + debug: true, + }); + + expect(result.session).toBeDefined(); + expect(result.session.user).toBeDefined(); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('Session invalid. Refreshing access token that ends in'), + ); + }); + + it('should handle refresh failure by returning auth URL', async () => { + // Setup invalid session + mockSession.accessToken = await generateTestToken({}, true); + + const nextCookies = await cookies(); + nextCookies.set( + 'wos-session', + await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }), + ); + + // Mock token verification to fail + (jwtVerify as jest.Mock).mockImplementation(() => { + throw new Error('Invalid token'); + }); + + // Mock refresh failure + jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('Refresh failed')); + + const result = await updateSession(new NextRequest(new URL('http://example.com/protected')), { + debug: true, + }); + + expect(result.session.user).toBeNull(); + expect(result.authorizationUrl).toBeDefined(); + expect(console.log).toHaveBeenCalledWith('Failed to refresh. Deleting cookie.', expect.any(Error)); + }); + }); + describe('refreshSession', () => { it('should refresh session successfully', async () => { jest.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({ @@ -647,54 +754,6 @@ describe('session.ts', () => { }); }); - describe('getSession', () => { - it('should return session info when valid', async () => { - const nextCookies = await cookies(); - nextCookies.set( - 'wos-session', - await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }), - ); - - const result = await getSession(); - expect(result).toHaveProperty('user'); - }); - - it('should return null user when no session exists', async () => { - const result = await getSession(); - expect(result).toEqual({ user: null }); - }); - - it('should return undefined if the access token is invalid', async () => { - mockSession.accessToken = 'invalid-token'; - - const nextCookies = await cookies(); - nextCookies.set( - 'wos-session', - await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }), - ); - - (jwtVerify as jest.Mock).mockImplementation(() => { - throw new Error('Invalid token'); - }); - - const result = await getSession(); - expect(result).toEqual(undefined); - }); - - it('should return cookie from a response object if provided', async () => { - mockSession.accessToken = await generateTestToken(); - - const response = new NextResponse(); - response.cookies.set( - 'wos-session', - await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }), - ); - - const result = await getSession(response); - expect(result).toEqual(mockSession); - }); - }); - describe('terminateSession', () => { it('should redirect to logout url when there is a session', async () => { const nextHeaders = await headers(); @@ -726,26 +785,3 @@ describe('session.ts', () => { }); }); }); - -async function generateTestToken(payload = {}, expired = false) { - const defaultPayload = { - sid: 'session_123', - org_id: 'org_123', - role: 'member', - permissions: ['posts:create', 'posts:delete'], - entitlements: ['audit-logs'], - }; - - const mergedPayload = { ...defaultPayload, ...payload }; - - const secret = new TextEncoder().encode(process.env.WORKOS_COOKIE_PASSWORD as string); - - const token = await new SignJWT(mergedPayload) - .setProtectedHeader({ alg: 'HS256' }) - .setIssuedAt() - .setIssuer('urn:example:issuer') - .setExpirationTime(expired ? '0s' : '2h') - .sign(secret); - - return token; -} diff --git a/jest.setup.ts b/jest.setup.ts index 7163a08..fea14f7 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -26,7 +26,10 @@ jest.mock('next/headers', () => { get: jest.fn((name: string) => cookieStore.get(name)), getAll: jest.fn(() => Array.from(cookieStore.entries())), set: jest.fn((name: string, value: string | { [key: string]: string | number | boolean }) => - cookieStore.set(name, value), + cookieStore.set(name, { + name, + value, + }), ), _reset: () => { cookieStore.clear(); diff --git a/src/index.ts b/src/index.ts index c9d16ca..daf3690 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,13 @@ import { handleAuth } from './authkit-callback-route.js'; -import { authkitMiddleware } from './middleware.js'; -import { withAuth, refreshSession, getSession } from './session.js'; +import { authkit, authkitMiddleware } from './middleware.js'; +import { withAuth, refreshSession } from './session.js'; import { getSignInUrl, getSignUpUrl, signOut } from './auth.js'; export { handleAuth, // authkitMiddleware, - getSession, + authkit, // getSignInUrl, getSignUpUrl, diff --git a/src/interfaces.ts b/src/interfaces.ts index f50448e..2d5568f 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -69,6 +69,18 @@ export interface AuthkitMiddlewareOptions { signUpPaths?: string[]; } +export interface AuthkitOptions { + debug?: boolean; + redirectUri?: string; + screenHint?: 'sign-up' | 'sign-in'; +} + +export interface AuthkitResponse { + session: UserInfo | NoUserInfo; + headers: Headers; + authorizationUrl?: string; +} + export interface CookieOptions { path: '/'; httpOnly: true; diff --git a/src/middleware.ts b/src/middleware.ts index 8474b31..633ad38 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,6 @@ -import { NextMiddleware } from 'next/server'; -import { updateSession } from './session.js'; -import { AuthkitMiddlewareOptions } from './interfaces.js'; +import { NextMiddleware, NextRequest } from 'next/server'; +import { updateSessionMiddleware, updateSession } from './session.js'; +import { AuthkitMiddlewareOptions, AuthkitOptions, AuthkitResponse } from './interfaces.js'; import { WORKOS_REDIRECT_URI } from './env-variables.js'; export function authkitMiddleware({ @@ -10,6 +10,10 @@ export function authkitMiddleware({ signUpPaths = [], }: AuthkitMiddlewareOptions = {}): NextMiddleware { return function (request) { - return updateSession(request, debug, middlewareAuth, redirectUri, signUpPaths); + return updateSessionMiddleware(request, debug, middlewareAuth, redirectUri, signUpPaths); }; } + +export async function authkit(request: NextRequest, options: AuthkitOptions = {}): Promise { + return await updateSession(request, options); +} diff --git a/src/session.ts b/src/session.ts index 0c741bf..7e92246 100644 --- a/src/session.ts +++ b/src/session.ts @@ -9,14 +9,21 @@ import { getCookieOptions } from './cookie.js'; import { workos } from './workos.js'; import { WORKOS_CLIENT_ID, WORKOS_COOKIE_PASSWORD, WORKOS_COOKIE_NAME, WORKOS_REDIRECT_URI } from './env-variables.js'; import { getAuthorizationUrl } from './get-authorization-url.js'; -import { AccessToken, AuthkitMiddlewareAuth, NoUserInfo, Session, UserInfo } from './interfaces.js'; +import { + AccessToken, + AuthkitMiddlewareAuth, + AuthkitOptions, + AuthkitResponse, + NoUserInfo, + Session, + UserInfo, +} from './interfaces.js'; import { parse, tokensToRegexp } from 'path-to-regexp'; import { redirectWithFallback } from './utils.js'; const sessionHeaderName = 'x-workos-session'; const middlewareHeaderName = 'x-workos-middleware'; -const redirectUriHeaderName = 'x-redirect-uri'; const signUpPathsHeaderName = 'x-sign-up-paths'; const JWKS = createRemoteJWKSet(new URL(workos.userManagement.getJwksUrl(WORKOS_CLIENT_ID))); @@ -28,7 +35,7 @@ async function encryptSession(session: Session) { }); } -async function updateSession( +async function updateSessionMiddleware( request: NextRequest, debug: boolean, middlewareAuth: AuthkitMiddlewareAuth, @@ -45,34 +52,14 @@ async function updateSession( ); } - const session = await getSessionFromCookie(); - const newRequestHeaders = new Headers(request.headers); - - // We store the current request url in a custom header, so we can always have access to it - // This is because on hard navigations we don't have access to `next-url` but need to get the current - // `pathname` to be able to return the users where they came from before sign-in - newRequestHeaders.set('x-url', request.url); - - // Record that the request was routed through the middleware so we can check later for DX purposes - newRequestHeaders.set(middlewareHeaderName, 'true'); - - // Record the sign up paths so we can use it later - if (signUpPaths.length > 0) { - newRequestHeaders.set(signUpPathsHeaderName, signUpPaths.join(',')); - } - let url; - // If the redirect URI is set, store it in the headers so we can use it later if (redirectUri) { - newRequestHeaders.set(redirectUriHeaderName, redirectUri); url = new URL(redirectUri); } else { url = new URL(WORKOS_REDIRECT_URI); } - newRequestHeaders.delete(sessionHeaderName); - if ( middlewareAuth.enabled && url.pathname === request.nextUrl.pathname && @@ -94,89 +81,131 @@ async function updateSession( return pathRegex.exec(request.nextUrl.pathname); }); - // If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit. - if (middlewareAuth.enabled && matchedPaths.length === 0 && !session) { - if (debug) console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`); + const { session, headers, authorizationUrl } = await updateSession(request, { + debug, + redirectUri, + screenHint: getScreenHint(signUpPaths, request.nextUrl.pathname), + }); - const redirectTo = await getAuthorizationUrl({ - returnPathname: getReturnPathname(request.url), - redirectUri: redirectUri, - screenHint: getScreenHint(signUpPaths, request.nextUrl.pathname), - }); + // If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit. + if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) { + if (debug) { + console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`); + } - return redirectWithFallback(redirectTo); + return redirectWithFallback(authorizationUrl as string); } - // If no session, just continue - if (!session) { - return NextResponse.next({ - request: { headers: newRequestHeaders }, - }); + // Record the sign up paths so we can use them later + if (signUpPaths.length > 0) { + headers.set(signUpPathsHeaderName, signUpPaths.join(',')); } - const hasValidSession = await verifyAccessToken(session.accessToken); - const cookieName = WORKOS_COOKIE_NAME || 'wos-session'; + return NextResponse.next({ + request: { headers }, + }); +} - const nextCookies = await cookies(); +async function updateSession( + request: NextRequest, + options: AuthkitOptions = { debug: false }, +): Promise { + const session = await getSessionFromCookie(); - if (hasValidSession) { - if (debug) console.log('Session is valid'); - // set the x-workos-session header according to the current cookie value - newRequestHeaders.set(sessionHeaderName, nextCookies.get(cookieName)!.value); - return NextResponse.next({ - request: { headers: newRequestHeaders }, - }); - } + const newRequestHeaders = new Headers(request.headers); - try { - if (debug) console.log(`Session invalid. Refreshing access token that ends in ${session.accessToken.slice(-10)}`); + // Record that the request was routed through the middleware so we can check later for DX purposes + newRequestHeaders.set(middlewareHeaderName, 'true'); - const { org_id: organizationId } = decodeJwt(session.accessToken); + // We store the current request url in a custom header, so we can always have access to it + // This is because on hard navigations we don't have access to `next-url` but need to get the current + // `pathname` to be able to return the users where they came from before sign-in + newRequestHeaders.set('x-url', request.url); - // If the session is invalid (i.e. the access token has expired) attempt to re-authenticate with the refresh token - const { accessToken, refreshToken, user, impersonator } = await workos.userManagement.authenticateWithRefreshToken({ - clientId: WORKOS_CLIENT_ID, - refreshToken: session.refreshToken, - organizationId, - }); + newRequestHeaders.delete(sessionHeaderName); + + if (!session) { + if (options.debug) { + console.log('No session found from cookie'); + } - if (debug) console.log(`Refresh successful. New access token ends in ${accessToken.slice(-10)}`); + return { + session: { user: null }, + headers: newRequestHeaders, + authorizationUrl: await getAuthorizationUrl({ + returnPathname: getReturnPathname(request.url), + redirectUri: options.redirectUri || WORKOS_REDIRECT_URI, + screenHint: options.screenHint, + }), + }; + } - // Encrypt session with new access and refresh tokens - const encryptedSession = await encryptSession({ - accessToken, - refreshToken, - user, - impersonator, - }); + const hasValidSession = await verifyAccessToken(session.accessToken); - newRequestHeaders.set(sessionHeaderName, encryptedSession); + const cookieName = WORKOS_COOKIE_NAME || 'wos-session'; + const nextCookies = await cookies(); - const response = NextResponse.next({ - request: { headers: newRequestHeaders }, - }); - // update the cookie - response.cookies.set(cookieName, encryptedSession, getCookieOptions(redirectUri)); - return response; - } catch (e) { - if (debug) console.log('Failed to refresh. Deleting cookie.', e); + if (!hasValidSession) { + if (options.debug) { + console.log(`Session invalid. Refreshing access token that ends in ${session.accessToken.slice(-10)}`); + } - nextCookies.delete(cookieName); + try { + const newSession = await refreshSession({ + ensureSignedIn: false, + }); + + if (options.debug) { + console.log('Session successfully refreshed'); + } + + newRequestHeaders.set(sessionHeaderName, nextCookies.get(cookieName)!.value); + + return { + session: newSession, + headers: newRequestHeaders, + }; + } catch (e) { + if (options.debug) { + console.log('Failed to refresh. Deleting cookie.', e); + } + + const nextCookies = await cookies(); + nextCookies.delete(cookieName); + + return { + session: { user: null }, + headers: newRequestHeaders, + authorizationUrl: await getAuthorizationUrl({ + returnPathname: getReturnPathname(request.url), + }), + }; + } } - if (middlewareAuth.enabled) { - // If we get here, the session is invalid and the user needs to sign in again because we're using middleware auth mode. - // We redirect to the current URL which will trigger the middleware again. - // This is outside of the above block because you cannot redirect in Next.js - // from inside a try/catch block. - if (debug) console.log('Redirecting to AuthKit to log in again.'); - return redirectWithFallback(request.url); - } + newRequestHeaders.set(sessionHeaderName, nextCookies.get(cookieName)!.value); - // If we aren't in middleware auth mode, we return a response and let the page handle what to do next. - return NextResponse.next({ - request: { headers: newRequestHeaders }, - }); + const { + sid: sessionId, + org_id: organizationId, + role, + permissions, + entitlements, + } = decodeJwt(session.accessToken); + + return { + session: { + sessionId, + user: session.user, + organizationId, + role, + permissions, + entitlements, + impersonator: session.impersonator, + accessToken: session.accessToken, + }, + headers: newRequestHeaders, + }; } async function refreshSession(options: { @@ -339,50 +368,18 @@ async function verifyAccessToken(accessToken: string) { } } -async function getSessionFromCookie(response?: NextResponse) { +async function getSessionFromCookie() { const cookieName = WORKOS_COOKIE_NAME || 'wos-session'; const nextCookies = await cookies(); - const cookie = response ? response.cookies.get(cookieName) : nextCookies.get(cookieName); + const cookie = nextCookies.get(cookieName); if (cookie) { - return unsealData(cookie.value ?? cookie, { + return unsealData(cookie.value, { password: WORKOS_COOKIE_PASSWORD, }); } } -/** - * Retrieves the session from the cookie. Meant for use in the middleware, for client side use `withAuth` instead. - * - * @returns UserInfo | NoUserInfo - */ -async function getSession(response?: NextResponse) { - const session = await getSessionFromCookie(response); - - if (!session) return { user: null }; - - if (await verifyAccessToken(session.accessToken)) { - const { - sid: sessionId, - org_id: organizationId, - role, - permissions, - entitlements, - } = decodeJwt(session.accessToken); - - return { - sessionId, - user: session.user, - organizationId, - role, - permissions, - entitlements, - impersonator: session.impersonator, - accessToken: session.accessToken, - }; - } -} - async function getSessionFromHeader(): Promise { const headersList = await headers(); const hasMiddleware = Boolean(headersList.get(middlewareHeaderName)); @@ -417,4 +414,4 @@ function getScreenHint(signUpPaths: string[] | undefined, pathname: string) { return screenHintPaths.length > 0 ? 'sign-up' : 'sign-in'; } -export { encryptSession, withAuth, refreshSession, terminateSession, updateSession, getSession }; +export { encryptSession, withAuth, refreshSession, terminateSession, updateSessionMiddleware, updateSession };