Skip to content

Conversation

@nicknisi
Copy link
Member

@nicknisi nicknisi commented Dec 22, 2025

Summary

  • Add handleAuthkitHeaders(), partitionAuthkitHeaders(), and applyResponseHeaders() helpers for composing custom proxy/middleware with AuthKit
  • Refactor session.ts to use new helpers internally (dogfooding)
  • Update README terminology for Next.js 16 (Proxy vs Middleware)

Why

authkitMiddleware() is opaque. Users could not inject custom logic (rate limiting, redirects, A/B testing) while preserving AuthKit's session handling. This caused:

  • Session data lost when returning custom NextResponse
  • Internal headers (x-workos-session) leaked to browser
  • Cookie handling broken (Set-Cookie requires append(), not set())
  • Missing Cache-Control: no-store when cookies present

Usage

// proxy.ts (Next.js 16+) or middleware.ts (Next.js ≤15)
import { NextRequest } from 'next/server';
import { authkit, handleAuthkitHeaders } from '@workos-inc/authkit-nextjs';

export default async function proxy(request: NextRequest) {
  const { session, headers, authorizationUrl } = await authkit(request);
  const { pathname } = request.nextUrl;

  // Redirect unauthenticated users on protected routes
  if (pathname.startsWith('/app') && !session.user && authorizationUrl) {
    return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl });
  }

  // Custom redirects
  if (pathname === '/old-path') {
    return handleAuthkitHeaders(request, headers, { redirect: '/new-path' });
  }

  // Continue request with properly merged headers
  return handleAuthkitHeaders(request, headers);
}

export const config = { matcher: ['/', '/app/:path*'] };

@nicknisi nicknisi force-pushed the nicknisi/composable-middleware-helpers branch from 50149a9 to 1a9b2bb Compare December 22, 2025 16:12
The manual header-handling example was superseded by the
handleAuthkitHeaders helper documented earlier in Setup.
@nicknisi nicknisi marked this pull request as ready for review December 22, 2025 16:51
@nicknisi nicknisi requested a review from a team as a code owner December 22, 2025 16:51
export default async function proxy(request: NextRequest) {
// For Next.js ≤15, use: export default async function middleware(request: NextRequest) {
// Get session, headers, and the WorkOS authorization URL for sign-in redirects
const { session, headers, authorizationUrl } = await authkit(request);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a silly question!

I'm looking at Vercel's example for setting headers in a proxy. The general shape of proxies seems to be do some stuff and then return a Response or NextResponse. It feels like we're hiding that a bit here with handleAuthkitHandlers.

What's preventing us from having authkit return headers that we can use directly in a more conventional-looking proxy? I'm sure this is missing something, but at a high level I'm thinking something more like...

export default async function proxy(request: NextRequest) {
  const {
    session,
    requestHeaders,
    responseHeaders,
    authorizationUrl
  } = await authkit(request);

  const { pathname } = request.nextUrl;

  // Redirect unauthenticated users on protected routes
  if (pathname.startsWith('/app') && !session.user && authorizationUrl) {
    return NextResponse.redirect(authorizationUrl)
  }

  return NextResponse.next({ request: { headers: requestHeaders} }, headers: responseHeaders })
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a silly question at all!

You can do exactly what you're describing! We export the primitives:

import { authkit, partitionAuthkitHeaders, applyResponseHeaders } from '@workos-inc/authkit-nextjs';

export default async function proxy(request: NextRequest) {
  const { session, headers, authorizationUrl } = await authkit(request);
  const { requestHeaders, responseHeaders } = partitionAuthkitHeaders(request, headers);

  if (!session.user && authorizationUrl) {
    return applyResponseHeaders(NextResponse.redirect(authorizationUrl), responseHeaders);
  }

  return applyResponseHeaders(
    NextResponse.next({ request: { headers: requestHeaders } }),
    responseHeaders
  );
}

handleAuthkitHeaders() is just a convenience wrapper around this pattern.

As for why authkit() doesn't return requestHeaders/responseHeaders directly - it already returns headers and changing that would be a breaking change. We could add them alongside headers and deprecate, but that felt like more complexity than the helper approach.

The reason applyResponseHeaders() exists at all is Set-Cookie handling - you need append() not set(), otherwise only the last cookie survives. Easy footgun.

Should we update the README to show both approaches? Happy to make handleAuthkitHeaders feel less like a black box.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation! Good to know about the Set-Cookie footgun. I think the README is good as-is. Just wanted a gut check on whether this approach would feel at least somewhat familiar to folks used to writing Next.js proxies.

export default async function proxy(request: NextRequest) {
// For Next.js ≤15, use: export default async function middleware(request: NextRequest) {
// Get session, headers, and the WorkOS authorization URL for sign-in redirects
const { session, headers, authorizationUrl } = await authkit(request);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation! Good to know about the Set-Cookie footgun. I think the README is good as-is. Just wanted a gut check on whether this approach would feel at least somewhat familiar to folks used to writing Next.js proxies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants