Skip to content

fix: add refresh token logic#1595

Closed
lucaseduoli wants to merge 2 commits into
mainfrom
fix/refresh_token
Closed

fix: add refresh token logic#1595
lucaseduoli wants to merge 2 commits into
mainfrom
fix/refresh_token

Conversation

@lucaseduoli

@lucaseduoli lucaseduoli commented May 13, 2026

Copy link
Copy Markdown
Collaborator

This pull request introduces support for proactively refreshing IBM JWT tokens when they are about to expire, improving authentication robustness and user experience. It adds new environment variables for refresh configuration, implements an asynchronous token refresh function, and ensures that refreshed tokens are set in cookies. The changes also update authentication dependencies to accommodate the new logic.

IBM JWT Token Refresh Support:

  • Added a new async function refresh_ibm_jwt in src/auth/ibm_auth.py to refresh IBM JWT tokens using the PLATFORM_REFRESH_URL environment variable, with retry logic and logging.
  • Modified the IBM authentication flow in src/dependencies.py to check token expiry and, if the token is close to expiring, attempt to refresh it and update the session cookie if successful.

Configuration Enhancements:

  • Introduced new environment variables PLATFORM_REFRESH_URL and PLATFORM_REFRESH_PERIOD in src/config/settings.py to control the refresh endpoint and threshold for token refresh.

Dependency and API Adjustments:

  • Updated authentication dependency functions in src/dependencies.py (_get_ibm_user, get_current_user, and get_optional_user) to accept and use the Response object, allowing them to set cookies after a token refresh. [1] [2] [3] [4] [5]
  • Added missing imports and minor refactoring to support the new logic. [1] [2]

Summary by CodeRabbit

Release Notes

  • New Features
    • Added IBM JWT token refresh functionality with configurable refresh endpoint and refresh period to keep platform tokens current during extended sessions.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds IBM JWT refresh capability triggered during request authentication. Defines platform refresh configuration (URL and period window), implements an async helper to POST expired tokens to the refresh endpoint with retry logic, and integrates it into the authentication dependency chain to refresh tokens and update session cookies before expiry.

Changes

IBM JWT Refresh Integration

Layer / File(s) Summary
Platform refresh configuration
src/config/settings.py
Introduces PLATFORM_REFRESH_URL environment variable and PLATFORM_REFRESH_PERIOD setting (default: 60 seconds) to control JWT refresh behavior.
JWT refresh helper function
src/auth/ibm_auth.py
Adds asyncio import and refresh_ibm_jwt async function that POSTs the current token to PLATFORM_REFRESH_URL with up to 10 retries on failure, extracts refreshed token from JSON fields or response text, and validates JWT shape (contains "."); returns None on failure or missing configuration.
Authentication dependency integration
src/dependencies.py
Updates _get_ibm_user to accept Response parameter and check token expiration; when expiring within the PLATFORM_REFRESH_PERIOD window, calls refresh_ibm_jwt and updates the IBM session cookie on success. Updates get_current_user and get_optional_user to pass Response through the dependency chain, preserving existing authentication priority and DB user attachment behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

enhancement

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: add refresh token logic' directly describes the main change: adding token refresh functionality across three key files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/refresh_token

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the backend 🔷 Issues related to backend services (OpenSearch, Langflow, APIs) label May 13, 2026
@github-actions github-actions Bot added bug 🔴 Something isn't working. and removed bug 🔴 Something isn't working. labels May 13, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/dependencies.py (1)

411-418: 💤 Low value

Move datetime and PLATFORM_REFRESH_PERIOD imports to module/function top.

These imports sit on a hot path (every authenticated request whose JWT carries an exp). import is cached after the first hit so the cost is small, but inlining them inside a nested if makes the control flow harder to read and breaks the pattern used elsewhere in this function (other settings are imported once near the top of _get_ibm_user). Suggest hoisting import datetime to the module imports and adding PLATFORM_REFRESH_PERIOD to the existing from config.settings import (...) block at lines 361–366.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dependencies.py` around lines 411 - 418, The in-function imports for
datetime and PLATFORM_REFRESH_PERIOD inside the exp check should be hoisted:
move "import datetime" to the module top-level imports and add
PLATFORM_REFRESH_PERIOD to the existing "from config.settings import (...)"
import group (used elsewhere in _get_ibm_user), then remove the nested imports
and reference datetime and PLATFORM_REFRESH_PERIOD directly in the exp handling
(the exp = claims.get("exp") / if exp - now < PLATFORM_REFRESH_PERIOD block).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/auth/ibm_auth.py`:
- Around line 109-119: The token-extraction can return the whole JSON body or
crash when data isn't a dict; update the logic in the function that refreshes
IBM tokens so you first attempt resp.json() and only treat it as a dict (use
data.get("token"/"access_token"/"refresh_token")) if isinstance(data, dict); if
json parsing fails or data is not a dict, treat resp.text as the fallback. After
extracting a candidate token (from dict keys or resp.text), validate it is a
string that looks like a JWT by requiring exactly two '.' characters (three
segments) before returning; tighten exception handling to catch
json.JSONDecodeError for json parsing and avoid broad excepts that swallow
AttributeError.
- Around line 105-125: The refresh_ibm_jwt retry loop is too long and can stall
request authentication; update refresh_ibm_jwt (called from _get_ibm_user) to
use a much smaller retry policy (e.g., 2–3 attempts) and a shorter per-attempt
timeout (e.g., 2–5s), only sleep/backoff between attempts and skip the
asyncio.sleep after the final attempt, and ensure immediate return None on final
failure; additionally add a short-lived in-memory de-dup / negative-result cache
keyed by the current token or user ID so concurrent calls to
refresh_ibm_jwt/PLATFORM_REFRESH_URL reuse a single in-flight attempt or the
recent negative result (TTL ~30–60s) to prevent fan-out.

In `@src/dependencies.py`:
- Around line 411-438: The code refreshes the IBM JWT via refresh_ibm_jwt and
reassigns ibm_token but continues to use user_id, email, and name that were
extracted earlier from the old claims; decode the newly returned token's claims
and re-derive user_id/email/name after a successful refresh (i.e., right after
setting the cookie/ibm_token) so downstream construction of the User object uses
the refreshed token's claims; reference refresh_ibm_jwt, ibm_token, claims,
user_id, email, name, response.set_cookie, IBM_SESSION_COOKIE_NAME and ensure
any logic that returns or raises (e.g., HTTPException) uses the up-to-date
derived values.
- Around line 423-429: The refreshed cookie set via response.set_cookie for
IBM_SESSION_COOKIE_NAME omits persistence and scoping attributes; update the
logic around response.set_cookie (where IBM_SESSION_COOKIE_NAME is written) to
(1) extract the JWT "exp" (if present) and compute max_age/expires and pass them
into response.set_cookie, (2) preserve or mirror the original cookie's
path/domain if available (e.g., from the incoming request cookies or headers) or
fall back to configurable defaults, and (3) make secure and samesite
configurable (or derive secure from the request scheme) instead of hardcoding
secure=True so local dev over HTTP works. Ensure these changes are applied where
the new_token is set so the rewritten cookie retains the same scope and lifetime
as the original.

---

Nitpick comments:
In `@src/dependencies.py`:
- Around line 411-418: The in-function imports for datetime and
PLATFORM_REFRESH_PERIOD inside the exp check should be hoisted: move "import
datetime" to the module top-level imports and add PLATFORM_REFRESH_PERIOD to the
existing "from config.settings import (...)" import group (used elsewhere in
_get_ibm_user), then remove the nested imports and reference datetime and
PLATFORM_REFRESH_PERIOD directly in the exp handling (the exp =
claims.get("exp") / if exp - now < PLATFORM_REFRESH_PERIOD block).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba9a66cc-7160-40ac-9bf5-891ffdb22253

📥 Commits

Reviewing files that changed from the base of the PR and between 9f25216 and 8dd07ee.

📒 Files selected for processing (3)
  • src/auth/ibm_auth.py
  • src/config/settings.py
  • src/dependencies.py

Comment thread src/auth/ibm_auth.py
Comment on lines +105 to +125
async with httpx.AsyncClient() as client:
for attempt in range(10):
try:
resp = await client.post(PLATFORM_REFRESH_URL, headers=headers, timeout=30.0)
if resp.status_code == 200:
data = resp.json()
new_token = (
data.get("token")
or data.get("access_token")
or data.get("refresh_token")
or resp.text.strip().strip('"')
)
if "." in new_token:
logger.info("Successfully refreshed IBM JWT token.")
return new_token
except Exception as e:
logger.warning(f"Failed to refresh IBM token (attempt {attempt + 1}): {e}")

await asyncio.sleep(10)

return None

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Retry loop can stall request authentication for minutes.

refresh_ibm_jwt runs inside the request-authentication path (called from _get_ibm_user). With 10 attempts, a 30s per-attempt timeout, and a 10s asyncio.sleep between attempts, a misbehaving/unreachable PLATFORM_REFRESH_URL can keep a single request hanging for ~400s before the dependency gives up. Worse, every concurrent request whose token is in the refresh window will independently enter this loop — there is no in-flight de-duplication or short-circuit — so an outage of the refresh endpoint becomes an effective DoS on the API. Also, asyncio.sleep(10) runs even after the final failed attempt, wasting another 10s before returning None.

Consider: lower the retry count and per-attempt timeout dramatically (e.g., 2–3 attempts with a small backoff), skip the sleep after the last attempt, and add a short-lived in-memory de-dup / negative-result cache keyed by the token (or user) so one failing refresh doesn’t fan out to every concurrent request.

♻️ Sketch
-    async with httpx.AsyncClient() as client:
-        for attempt in range(10):
-            try:
-                resp = await client.post(PLATFORM_REFRESH_URL, headers=headers, timeout=30.0)
+    max_attempts = 3
+    async with httpx.AsyncClient(timeout=10.0) as client:
+        for attempt in range(max_attempts):
+            try:
+                resp = await client.post(PLATFORM_REFRESH_URL, headers=headers)
                 if resp.status_code == 200:
                     ...
                     if "." in (new_token or ""):
                         logger.info("Successfully refreshed IBM JWT token.")
                         return new_token
+                else:
+                    logger.warning(
+                        "IBM JWT refresh returned non-200",
+                        attempt=attempt + 1,
+                        status_code=resp.status_code,
+                    )
             except Exception as e:
                 logger.warning(f"Failed to refresh IBM token (attempt {attempt + 1}): {e}")
-
-            await asyncio.sleep(10)
+            if attempt < max_attempts - 1:
+                await asyncio.sleep(1)
📝 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
async with httpx.AsyncClient() as client:
for attempt in range(10):
try:
resp = await client.post(PLATFORM_REFRESH_URL, headers=headers, timeout=30.0)
if resp.status_code == 200:
data = resp.json()
new_token = (
data.get("token")
or data.get("access_token")
or data.get("refresh_token")
or resp.text.strip().strip('"')
)
if "." in new_token:
logger.info("Successfully refreshed IBM JWT token.")
return new_token
except Exception as e:
logger.warning(f"Failed to refresh IBM token (attempt {attempt + 1}): {e}")
await asyncio.sleep(10)
return None
max_attempts = 3
async with httpx.AsyncClient(timeout=10.0) as client:
for attempt in range(max_attempts):
try:
resp = await client.post(PLATFORM_REFRESH_URL, headers=headers)
if resp.status_code == 200:
data = resp.json()
new_token = (
data.get("token")
or data.get("access_token")
or data.get("refresh_token")
or resp.text.strip().strip('"')
)
if "." in (new_token or ""):
logger.info("Successfully refreshed IBM JWT token.")
return new_token
else:
logger.warning(
"IBM JWT refresh returned non-200",
attempt=attempt + 1,
status_code=resp.status_code,
)
except Exception as e:
logger.warning(f"Failed to refresh IBM token (attempt {attempt + 1}): {e}")
if attempt < max_attempts - 1:
await asyncio.sleep(1)
return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/ibm_auth.py` around lines 105 - 125, The refresh_ibm_jwt retry loop
is too long and can stall request authentication; update refresh_ibm_jwt (called
from _get_ibm_user) to use a much smaller retry policy (e.g., 2–3 attempts) and
a shorter per-attempt timeout (e.g., 2–5s), only sleep/backoff between attempts
and skip the asyncio.sleep after the final attempt, and ensure immediate return
None on final failure; additionally add a short-lived in-memory de-dup /
negative-result cache keyed by the current token or user ID so concurrent calls
to refresh_ibm_jwt/PLATFORM_REFRESH_URL reuse a single in-flight attempt or the
recent negative result (TTL ~30–60s) to prevent fan-out.

Comment thread src/auth/ibm_auth.py
Comment on lines +109 to +119
if resp.status_code == 200:
data = resp.json()
new_token = (
data.get("token")
or data.get("access_token")
or data.get("refresh_token")
or resp.text.strip().strip('"')
)
if "." in new_token:
logger.info("Successfully refreshed IBM JWT token.")
return new_token

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Token-extraction fallback can return the entire JSON body as the “token”.

When the server returns a JSON object that does not contain token / access_token / refresh_token, data.get(...) yields None, so the expression falls through to resp.text.strip().strip('"'). For a JSON response such as {"foo": "a.b.c"} the fallback returns the literal body string, which contains a . and therefore passes the "." in new_token shape check — the function then returns a JSON document as if it were a JWT. Additionally, if resp.json() returns a non-dict (e.g., the server returns a bare JSON string token), data.get raises AttributeError and the whole attempt is lost in the broad except Exception.

Suggest narrowing the response handling: use the text fallback only when the response is non-JSON / non-dict, and require the extracted value to be a string of the expected JWT shape (three dot-separated segments).

♻️ Suggested handling
-                resp = await client.post(PLATFORM_REFRESH_URL, headers=headers, timeout=30.0)
-                if resp.status_code == 200:
-                    data = resp.json()
-                    new_token = (
-                        data.get("token")
-                        or data.get("access_token")
-                        or data.get("refresh_token")
-                        or resp.text.strip().strip('"')
-                    )
-                    if "." in new_token:
-                        logger.info("Successfully refreshed IBM JWT token.")
-                        return new_token
+                resp = await client.post(PLATFORM_REFRESH_URL, headers=headers, timeout=30.0)
+                if resp.status_code == 200:
+                    new_token: str | None = None
+                    try:
+                        data = resp.json()
+                    except ValueError:
+                        data = None
+                    if isinstance(data, dict):
+                        new_token = (
+                            data.get("token")
+                            or data.get("access_token")
+                            or data.get("refresh_token")
+                        )
+                    elif isinstance(data, str):
+                        new_token = data
+                    else:
+                        new_token = resp.text.strip().strip('"')
+                    if isinstance(new_token, str) and new_token.count(".") == 2:
+                        logger.info("Successfully refreshed IBM JWT token.")
+                        return new_token
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/ibm_auth.py` around lines 109 - 119, The token-extraction can return
the whole JSON body or crash when data isn't a dict; update the logic in the
function that refreshes IBM tokens so you first attempt resp.json() and only
treat it as a dict (use data.get("token"/"access_token"/"refresh_token")) if
isinstance(data, dict); if json parsing fails or data is not a dict, treat
resp.text as the fallback. After extracting a candidate token (from dict keys or
resp.text), validate it is a string that looks like a JWT by requiring exactly
two '.' characters (three segments) before returning; tighten exception handling
to catch json.JSONDecodeError for json parsing and avoid broad excepts that
swallow AttributeError.

Comment thread src/dependencies.py
Comment on lines +411 to +438
exp = claims.get("exp")
if exp:
import datetime

from config.settings import PLATFORM_REFRESH_PERIOD

now = datetime.datetime.now(datetime.UTC).timestamp()
if exp - now < PLATFORM_REFRESH_PERIOD:
logger.info("IBM JWT token is expiring soon, attempting refresh...")
new_token = await ibm_auth.refresh_ibm_jwt(ibm_token)
if new_token:
ibm_token = new_token
response.set_cookie(
key=IBM_SESSION_COOKIE_NAME,
value=new_token,
httponly=True,
secure=True,
samesite="lax",
)
logger.info("Successfully refreshed IBM JWT token and updated cookie.")
else:
if now >= exp:
if required:
raise HTTPException(
status_code=401,
detail="IBM session expired. Please log in again.",
)
return None

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale claims used after a successful refresh.

When refresh_ibm_jwt returns a new token, ibm_token is reassigned but user_id, email, and name were already computed from the old token's claims (lines 407–409). The downstream branches at lines 472 and 511 then build the User from those stale claims. In the normal case the refreshed token represents the same subject so this is benign, but if the refresh response ever returns a token bound to a different identity (e.g., impersonation, role-switch flows), the request will silently run as the old identity. Decoding the refreshed token and re-deriving user_id/email/name from its claims would close that gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dependencies.py` around lines 411 - 438, The code refreshes the IBM JWT
via refresh_ibm_jwt and reassigns ibm_token but continues to use user_id, email,
and name that were extracted earlier from the old claims; decode the newly
returned token's claims and re-derive user_id/email/name after a successful
refresh (i.e., right after setting the cookie/ibm_token) so downstream
construction of the User object uses the refreshed token's claims; reference
refresh_ibm_jwt, ibm_token, claims, user_id, email, name, response.set_cookie,
IBM_SESSION_COOKIE_NAME and ensure any logic that returns or raises (e.g.,
HTTPException) uses the up-to-date derived values.

Comment thread src/dependencies.py
Comment on lines +423 to +429
response.set_cookie(
key=IBM_SESSION_COOKIE_NAME,
value=new_token,
httponly=True,
secure=True,
samesite="lax",
)

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refreshed cookie loses persistence/scoping attributes.

response.set_cookie here omits max_age/expires, so the rewritten IBM_SESSION_COOKIE_NAME cookie becomes a session-only cookie — when the browser is closed the user is logged out, even though the JWT itself may still be valid for hours. It also doesn’t mirror whatever path/domain Traefik used when originally setting this cookie, which can produce a sibling cookie that browsers will send for some routes but not others (and secure=True will be wrong for local dev over HTTP). If the new JWT carries an exp, set it as max_age / expires, and consider making cookie attributes configurable so they match Traefik’s issuance.

♻️ Sketch
-                        if new_token:
-                            ibm_token = new_token
-                            response.set_cookie(
-                                key=IBM_SESSION_COOKIE_NAME,
-                                value=new_token,
-                                httponly=True,
-                                secure=True,
-                                samesite="lax",
-                            )
+                        if new_token:
+                            ibm_token = new_token
+                            new_claims = ibm_auth.decode_ibm_jwt(new_token) or {}
+                            new_exp = new_claims.get("exp")
+                            max_age = int(new_exp - now) if new_exp else None
+                            response.set_cookie(
+                                key=IBM_SESSION_COOKIE_NAME,
+                                value=new_token,
+                                httponly=True,
+                                secure=True,
+                                samesite="lax",
+                                max_age=max_age,
+                                path="/",
+                            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dependencies.py` around lines 423 - 429, The refreshed cookie set via
response.set_cookie for IBM_SESSION_COOKIE_NAME omits persistence and scoping
attributes; update the logic around response.set_cookie (where
IBM_SESSION_COOKIE_NAME is written) to (1) extract the JWT "exp" (if present)
and compute max_age/expires and pass them into response.set_cookie, (2) preserve
or mirror the original cookie's path/domain if available (e.g., from the
incoming request cookies or headers) or fall back to configurable defaults, and
(3) make secure and samesite configurable (or derive secure from the request
scheme) instead of hardcoding secure=True so local dev over HTTP works. Ensure
these changes are applied where the new_token is set so the rewritten cookie
retains the same scope and lifetime as the original.

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

Labels

backend 🔷 Issues related to backend services (OpenSearch, Langflow, APIs) bug 🔴 Something isn't working.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant