-
Notifications
You must be signed in to change notification settings - Fork 426
Description
Checklist
- The issue can be reproduced in the nextjs-auth0 sample app (or N/A).
- I have looked into the Readme, Examples, and FAQ and have not found a suitable solution or answer.
- I have looked into the API documentation and have not found a suitable solution or answer.
- I have searched the issues and have not found a suitable solution or answer.
- I have searched the Auth0 Community forums and have not found a suitable solution or answer.
- I agree to the terms within the Auth0 Code of Conduct.
Description
The current documentation and examples don't seem to contain complete solutions for more complex auth flows.
Reproduction
Context
- Next.js app uses App Router only
- Refresh tokens are rotated on each use
- Auth is checked in the Middleware according to the Examples doc:
const authRes = await auth0.middleware(request)
if (isPublicRoute() || isAuthRoute()) {
return authRes
}
const session = await auth0.getSession(request)
if (!session) {
return redirectToLogin() // Default "/auth/login"
}
Flow 1: Access token expiration when there is no Refresh token and Auth0 doesn't have a custom domain set
Having the app on app.com
and auth on something.auth0.com
means that Route Handler redirects will be blocked due to CORS issues.
This also applies to local development, even if Auth0 has a custom domain set.
The Examples doc states:
Getting an access token > On the server (App Router) > Important
It is recommended to call getAccessToken(req, res) in the middleware if you need to use the refresh token in a Server Component as this will ensure the token is refreshed and correctly persisted.
Which is discouraged by Next.js Middleware docs:
Middleware is not a good fit for:
- Session management
But assuming the Access token is handled in the Middleware with this check:
const accessToken = await auth0.getAccessToken(request, authRes)
The user will receive this message:
"The access token has expired and a refresh token was not provided. The user needs to re-authenticate."
We can wrap the getAccessToken()
in try / catch and redirect the user to the login page:
try {
const accessToken = await auth0.getAccessToken(request, authRes)
} catch (error) {
if (error instanceof AccessTokenError) {
return redirectToLogin()
}
...
}
Is this the correct way to handle such cases?
This will work for browser routing requests, but not for Route Handler API requests, which will appear as:
app.com/api/test
> (Redirect to) app.com/auth/login
> (Redirect to) something.auth0.com/authorize
And this will be blocked by CORS.
It looks like the only solution to this is for the request to return 401 in such cases, so that the client could trigger a proper redirect.
Executing getAccessToken()
in the Middleware introduces other issues as well:
- If the request was initiated by a Client component and was going through a Route Handler and the session is updated in the Middleware, the Route Handler gets the old session with
await auth0.getSession(request)
and is unable to do anything, because the old Refresh token was rotated and is no longer valid.
Instead of adding getAccessToken()
in the Middleware, server components could fetch the data using existing Route Handlers, which allow setting cookies.
But this requires cookie forwarding:
export default async function ServerComponent() {
const cookieStore = await cookies()
const response = fetch('app.com/api/test', {
headers: {
Cookies: cookieStore.toString(),
},
})
}
The downsides I see here:
- The need to ensure these cookies are not sent to a third party
- Two requests instead of one, increasing the response time and server load
- Are there other potential issues?
An even better approach is to use Server Actions from Server components and use the redirect()
helper.
Flow 2: Token renewal during parallel requests
Let's say we have multiple parallel requests from Client components going through Route Handlers when the Access token is expired.
I assumed there should be an Internal Locking / Request Coalescing mechanism that would enforce a maximum of one simultaneous token renewal request per user. So that parallel requests would all wait until the tokens are renewed.
However, such a mechanism doesn't seem to be present. Instead, each parallel request tries renewing the tokens, but only the first one succeeds, while others result in an error, because the Refresh token was rotated and is no longer valid.
What is the suggested approach here?
Flow 3: Setting Access token expiration buffer
The current implementation of getAccessToken()
refreshes the tokens only after they have already expired:
nextjs-auth0/src/server/auth-client.ts
Line 706 in 0ed21d1
if (forceRefresh || tokenSet.expiresAt <= Date.now() / 1000) { |
But this may lead to an issue where the token expires after the frontend check, but before it's actually checked by the backend. Or if there's a slight time mismatch between the frontend and backend.
It seems reasonable to add a small default buffer (e.g. 15 seconds) and refresh the tokens shortly before they have actually expired.
This could likely be achieved by manually passing the refresh
param, though a global setting seems preferable:
const accessToken = await auth0.getAccessToken(request, authRes, {
refresh: session.tokenSet.expiresAt - 15 <= Date.now() / 1000,
})
Flow 4: Session / Id token expiration when Access token is not used
If the audience param is empty or if await getAccessToken()
is not used, the user might have an expired session / Id token.
What is the correct way to handle this?
nextjs-auth0/src/types/index.ts
Lines 17 to 28 in 0ed21d1
export interface SessionData { | |
user: User; | |
tokenSet: TokenSet; | |
internal: { | |
// the session ID from the authorization server | |
sid: string; | |
// the time at which the session was created in seconds since epoch | |
createdAt: number; | |
}; | |
connectionTokenSets?: ConnectionTokenSet[]; | |
[key: string]: unknown; | |
} |
The session object has:
- An untyped
session.exp
- this one looks like it's driven by the Auth0Client inactivityDuration / absoluteDuration params session.tokenSet.expiresAt
- is this the same as Id token's expiration time?
Flow 5: Immediate re-authentication after the user returns to an inactive page with an expired session
What is the suggested approach to check and handle this case?
Flow 6: Silent authentication
Asked here:
#2128
Additional
It would be great to have examples that show how to handle token renewal, session management and both server-side and client-side requests in a single integrated solution.
Additional context
- Next.js app uses App Router only
- Refresh tokens are rotated on each use
nextjs-auth0 version
4.6.0
Next.js version
15.3
Node.js version
22