nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
23.24k stars 3.18k forks source link

Enable withAuth to return 401 unauthorized instead of 404 not found for unauthorized requests #6426

Open jmarceli opened 1 year ago

jmarceli commented 1 year ago

Description 📓

Currently there is no way to customize withAuth middleware response in case of requests that fails to pass authorized callback (https://github.com/nextauthjs/next-auth/blob/next-auth%404.18.8/packages/next-auth/src/next/middleware.ts#L145). Such requests are always redirected to the signInPage (https://github.com/nextauthjs/next-auth/blob/next-auth%404.18.8/packages/next-auth/src/next/middleware.ts#L153). That behaviour is perfectly fine for a regular web application pages, but could be improved in case of API responses. For API routes I would rather like to return HTTP 401 Unauthorized response and probably some response body as well instead of HTTP 404 Not Found not to mention other possible responses e.g. HTTP 403 Forbidden etc.

Fortunately it is easily solvable by extending withAuth by adding for example new available callback e.g. onFailure. Such callback if provided would be executed instead of the default NextResponse.redirect(signInUrl) (https://github.com/nextauthjs/next-auth/blob/next-auth%404.18.8/packages/next-auth/src/next/middleware.ts#L153) and would allow to return any kind of failure response.

I would be more than happy to provide a relevant PR if only you find this proposal attractive/useful. This is similar to https://github.com/nextauthjs/next-auth/pull/4788 but I believe that even fewer changes are necessary as developers can implement onFailure however they want and there is no need to provide a separate noUser callback.

How to reproduce ☕️

Sample implementation of that enhancement:

  // ... existing code (https://github.com/nextauthjs/next-auth/blob/next-auth%404.18.8/packages/next-auth/src/next/middleware.ts#L148)
  const signInUrl = new URL(`${basePath}${signInPage}`, origin);
  signInUrl.searchParams.append(
    "callbackUrl",
    `${basePath}${pathname}${search}`
  );

  // ... changes begining
  return (
    (await options?.callbacks?.onFailure?.({ req, token })) ??
    NextResponse.redirect(signInUrl)
  );
  // ... changes ending
}
// ... existing code

Contributing 🙌🏽

Yes, I am willing to help implement this feature in a PR

maslennikov commented 1 year ago

@jmarceli As an alternative, you can use an API path condition with manual token check, like this:

export default async function middleware(req: NextRequest) {
  if (req.nextUrl.pathname.match(/\/api(?!\/auth\/).*/)) {
    const token = await getToken({req})
    if (!token) return NextResponse.json({error: 'Unauthorized'}, {status: 401})
  } else {
    return withAuth(req as NextRequestWithAuth)
  }
}
mschipperheyn commented 1 year ago

The correct status code for unauthorized is not 401 (not authenticated), it's 403 (not authorized)

jmarceli commented 11 months ago

@mschipperheyn you are right regarding use case for the given status codes but not regarding their names :) https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 - 401 Unauthorized https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403 - 403 Forbidden

@maslennikov many thanks for you suggestion. With your approach 401 Unauthorized is returned also for non-existing API endpoints that should return 404 Not Found. A minor issue but maybe you have some idea how to fix that as well?

maslennikov commented 11 months ago

@jmarceli oh, that's an interesting one. I don't know how to approach this elegantly. After some time trying to incorporate nextauth middleware into my next13 with app directory I abandoned this idea because it felt awkward.

I rely on token refresh flow implemented inside jwt() callback, and middleware seems not to work well with getServerSession() triggering this callback, because it is not compatible with NextResponse format, see getServerSession() implementation

I implemented all my server auth logic with custom getToken() wrapper which I use inside route handlers and server fetch requests + client session provider which polls my /api/auth/session endpoint every minute to force jwt() callback recheck auth refresh status.