Skip to content

Conversation

@wilsonrivera
Copy link
Contributor

@wilsonrivera wilsonrivera commented Jan 7, 2026

The goal of this PR is to avoid creating Keycloak access tokens every time we call authenticateClient.

To achieve this we are taking a look at the existing token (if any) and, if that token still valid, we don't do anything, however, if there is a refresh token, we try to refresh the current access token with it and if everything fails or is unavailable, we fallback to the original flow or creating a new access token.

Summary by CodeRabbit

  • Bug Fixes
    • Improved authentication efficiency by checking token expiration before re-authenticating, reducing unnecessary authentication calls.
    • Added support for token refresh flow to extend session validity without requiring full re-authentication.

✏️ Tip: You can customize this high-level summary in your review settings.

Checklist

  • I have discussed my proposed changes in an issue and have received approval to proceed.
  • I have followed the coding standards of the project.
  • Tests or benchmarks have been added or updated.
  • Documentation has been updated on https://github.com/wundergraph/cosmo-docs.
  • I have read the Contributors Guide.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 7, 2026

Walkthrough

The Keycloak authentication service is enhanced to check JWT token expiration and conditionally refresh tokens. When an access token exists, it's decoded to verify expiration; if valid, re-authentication is skipped. If expired and a refresh token is available, a refresh token grant is used to obtain a new token. The password grant remains the fallback mechanism.

Changes

Cohort / File(s) Summary
Keycloak authentication token handling
controlplane/src/core/services/Keycloak.ts
Added decodeJwt import from jose. Enhanced authenticateClient to decode existing access tokens, check expiration (exp claim), skip re-auth if valid, attempt refresh token grant if expired, and handle decoding failures gracefully. Password grant flow retained as fallback.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the main objective: avoiding unnecessary token creation by checking expiration and using refresh tokens when possible.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @controlplane/src/core/services/Keycloak.ts:
- Around line 41-48: The current check in the Keycloak token validation treats
tokens missing an exp claim as valid indefinitely; update the logic in the
Keycloak method that inspects this.client.accessToken (where decodeJwt is used)
so that absence of exp does NOT cause an early return—only return early when exp
exists and exp*1000 > Date.now(); if exp is missing or expired, fall through to
re-authenticate. Ensure you only change the conditional that now reads something
like `if (!exp || exp * 1000 > Date.now()) return;` to require exp to be present
and valid (e.g., `if (exp && exp * 1000 > Date.now()) return;`), so tokens
without expiration force re-authentication.
🧹 Nitpick comments (2)
controlplane/src/core/services/Keycloak.ts (2)

50-58: Token refresh logic looks correct.

The refresh token flow properly uses the refresh_token grant type and maintains consistency with the password flow by using the same admin-cli client ID.

Consider adding logging when token refresh succeeds to aid in debugging and monitoring:

🔎 Suggested logging enhancement
         if (this.client.refreshToken) {
           await this.client.auth({
             grantType: 'refresh_token',
             refreshToken: this.client.refreshToken,
             clientId: 'admin-cli',
           });
-
+          this.logger.debug('Successfully refreshed Keycloak admin access token');
           return;
         }

59-61: Add logging in the error handler for better observability.

The silent catch block makes it difficult to diagnose token refresh failures. Adding debug-level logging would help identify whether the issue is token decode failure, refresh token expiration, or network problems.

🔎 Proposed enhancement
-      } catch {
-        // ignore
+      } catch (error) {
+        this.logger.debug(error, 'Token validation or refresh failed, falling back to password grant');
       }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa4c66a and ddc6c0c.

📒 Files selected for processing (1)
  • controlplane/src/core/services/Keycloak.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: wilsonrivera
Repo: wundergraph/cosmo PR: 2100
File: docker/keycloak/realm.json:1768-1774
Timestamp: 2025-08-04T17:50:30.553Z
Learning: The sso-cookie-authenticator in Keycloak authentication flows must use "REQUIRED" as the requirement setting, not "ALTERNATIVE". This is due to implementation constraints of the custom SSOCookieAuthenticator class.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build_push_image
  • GitHub Check: build_test
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
🔇 Additional comments (2)
controlplane/src/core/services/Keycloak.ts (2)

44-48: Consider the security implications of not verifying JWT signatures.

decodeJwt only decodes the token without cryptographic signature verification. While this may be acceptable for the admin client's self-managed token (since Keycloak will validate it on actual API calls), it does mean that a tampered or forged token could bypass the expiration check.

Please confirm that skipping signature verification aligns with your security requirements. If this token could be exposed or if there's a risk of tampering, consider using jwtVerify from jose instead, which validates signatures against a JWKS endpoint.


6-6: No action needed. The jose dependency is properly declared in controlplane/package.json as "jose": "^5.2.4". The import statement at line 6 is valid and will not cause runtime failures.

Likely an incorrect or invalid review comment.

Comment on lines +41 to +48
if (this.client.accessToken) {
// We already have an access token, determine whether the token still valid before trying to authenticate again
try {
const { exp } = decodeJwt(this.client.accessToken);
if (!exp || exp * 1000 > Date.now()) {
// Either the access token never expires or it hasn't expired
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reconsider accepting tokens without an expiration claim.

Line 45 returns early when !exp is true, meaning tokens without an expiration claim are treated as valid indefinitely. This poses a security risk because:

  • Compromised tokens would remain valid forever
  • There's no mechanism to force re-authentication
  • It violates JWT security best practices

Consider changing the logic to require expiration:

🔎 Proposed fix to require expiration claim
-        const { exp } = decodeJwt(this.client.accessToken);
-        if (!exp || exp * 1000 > Date.now()) {
-          // Either the access token never expires or it hasn't expired
+        const { exp } = decodeJwt(this.client.accessToken);
+        if (exp && exp * 1000 > Date.now()) {
+          // Access token is valid and hasn't expired
           return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (this.client.accessToken) {
// We already have an access token, determine whether the token still valid before trying to authenticate again
try {
const { exp } = decodeJwt(this.client.accessToken);
if (!exp || exp * 1000 > Date.now()) {
// Either the access token never expires or it hasn't expired
return;
}
if (this.client.accessToken) {
// We already have an access token, determine whether the token still valid before trying to authenticate again
try {
const { exp } = decodeJwt(this.client.accessToken);
if (exp && exp * 1000 > Date.now()) {
// Access token is valid and hasn't expired
return;
}
🤖 Prompt for AI Agents
In @controlplane/src/core/services/Keycloak.ts around lines 41 - 48, The current
check in the Keycloak token validation treats tokens missing an exp claim as
valid indefinitely; update the logic in the Keycloak method that inspects
this.client.accessToken (where decodeJwt is used) so that absence of exp does
NOT cause an early return—only return early when exp exists and exp*1000 >
Date.now(); if exp is missing or expired, fall through to re-authenticate.
Ensure you only change the conditional that now reads something like `if (!exp
|| exp * 1000 > Date.now()) return;` to require exp to be present and valid
(e.g., `if (exp && exp * 1000 > Date.now()) return;`), so tokens without
expiration force re-authentication.

@codecov
Copy link

codecov bot commented Jan 7, 2026

Codecov Report

❌ Patch coverage is 47.05882% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.74%. Comparing base (aa4c66a) to head (ddc6c0c).

Files with missing lines Patch % Lines
controlplane/src/core/services/Keycloak.ts 47.05% 9 Missing ⚠️

❌ Your patch check has failed because the patch coverage (47.05%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2443      +/-   ##
==========================================
- Coverage   62.74%   62.74%   -0.01%     
==========================================
  Files         296      296              
  Lines       41353    41370      +17     
  Branches     4247     4251       +4     
==========================================
+ Hits        25949    25957       +8     
- Misses      15383    15392       +9     
  Partials       21       21              
Files with missing lines Coverage Δ
controlplane/src/core/services/Keycloak.ts 81.26% <47.05%> (-1.69%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

2 participants