Skip to content

Handle missing refresh token in admin client - #51232

Open
rblaine95 wants to merge 1 commit into
keycloak:mainfrom
rblaine95:fix-admin-client-missing-refresh-token
Open

Handle missing refresh token in admin client#51232
rblaine95 wants to merge 1 commit into
keycloak:mainfrom
rblaine95:fix-admin-client-missing-refresh-token

Conversation

@rblaine95

@rblaine95 rblaine95 commented Jul 28, 2026

Copy link
Copy Markdown

Keycloak only issues a refresh token for the client_credentials
grant when the client is configured to, so auth() ended up passing
undefined to decodeToken() and threw a TypeError. The refresh
token is now optional in the token response types, and
setRefreshToken() clears the decoded token instead of trying to
decode nothing.

The same applies when refreshing: if the token endpoint leaves the
refresh token out of the response, the existing one is kept rather
than cleared.

Closes #50845
Relates to #46834


Generative AI Disclosure:

Claude Opus 5 was used in this pull request.

Copilot AI review requested due to automatic review settings July 28, 2026 09:51
@rblaine95
rblaine95 requested review from a team as code owners July 28, 2026 09:51
edewit
edewit previously approved these changes Jul 28, 2026
@edewit
edewit enabled auto-merge (squash) July 28, 2026 10:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Updates the Keycloak admin client to tolerate missing refresh_token values in token responses (notably for client_credentials) and preserves an existing refresh token when a refresh response omits it.

Changes:

  • Make refresh_token optional in the token response typings.
  • Allow setRefreshToken() to accept undefined and avoid decoding when missing.
  • Add tests covering missing refresh token on initial auth and on refresh.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
js/libs/keycloak-admin-client/test/client.spec.ts Adds test coverage for missing/omitted refresh tokens using a local HTTP stub server.
js/libs/keycloak-admin-client/src/utils/auth.ts Makes refresh token fields optional in token response interfaces.
js/libs/keycloak-admin-client/src/client.ts Handles absent refresh tokens safely and keeps existing refresh token on refresh when omitted.

Comment thread js/libs/keycloak-admin-client/test/client.spec.ts Outdated
Comment thread js/libs/keycloak-admin-client/test/client.spec.ts Outdated
Comment thread js/libs/keycloak-admin-client/test/client.spec.ts
Comment thread js/libs/keycloak-admin-client/test/client.spec.ts
Copilot AI review requested due to automatic review settings July 28, 2026 10:25
auto-merge was automatically disabled July 28, 2026 10:25

Head branch was pushed to by a user without write access

@rblaine95
rblaine95 force-pushed the fix-admin-client-missing-refresh-token branch from c72c77a to 4edce93 Compare July 28, 2026 10:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

js/libs/keycloak-admin-client/test/client.spec.ts:30

  • responses is declared but not initialized before it’s used in the server request handler (responses.shift()), which can throw TypeError: Cannot read properties of undefined if a request arrives before a test assigns responses (or if a test forgets to). Initialize responses to an empty array (and consider resetting it in beforeEach) so the handler fails gracefully with the intended 500 response instead of crashing.
  let responses: object[];

  before(async () => {
    server = createServer((_req, res) => {
      const response = responses.shift();
      res.writeHead(response ? 200 : 500, {
        "content-type": "application/json",
      });
      res.end(JSON.stringify(response ?? { error: "No response queued." }));
    });

js/libs/keycloak-admin-client/src/client.ts:150

  • Using a truthiness check here treats an empty string refresh token as “missing” and silently clears decoded state. If the intent is specifically “only skip decode when the token is absent,” check for token !== undefined (or typeof token !== \"undefined\") rather than truthiness so behavior matches the declared type (string | undefined).
  public setRefreshToken(token?: string) {
    this.refreshToken = token;
    this.#refreshTokenDecoded = token ? decodeToken(token) : undefined;

js/libs/keycloak-admin-client/src/client.ts:190

  • This condition is meant to detect “omitted refresh token,” but if (refreshToken) is a truthiness check and will also skip updates for empty-string values. To align with the comment and with refreshToken?: string, check explicitly for omission (e.g., refreshToken !== undefined) so the logic is based on presence rather than truthiness.
    // The current refresh token remains valid if the response does not carry a new one.
    if (refreshToken) {
      this.setRefreshToken(refreshToken);
    }

Copilot AI review requested due to automatic review settings July 28, 2026 10:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

js/libs/keycloak-admin-client/src/client.ts:190

  • if (refreshToken) treats an empty-string refresh token as “missing” and will incorrectly keep the old refresh token. Prefer checking explicitly for omission (e.g., refreshToken !== undefined) so only a truly absent field keeps the prior token.
    this.setAccessToken(accessToken);

    // The current refresh token remains valid if the response does not carry a new one.
    if (refreshToken) {
      this.setRefreshToken(refreshToken);
    }

js/libs/keycloak-admin-client/test/client.spec.ts:44

  • The stubbed token endpoint responses don’t match the library’s declared TokenResponseRaw shape (many fields are required by type, but omitted in tests). To keep these tests robust against future implementation changes (e.g., if auth starts reading expires_in, token_type, etc.), consider returning a more realistic full response object in the fixtures while still omitting refresh_token in the relevant cases.
    responses = [{ access_token: token(60) }];

js/libs/keycloak-admin-client/test/client.spec.ts:59

  • The stubbed token endpoint responses don’t match the library’s declared TokenResponseRaw shape (many fields are required by type, but omitted in tests). To keep these tests robust against future implementation changes (e.g., if auth starts reading expires_in, token_type, etc.), consider returning a more realistic full response object in the fixtures while still omitting refresh_token in the relevant cases.
    responses = [
      { access_token: token(-60), refresh_token: refreshToken },
      { access_token: renewedAccessToken },
    ];

@edewit
edewit enabled auto-merge (squash) July 28, 2026 11:50
Copilot AI review requested due to automatic review settings July 31, 2026 11:05
@edewit
edewit force-pushed the fix-admin-client-missing-refresh-token branch from 4edce93 to 696d3bc Compare July 31, 2026 11:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (2)

js/libs/keycloak-admin-client/test/client.spec.ts:37

  • closeAllConnections() was added in Node 18.2, while this package declares support for all Node versions >=18; on 18.0/18.1 the teardown throws a TypeError. Guard the optional cleanup method so the test still runs across the declared engine range.
    server.closeAllConnections();

js/libs/keycloak-admin-client/src/client.ts:150

  • The new auth test starts with an empty client, so it would still pass if a missing refresh token were handled as a no-op and a token from a previous auth() remained. Add a regression case that authenticates once with a refresh token and then without one, asserting both the stored and decoded refresh-token state are cleared (for example via isRefreshTokenExpired() after using an already-expired first token).
  public setRefreshToken(token?: string) {
    this.refreshToken = token;
    this.#refreshTokenDecoded = token ? decodeToken(token) : undefined;

Keycloak only issues a refresh token for the `client_credentials`
grant when the client is configured to, so `auth()` ended up passing
undefined to `decodeToken()` and threw a `TypeError`. The refresh
token is now optional in the token response types, and
`setRefreshToken()` clears the decoded token instead of trying to
decode nothing.

The same applies when refreshing: if the token endpoint leaves the
refresh token out of the response, the existing one is kept rather
than cleared.

Closes keycloak#50845

Signed-off-by: Robbie Blaine <robbieblaine.rb@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 18:33
auto-merge was automatically disabled July 31, 2026 18:33

Head branch was pushed to by a user without write access

@rblaine95
rblaine95 force-pushed the fix-admin-client-missing-refresh-token branch from 696d3bc to 388e9ca Compare July 31, 2026 18:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplicate of #46382, #46178, #46433 : reporting because none were actually fixed for this case.

3 participants