Open aaronadamsCA opened 11 months ago
Some thoughts on solutions.
The Stack Overflow answers suggest a state store as a guard:
state
is stored, it's pushed to an array.state
to the entire array.This would appear to resolve the issue:
state
to session, redirects to OAuthstate
to session, redirects to OAuthstate
from session, writes user
to sessionuser
from sessionDue to race conditions with session storage itself, it's not a perfect solution:
session
from session storagesession
from session storagesession
to session storagesession
to session storage
session
But the race condition window should be substantially narrowed from seconds to milliseconds.
It's also worth noting implementers are currently encouraged to store their own auth state (e.g. returnTo
) separately, and this is subject to the same race condition (tab 1 returnTo
is overwritten by tab 2 returnTo
). If this library chooses to implement a state store, I think it would make a lot of sense to reconsider allowing custom state (or just returnTo
) to be stored alongside the UUID for this reason.
I recognise it's a late response - but we solved this with the following pattern:
isAuthenticated
. It wraps our call to authenticator.isAuthenticated
./auth/login
, which performs the session handling. We set a returnTo
value that allows us to bring them back to where they came from after login./auth/login
has only a single loader - executed for the route. This is where we execute authenticator.authenticate
then - without any race as it's only called once.That's a great idea, but I think it solves race conditions between routes whereas this issue is between tabs.
For now we're working around it with a retry inside our auth callback route:
try {
return await authenticator.authenticate("auth0", request, {
successRedirect: returnPath,
throwOnError: true,
});
} catch (exception) {
if (exception instanceof AuthorizationError) {
throw redirect(createAuthPath(returnPath));
}
throw exception;
}
But I still think the best way to solve this is a state store like this:
"oauth2:state": {
"12345678": {},
"90abcdef": {}
}
This would allow parallel authenticate()
calls to complete successfully.
It would also allow #73 to be reopened so we could securely store our own values (like returnTo
) inside those empty objects without any security impacts. I'm still pretty sure that issue was closed only because the ask was misunderstood.
We started running into occasional Auth0 failures with "State doesn't match". After a day of banging my head against the wall, I think I have a pretty good idea of what's going on:
https://stackoverflow.com/questions/65493296/authorization-code-flow-concurrent-requests-from-multiple-tabs
If a user simultaneously loads multiple pages while unauthenticated, the result is a race condition:
state
and redirects to OAuthstate
and redirects to OAuthstate
This is pretty common when reopening a closed browser, for example.