Describe the bug
OAuthService.refreshToken() unconditionally POSTs grant_type=refresh_token with whatever is currently in storage. There is no guard, no shared in-flight promise and no queue, so two overlapping calls send the same refresh token twice.
With refresh token rotation and replay detection enabled - the Keycloak default (Revoke Refresh Token on, Refresh Token Max Reuse = 0), and what RFC 9700 section 4.14.2 requires of public clients - the second request is deterministically rejected with invalid_grant, and the application loses its session.
Three callers can overlap, two of them inside the library itself:
setupAutomaticSilentRefresh() -> refreshInternal() -> refreshToken()
handleSessionChange() -> refreshToken(), driven by the OP session-check iframe
- any direct application call, e.g. from an HTTP interceptor retrying several 401s at once
silentRefresh() at least tears down an existing iframe before starting a new one; refreshToken() has no equivalent.
To reproduce
- Configure code flow with
useSilentRefresh: false against an identity provider with refresh token rotation and reuse detection.
- Call
oauthService.refreshToken() twice in the same tick, or let setupAutomaticSilentRefresh() fire while an interceptor also refreshes.
- The first call succeeds and rotates the token; the second is rejected with
invalid_grant.
Expected behaviour
Concurrent refreshToken() calls share one in-flight request and resolve with the same TokenResponse, so a rotated refresh token is never sent twice.
Suggested fix
private tokenRefresh?: Promise<TokenResponse>;
public refreshToken(): Promise<TokenResponse> {
this.tokenRefresh ??= this.refreshTokenInternal().finally(() => {
this.tokenRefresh = undefined;
});
return this.tokenRefresh;
}
where refreshTokenInternal() is the current body.
Version: angular-oauth2-oidc 20.0.3 (present in 19.x as well)
Describe the bug
OAuthService.refreshToken()unconditionally POSTsgrant_type=refresh_tokenwith whatever is currently in storage. There is no guard, no shared in-flight promise and no queue, so two overlapping calls send the same refresh token twice.With refresh token rotation and replay detection enabled - the Keycloak default (
Revoke Refresh Tokenon,Refresh Token Max Reuse = 0), and what RFC 9700 section 4.14.2 requires of public clients - the second request is deterministically rejected withinvalid_grant, and the application loses its session.Three callers can overlap, two of them inside the library itself:
setupAutomaticSilentRefresh()->refreshInternal()->refreshToken()handleSessionChange()->refreshToken(), driven by the OP session-check iframesilentRefresh()at least tears down an existing iframe before starting a new one;refreshToken()has no equivalent.To reproduce
useSilentRefresh: falseagainst an identity provider with refresh token rotation and reuse detection.oauthService.refreshToken()twice in the same tick, or letsetupAutomaticSilentRefresh()fire while an interceptor also refreshes.invalid_grant.Expected behaviour
Concurrent
refreshToken()calls share one in-flight request and resolve with the sameTokenResponse, so a rotated refresh token is never sent twice.Suggested fix
where
refreshTokenInternal()is the current body.Version: angular-oauth2-oidc 20.0.3 (present in 19.x as well)