Skip to content

refreshToken() has no in-flight de-duplication, which breaks refresh token rotation #1509

Description

@BegoPochert

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

  1. Configure code flow with useSilentRefresh: false against an identity provider with refresh token rotation and reuse detection.
  2. Call oauthService.refreshToken() twice in the same tick, or let setupAutomaticSilentRefresh() fire while an interceptor also refreshes.
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions