Hi @robin.martin
I am sorry for the late reply to your inquiry.
The general recommendation would be to submit an issue via Github as you have done. Otherwise, I will try to provide more information here on the issue.
Root Cause: Auth0's MRRT feature allows a single refresh token to be used to obtain access tokens for multiple audiences in a single request. However, the auth0-spa-js library's token refresh mechanism uses a per-audience lock to prevent duplicate token refresh requests for the same audience. This design works well for sequential requests or requests to a single audience, but creates a race condition when parallel requests are made to different audiences (e.g., API-A with audience api-a and API-B with audience api-b), both audiences need token refresh, per-audience locks do not synchronize (since each audience has its own lock, both refresh requests proceed in parallel instead of being serialized), and multiple refresh token exchanges occur (both requests exchange the refresh token for new access tokens, potentially causing token reuse detection failure if Auth0's reuse detection is enabled, or inconsistent token state where the two requests end up with different refresh tokens). MRRT is designed to allow a single refresh token exchange to obtain access tokens for multiple audiences, but if the auth0-spa-js library initiates separate refresh exchanges for each audience due to per-audience locks, it defeats the purpose of MRRT and increases the risk of token reuse detection failures.
Recommended Workarounds:
Workaround 1: Use a global lock instead of per-audience locks (SDK modification)
If you have the ability to modify the auth0-spa-js library or use a patched version, implement a global lock that serializes all token refresh requests, regardless of audience. This ensures that only one refresh token exchange occurs at a time, even when multiple audiences are requested concurrently. However, this requires forking or patching the SDK, which is not ideal for long-term maintenance.
Workaround 2: Serialize audience requests in your application (Recommended)
Instead of making parallel requests to multiple audiences, serialize them in your application logic:
// Instead of this (parallel requests):
const [tokenA, tokenB] = await Promise.all([
getAccessTokenSilently({ authorizationParams: { audience: 'api-a' } }),
getAccessTokenSilently({ authorizationParams: { audience: 'api-b' } })
]);
// Do this (sequential requests):
const tokenA = await getAccessTokenSilently({ authorizationParams: { audience: 'api-a' } });
const tokenB = await getAccessTokenSilently({ authorizationParams: { audience: 'api-b' } });
This ensures that token refresh requests are serialized, preventing race conditions. The downside is that it may introduce slight latency if both tokens need to be refreshed.
Workaround 3: Request all audiences in the initial login
Configure your Auth0 application to request all audiences upfront during the initial login flow, rather than requesting them on-demand:
<Auth0Provider
domain={domain}
clientId={clientId}
authorizationParams={{
audience: 'api-a api-b', // Request all audiences upfront
scope: 'openid profile email offline_access'
}}
useRefreshTokens={true}
cacheLocation="localstorage"
>
<App />
</Auth0Provider>
This way, all access tokens are obtained in a single exchange, and subsequent requests can use the cached tokens without triggering concurrent refresh exchanges.
Workaround 4: Use a custom HTTP interceptor to queue requests
Implement a custom HTTP interceptor in your Angular application that queues requests to different audiences and processes them sequentially:
@Injectable()
export class TokenQueueInterceptor implements HttpInterceptor {
private tokenQueue: Promise<string> = Promise.resolve('');
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const audience = this.getAudienceForUrl(req.url);
this.tokenQueue = this.tokenQueue.then(() =>
this.auth0.getAccessTokenSilently({ authorizationParams: { audience } }).toPromise()
);
return this.tokenQueue.then(token => {
const clonedReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next.handle(clonedReq);
}).toPromise().then(event => of(event)).pipe(mergeMap(x => x));
}
private getAudienceForUrl(url: string): string {
if (url.includes('api-a')) return 'api-a';
if (url.includes('api-b')) return 'api-b';
return 'default';
}
}
This ensures that token requests are processed sequentially, preventing race conditions.
Next Steps:
- Check the GitHub issue: Review the GitHub issue you opened to see if Auth0 maintainers have acknowledged this as a known issue or provided guidance.
- Contact Auth0 Support: If this is blocking your production deployment, contact Auth0 Support with details about your use case and the specific race condition you are experiencing.
- Consider upgrading to auth0-react: If you are able to migrate from Angular to React, the auth0-react SDK may have improved handling of concurrent requests and MRRT.
- Monitor the auth0-spa-js repository: Watch for future releases of auth0-spa-js that may address this issue with a global lock or improved MRRT handling.
Important Notes: The per-audience lock design in auth0-spa-js is intentional to prevent duplicate refresh requests for the same audience, but it does not account for concurrent requests to different audiences. Auth0's MRRT feature is specifically designed to handle multiple audiences efficiently, but the SDK's locking mechanism may not fully leverage it. While serialization may introduce slight latency, it is the safest workaround that does not require SDK modifications.
We hope this helps you understand the race condition and provides practical workarounds. Please follow up with Auth0 Support or the GitHub issue if you need further assistance or if you discover that this is a confirmed bug in the SDK.