Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4: Calls to Auth0 middleware or getAccessToken in Edge runtime can cause unhandled 403 errors #1862

Open
6 tasks done
WilHall opened this issue Jan 9, 2025 · 11 comments
Open
6 tasks done

Comments

@WilHall
Copy link

WilHall commented Jan 9, 2025

Checklist

Description

What happens

The problem we are experiencing appears to ultimately be caused by this library's calls to oauth resulting from our middleware calling either auth0.middleware or auth0.getAccessToken. When it happens, users see a 403 page, and we are unable to catch and handle the error in our middleware because it is unhandled in the Auth0 middleware / route handlers.

The actual cause of the underlying OAuth errors are issues with the OAuth providers we use, and are not the focus of this bug report.

What we would expect to happen

We would expect calls to auth0.middleware or auth0.getAccessToken to be safe to call from our middleware, either not erroring or erroring in a way which we are able to handle. But because these errors are unhandled in the Auth0 middleware / route handlers we cannot handle them with a try...catch; my understanding here is that this is a limitation of error handling in Next.js/ the Edge runtime.

Additionally, the 403 page users wee when this happens is just a plain, unstyled 403 Forbidden page, and we don't appear to have any control over this page or the ability to override this behavior.

Reproduction

This issue can be reproduced in the Next.js Edge runtime by calling auth0.middleware or auth0.getAccessToken from the middleware in a configuration which would produce an OAuth error. Reproduction of the underlying OAuth error would depend on the OAuth error you are attempting to reproduce, your Auth0 configuration, your SSO configuration, etc. In our experience, any OAuth error reproduces the issue.

Additional context

OAuth errors that cause this issue

I assume any OAuth error could cause this issue, but the ones we have seen are:

  1. { code: 'OAUTH_RESPONSE_BODY_ERROR', error: 'mfa_required', status: 403, error_description: 'Multifactor authentication required' }
  2. { code: 'OAUTH_RESPONSE_IS_NOT_CONFORM' }

When caused by calls to auth0.middleware

  1. We call await auth0.middleware(request) in our middleware
  2. Middleware calls: handleAccessToken
  3. handleAccessToken calls: getTokenSet
  4. getTokenSet calls oauth which raises this error
iF: server responded with an error in the response body
  at ae (.next/server/middleware.js:13:109533)
  at async aG.getTokenSet (.next/server/middleware.js:13:129399)
  at async aG.handleAccessToken (.next/server/middleware.js:13:128080)
  at async oZ (.next/server/middleware.js:13:171367)
  at async handler (.next/server/middleware.js:13:173296)
  at async (.next/server/middleware.js:13:32412)
  at async ti (.next/server/middleware.js:13:29268) {
    code: 'OAUTH_RESPONSE_BODY_ERROR',
    error: 'mfa_required',
    status: 403,
    error_description: 'Multifactor authentication required'
  }

When caused by calls to auth0.getAccessToken

  1. We call await auth0.getAccessToken() in our middleware
    2.getAccessToken definition: getAccessToken
  2. getAccessToken calls: getTokenSet
  3. getTokenSet calls oauth which raises this error
iF: server responded with an error in the response body
  at ae (.next/server/middleware.js:13:109533)
  at async aG.getTokenSet (.next/server/middleware.js:13:129399)
  at async oV.getAccessToken (.next/server/middleware.js:13:167594)
  at async oZ (.next/server/middleware.js:13:171624)
  at async handler (.next/server/middleware.js:13:173296)
  at async (.next/server/middleware.js:13:32412)
  at async ti (.next/server/middleware.js:13:29268) {
    code: 'OAUTH_RESPONSE_BODY_ERROR',
    error: 'mfa_required',
    status: 403,
    error_description: 'Multifactor authentication required'
  }

nextjs-auth0 version

4.0.0-beta.13

Next.js version

15.1.3

Node.js version

22.12.0

@WilHall
Copy link
Author

WilHall commented Jan 14, 2025

@gyaneshgouraw-okta @nandan-bhat @arpit-jn @guabu Apologies for pinging y'all directly, but this is a serious production issue that leaves users in an unrecoverable error state. I've escalated the issue to Auth0 support in hopes they can help us understand the underlying OAuth issues, but regardless my understanding is that the nextjs-auth0 library could be gracefully handling these. Let me know if you need any additional details. Thanks!

@guabu
Copy link

guabu commented Jan 14, 2025

Hey @WilHall 👋 From the error you shared, it looks like the user needs to complete MFA before they can refresh their token set (via getAccessToken). In this case, the user will need to be redirected to complete the login and MFA again at Auth0 (e.g.: redirecting the user to /auth/login).

You might be able to wrap the calls in your app to getAccessToken with a try / catch and redirect the user appropriately. The middleware itself does not call handleAccessToken unless getAccessToken is explicitly called.

@WilHall
Copy link
Author

WilHall commented Jan 14, 2025

@guabu 👋🏻 Thanks for the reply 🙂 Redirecting the user to /auth/login yields the same user-facing issue: the user receives a 403 error page.

The middleware does call handleAccessToken, right here, to handle the /auth/access-token route:
https://github.com/auth0/nextjs-auth0/blob/v4.0.0-beta.14/src/server/auth-client.ts#L207

We are wrapping the call to await auth0.middleware(request) from our middleware in a try...catch but these errors are not caught and result in the user seeing an unstyled 403 Forbidden page.

In our current implementation, we do not call getAccessToken from the middleware.

Any thoughts on:

  1. Why we cannot catch the errors caused by the call to await auth0.middleware(request)
  2. Why redirecting the user to /auth/login yields a 403 Forbidden page, making the error unrecoverable to the user
  3. Should the Auth0 middleware be handling these errors more gracefully?

Thanks,

@guabu
Copy link

guabu commented Jan 14, 2025

In our current implementation, we do not call getAccessToken from the middleware.

Are you calling getAccessToken elsewhere in your app (on the client or server)? Have you tried wrapping those calls to handle the redirect the user instead of wrapping the middleware?

@WilHall
Copy link
Author

WilHall commented Jan 14, 2025

@guabu We call getAccessToken on the client side, and that call fails with the same error but does not cause the 403 Forbidden error page.

We additionally call await auth0.getAccessToken().catch(() => null) in a server component, but as you can see we have a catch on that.

@guabu
Copy link

guabu commented Jan 16, 2025

I was trying to reproduce the error you mentioned but I was able to successfully wrap the getAccessToken call to catch the failed_to_refresh_token due to MFA being required and redirecting the user back to login and complete MFA:

import { redirect } from "next/navigation"

import { auth0 } from "@/lib/auth0"

export default async function Home() {
  const session = await auth0.getSession()

  if (!session) {
    return <main>Hello world!</main>
  }

  try {
    const at = await auth0.getAccessToken()
    console.log(at)
  } catch (e: any) {
    console.log(e.code) // failed_to_refresh_token
    redirect("/auth/login")
  }

  return (
    <main>
      <h1>Welcome, {session.user.email}!</h1>
    </main>
  )
}

Once the user completed MFA for their session, you will be able to refresh the token (if it was expired).

@WilHall
Copy link
Author

WilHall commented Jan 16, 2025

@guabu Let's take a few steps back here. I think some important details were missed when reviewing my initial bug report.

The problem we are experiencing appears to ultimately be caused by this library's calls to oauth resulting from our middleware calling either auth0.middleware or auth0.getAccessToken. When it happens, users see a 403 page, and we are unable to catch and handle the error in our middleware because it is unhandled in the Auth0 middleware / route handlers.

The bug report is specifically about error handling in our Next.js middleware, not in a server component as in your example.

The actual cause of the underlying OAuth errors are issues with the OAuth providers we use, and are not the focus of this bug report.

I think you're getting caught up on how to fix the MFA issue specifically, but this bug report is not about the MFA issue or any specific issue - it's about how this library handles issues when they occur.

As I mentioned in the report, another example of an underlying OAuth error which causes this error is { code: 'OAUTH_RESPONSE_IS_NOT_CONFORM' }, which is not an MFA issue.

This issue can be reproduced in the Next.js Edge runtime by calling auth0.middleware or auth0.getAccessToken from the middleware in a configuration which would produce an OAuth error.

Here is a code sample that should reproduce the issue for you, assuming you trigger one of the underlying OAuth issues I mentioned and then visit a page handled by the middleware:

export async function middleware(request: NextRequest) {
  let authResponse: NextResponse;
  try {
    authResponse = await auth0.middleware(request);

    if (request.nextUrl.pathname.startsWith('/auth')) {
      return authResponse;
    }
  } catch (error) {
    // When an underlying OAuth issue occurs, this never happens. Instead, the user sees a "403 Forbidden" page in
    // their browser
    return NextResponse.redirect('/auth/login');
  }

  const response = NextResponse.next();
  for (const [key, value] of authResponse.headers) {
    response.headers.set(key, value);
  }

  return response;
}

As far as I can tell, this is no different than the examples in the documentation of this library, except the introduction of the try...catch.

Additionally, the 403 page users wee when this happens is just a plain, unstyled 403 Forbidden page, and we don't appear to have any control over this page or the ability to override this behavior.

When the underlying OAuth issues in my report occur, the try...catch does not catch the errors allowing us to handle them. Instead, the user sees a 403 Forbidden error in their browser, and cannot recover from the error unless they clear their cookies.

At first glance, this would appear to be because this library handles the error and returns an unauthorized response:

https://github.com/auth0/nextjs-auth0/blob/v4.0.0-beta.14/src/server/auth-client.ts#L486-L500

const [error, updatedTokenSet] = await this.getTokenSet(session.tokenSet)

if (error) {
  return NextResponse.json(
    {
      error: {
        message: error.message,
        code: error.code,
      },
    },
    {
      status: 401,
    }
  )
}

However, this is a 401, not a 403. And if this was the source of the error, we could handle it with the following adjustment to my middleware code sample, but this also does not handle the error:

export async function middleware(request: NextRequest) {
  let authResponse: NextResponse;
  try {
    authResponse = await auth0.middleware(request);

    if (request.nextUrl.pathname.startsWith('/auth')) {
      if (request.nextUrl.pathname === '/auth/access-token') {
        return authResponse;
      }

      if (authResponse.status >= 400 && authResponse.status <= 599) {
        // When an underlying OAuth issue occurs, this never happens. Instead, the user sees a "403 Forbidden" page in
        // their browser
        return NextResponse.redirect('/auth/login');
      }
      
      return authResponse;
    }
  } catch (error) {
    // When an underlying OAuth issue occurs, this never happens. Instead, the user sees a "403 Forbidden" page in
    // their browser
    return NextResponse.redirect('/auth/login');
  }

  const response = NextResponse.next();
  for (const [key, value] of authResponse.headers) {
    response.headers.set(key, value);
  }

  return response;
}

*In the above code, we never receive an authResponse from the middleware with a 403 error. The error appears to be internal to the library, meaning the user just sees a 403 Forbidden error page.

Please let me know if you need any additional details.

Thanks,

@guabu
Copy link

guabu commented Jan 17, 2025

Thanks for the additional context @WilHall.

I understand the root of the issue you're reporting is around error handling in the middleware. I was using the MFA error as a more concrete starting point to reproduce the issue since it was easier to trigger.

The goal is to reproduce the 403 Forbidden page so we can pinpoint where the issue is coming from and, if it's from the SDK, we can handle it more gracefully.

Unfortunately, I haven't had much success in triggering the case where a 403 page is returned using the default SDK setup with middleware. Would you be able to share:

  1. A repository with a basic setup where you're able to trigger this scenario (403 Page)
  2. The steps you took to trigger the error in that repo
  3. Any relevant client configuration from the Dashboard (e.g.: OIDC Conformant flag, app type, grants)

This would help us narrow down the issue and make sure there aren't other factors at play that might be different between our setups.

Thanks for your patience with this!

@WilHall
Copy link
Author

WilHall commented Jan 17, 2025

@guabu Thanks for the response 🙂

I was finally able to reliably reproduce this issue with my own account (previously we were only able to reproduce with real users) and I believe I've identified the root cause as it related to this Auth0 library.

Effectively what was happening for users experiencing this was they were ending up in a login redirect loop between our app and Auth0. This had multiple causes which we have identified and are not the fault of this library.

Within that redirect loop Auth0 sets __txn_* cookies which normally are consumed and translated into a __session cookie by the Auth0 library, but it appears that in certain error cases they are not deleted and there does not appear to be logic to clean them up.

So a user who experienced a login redirect loop would loop until the browser terminated the loop or until their cookies accumulated to 10kb, which caused our AWS WAF to return a 403 error because the cookies exceeded its maximum allowed size of 10kb.

Once a user is in this state, the only solution is to clear their browser cookies because there is nothing we can do since their requests don't even hit our app.

We're applying a fix to our middleware which resolves the root causes of our redirect loops with Auth0, and also that removes extraneous __txn_* cookies.

As it pertains to this bug report, my question is: should this library be cleaning up extraneous __txn_* cookies? Is this something you would anticipate the library handling in the future?

Thanks!

@guabu
Copy link

guabu commented Jan 17, 2025

That's great to hear, I'm glad you managed to get to the root of the issue! And thanks for sharing the context.

As it pertains to this bug report, my question is: should this library be cleaning up extraneous _txn* cookies? Is this something you would anticipate the library handling in the future?

The transaction cookies are generally short-lived. We set them to have a max age of 1 hour so the browser should clear them shortly after, if they have not already been consumed. However, if the transaction is successful, they'll be cleared out on callback as you mentioned.

We definitely look into capping the number of __txn_ cookies to prevent such scenarios or even shortening their lifespan if this continues to be an issue.

@WilHall
Copy link
Author

WilHall commented Jan 17, 2025

@guabu Yes, I'm glad we were able to finally figure it out 🙂

Although the transaction cookies have a short lifetime, that won't prevent this issue in the case os a redirect loop.

Effectively what we have done as a fix is both in our middleware and in the Auth0 client onCallback function when there is an error passed in, we:

  1. Call cookies() and enumerate all __txn_ cookies
  2. Expire the cookies on the response (except for /auth/* routes, unless it's /auth/callback and there was an error

Could this cleanup or similar be added to the library? Otherwise, the issue still exists without this manual fix to our middleware and onCallback functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants