feat: Add IBM Watsonx Data (AMS) auth support#1090
feat: Add IBM Watsonx Data (AMS) auth support#1090edwinjosechittilappilly wants to merge 16 commits into
Conversation
Introduce support for IBM Watsonx Data (AMS) embedded authentication via the ibm-lh-console-session cookie. Add IBM_AUTH_ENABLED and IBM_JWT_PUBLIC_KEY_URL settings and .env.example entries; pre-fetch and cache IBM JWT public key at startup. Implement JWT validation and key rotation handling in new auth.ibm_auth module. Wire IBM auth into dependencies (extract/validate cookie, set request.state.user), main service initialization, auth_service (prevent Google OAuth login when IBM auth is active and expose ibm_auth_mode in user info), and session_manager (use IBM JWT as-is). Update frontend auth context and login page to recognize IBM auth mode and disable local login/logout and redirect accordingly. Adjust logout endpoint to no-op when IBM auth is enabled.
Update IBM public key fetching to expect a JSON payload with a "public_key" field instead of using raw response content. Adds validation and logging when the key is missing, encodes the key if returned as a string, and continues to cache the loaded PEM public key. Raises a ValueError on missing key to surface the error earlier.
There was a problem hiding this comment.
Pull request overview
Adds an IBM AMS (Watsonx Data embedded) authentication mode that bypasses Google OAuth, authenticates users via an IBM session cookie, and adjusts OpenSearch auth header handling to support IBM tokens.
Changes:
- Introduces IBM auth mode flags/config (
IBM_AUTH_ENABLED, cookie name, optional public-key URL) and updates “no-auth mode” behavior. - Adds IBM cookie-based user extraction on the backend plus an IBM login endpoint and
/auth/meresponse shape for IBM mode. - Updates frontend auth context + login page to support IBM auth mode and an IBM login form flow.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
src/session_manager.py |
Preserves IBM JWTs as-is (avoids anonymous JWT override) when IBM auth is enabled. |
src/services/auth_service.py |
Blocks Google OAuth app login when IBM auth mode is enabled; adds IBM-mode /auth/me response handling. |
src/main.py |
Logs IBM auth mode startup note; adds /auth/ibm/login route. |
src/dependencies.py |
Adds IBM AMS cookie user extraction and routes auth dependencies through it when enabled. |
src/config/settings.py |
Adds IBM auth env vars; changes is_no_auth_mode() semantics; adjusts OpenSearch auth header generation. |
src/auth/ibm_auth.py |
New helper for decoding/validating IBM JWTs and optionally fetching a public key. |
src/api/auth.py |
Clears IBM cookies on logout; adds IBM login endpoint for cookie setup. |
frontend/contexts/auth-context.tsx |
Adds IBM auth-mode state, IBM login helper, and mode-aware login/logout behavior. |
frontend/app/login/page.tsx |
Adds an IBM username/password login form when IBM auth mode is detected. |
.env.example |
Documents IBM auth-related environment variables. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| # Local dev fallback only — in production Traefik sets the session cookie. | ||
| if not request.cookies.get(IBM_SESSION_COOKIE_NAME): | ||
| auth_header = request.headers.get("Authorization", "") | ||
| if auth_header.startswith("Basic "): | ||
| response.set_cookie( | ||
| "ibm-auth-basic", | ||
| auth_header, | ||
| httponly=True, | ||
| samesite="lax", | ||
| ) | ||
|
|
| # ── Option 2: ibm-auth-basic cookie (local dev, no Traefik) ───────── | ||
| auth_header = request.cookies.get("ibm-auth-basic", "") | ||
| if auth_header.startswith("Basic "): | ||
| try: | ||
| decoded = base64.b64decode(auth_header[6:]).decode("utf-8") | ||
| username = decoded.split(":", 1)[0] | ||
| except Exception: | ||
| username = "unknown" | ||
|
|
||
| user = User( | ||
| user_id=username, | ||
| email=username, | ||
| name=username, | ||
| picture=None, | ||
| provider="ibm_ams_basic", | ||
| jwt_token=auth_header, | ||
| ) | ||
| request.state.user = user | ||
| return user |
| // Don't allow logout in no-auth mode or IBM auth mode | ||
| if (isNoAuthMode || isIbmAuthMode) { | ||
| console.log("Logout attempted in no-auth/IBM auth mode - ignored"); |
| const data = await response.json(); | ||
| console.log("[checkAuth] /api/auth/me response:", data); | ||
|
|
| console.log( | ||
| "[loginWithIbm] response cookies after login:", | ||
| document.cookie, | ||
| ); |
| def is_no_auth_mode(): | ||
| """Check if we're running in no-auth mode (OAuth credentials missing)""" | ||
| if IBM_AUTH_ENABLED: | ||
| return False # IBM cookie auth is a valid auth mode | ||
| result = not (GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET) | ||
| return result |
| """ | ||
| try: | ||
| return jwt.decode(token, options={"verify_signature": False}) | ||
| except jwt.DecodeError as exc: |
init_oauth() raises ValueError when IBM auth mode is enabled, but /auth/init currently catches all exceptions and returns HTTP 500. This makes a normal “login not supported in IBM mode” case look like a server error. Prefer raising an HTTPException(status_code=400/409, ...) here, or update auth_init to translate ValueError into a 4xx response.
create_user_opensearch_client() now calls jwt_token.startswith(...), which will raise if jwt_token is None. There are existing call sites that pass jwt_token=None (e.g., API key auth / internal tasks), and while SessionManager.get_effective_jwt_token() often fills this in, it can still return None for unknown users. Consider handling None explicitly (e.g., raise a clear error or default to Bearer wrapping only when jwt_token is a non-empty string) to avoid an unexpected AttributeError.
decode_ibm_jwt() only catches jwt.DecodeError, but expired tokens (and many other invalid token conditions) raise ExpiredSignatureError / InvalidTokenError and will currently bubble up, likely turning a normal “not authenticated” case into a 500. Consider catching jwt.InvalidTokenError (or jwt.PyJWTError) and returning None consistently.
| response = JSONResponse({"status": "ok"}) | ||
|
|
||
| # Local dev fallback only — in production Traefik sets the session cookie. | ||
| if not request.cookies.get(IBM_SESSION_COOKIE_NAME): |
There was a problem hiding this comment.
(1a) [Critical] Security: Authentication bypass via ibm-auth-basic cookie
Problem: Theibm_loginendpoint stores the rawAuthorization: Basic <base64>header in theibm-auth-basic cookie. _get_ibm_userthen reads that cookie, decodes only the username (discarding the password entirely at decoded.split(":", 1)[0]), and constructs a fully authenticated User.- There doesn't appears to be any credential validation
- i.e. Any caller can POST a crafted Basic header to
/auth/ibm/loginand authenticate as any arbitrary username.
- i.e. Any caller can POST a crafted Basic header to
There was a problem hiding this comment.
Proxy will be used to intercept the unautheticated requests. so Only authenticated requests will be available for BE.
cc. @zzzming
There was a problem hiding this comment.
in a follow up PR we would be reading the relevant creds from aunthenticated header from Proxy. Which will be used to authenticate Opensearch.
| return user | ||
|
|
||
| # ── Option 2: ibm-auth-basic cookie (local dev, no Traefik) ───────── | ||
| auth_header = request.cookies.get("ibm-auth-basic", "") |
There was a problem hiding this comment.
(1b) [Critical] Authentication bypass via ibm-auth-basic cookie
- See PR comment: (1a) [Critical] Security: Authentication bypass via ibm-auth-basic cookie
| return None | ||
|
|
||
|
|
||
| async def fetch_ibm_public_key(url: str): |
There was a problem hiding this comment.
(1c) [Critical] Security: validate_ibm_jwt and fetch_ibm_public_key appear to be dead code
Problem: IBM JWTs are never cryptographically verified- The module docstring advertises RS256 validation via
IBM_JWT_PUBLIC_KEY_URL, andvalidate_ibm_jwt+fetch_ibm_public_keyare fully implemented.- However, the only function called in the actual request path (
_get_ibm_userOption 1) isdecode_ibm_jwt, which usesoptions={"verify_signature": False}. - The validation infrastructure appears to be completely disconnected.
- A forged or expired IBM JWT would be accepted verbatim if it bypasses Traefik and reaches the backend directly.
IBM_JWT_PUBLIC_KEY_URLmisleads operators into believing verification is
active.
- However, the only function called in the actual request path (
There was a problem hiding this comment.
Agreed!
This was an initial approach, kept the legacy code for future tasks. we can remove it later.
| _cached_public_key = None | ||
|
|
||
|
|
||
| def decode_ibm_jwt(token: str) -> dict | None: |
There was a problem hiding this comment.
(1d) [Critical] Security: validate_ibm_jwt and fetch_ibm_public_key appear to be dead code
- See: PR comment: (1c) [Critical] Security: validate_ibm_jwt and fetch_ibm_public_key appear to be dead code
| # ── Option 1: ibm-openrag-session cookie (production via Traefik) ─── | ||
| ibm_token = request.cookies.get(IBM_SESSION_COOKIE_NAME) | ||
| if ibm_token: | ||
| claims = ibm_auth.decode_ibm_jwt(ibm_token) |
There was a problem hiding this comment.
(1e) [Critical] Security: validate_ibm_jwt and fetch_ibm_public_key appear to be dead code
- See: PR comment: (1c) [Critical] Security: validate_ibm_jwt and fetch_ibm_public_key appear to be dead code
| if not request.cookies.get(IBM_SESSION_COOKIE_NAME): | ||
| auth_header = request.headers.get("Authorization", "") | ||
| if auth_header.startswith("Basic "): | ||
| response.set_cookie( |
There was a problem hiding this comment.
(1f) [Major] ibm-auth-basic cookie missing secure=True
Problem: Theibm_loginendpoint sets the ibm-auth-basic cookie withhttponly=Trueandsamesite="lax"but nosecure=True.- The cookie value is
Basic <base64(username:password)>- Base64 is not encryption and the plaintext password is trivially recoverable (atob() in the browser).
- Without secure=True, this credential cookie will be transmitted over unencrypted HTTP connections, exposing recoverable passwords to any network observer.
There was a problem hiding this comment.
This flow is only for local development
There was a problem hiding this comment.
secure =True not needed for local development
| # IBM AMS authentication helper | ||
| # ───────────────────────────────────────────── | ||
|
|
||
| def _get_ibm_user(request: Request, required: bool) -> Optional["User"]: |
There was a problem hiding this comment.
(1g) [Normal] _get_ibm_user never registers the user in session_manager.users
Problem: The API key auth path has an explicit comment and code:if user.user_id not in session_manager.users: session_manager.users[user.user_id] = user- establishing the pattern that all auth paths must register users so get_effective_jwt_token can look them up._get_ibm_userdoes not appear to do this.- When downstream code calls
get_effective_jwt_token(ibm_user_id, None), theget_user(user_id)lookup returnsNonefor IBM users, causingget_effective_jwt_tokento returnNone- which silently produces an unauthenticated OpenSearch client rather than explicitly failing. Proposed Solution: Follow API key path pattern in src/dependencies.py lines 283–284 (?)
There was a problem hiding this comment.
This is the second part of the Auth solution which will be in seperate PR for Accessing the IBM Login Creds from headers for an Authenticated Request from Traefic
| {"status": "logged_out", "message": "Successfully logged out"} | ||
| ) | ||
|
|
||
| if IBM_AUTH_ENABLED: |
There was a problem hiding this comment.
(1h) [Normal] auth_logout deletes a Traefik-managed cookie (false logout confidence)
Problem: auth_logout callsresponse.delete_cookie(key=IBM_SESSION_COOKIE_NAME, ...)to clear ibm-openrag-session.- But this cookie is owned and set by Traefik, not the backend.
- The backend's
Set-Cookie: ibm-openrag-session=; expires=<past>header clears it from the browser, but on the very next proxied request Traefik re-validates with AMS (which still has an active session) and re-injects the cookie. - True session termination requires an AMS-side call.
- The logout appears to succeed but doesn't actually terminate the IBM session.
- Impact is partially mitigated by the frontend already no-opting logout when isIbmAuthMode is true.
There was a problem hiding this comment.
Added a partial logout and proper message for handling this too.
| "ok:", | ||
| response.ok, | ||
| ); | ||
| console.log( |
There was a problem hiding this comment.
(1i) [Minor] Debug console.log statements left in production, including document.cookie dump
Problem: Multiple console.log debug statements were left in this file- Recommend evaluating all
console.logstatements and consider removing if done debugging
- Recommend evaluating all
Problem: Dumping all browser cookies (including ibm-openrag-session and any other session tokens) to the DevTools console unconditionally on every IBM login.- Note: No remote exploitability, but exposes session state on shared or monitored machines.
- This is a Minor severity issue. Please feel free to optionally fix or ignore
|
|
Thank You @mpawlow for the review. |
When IBM_AUTH_ENABLED, return a "partial_logout" response that clears IBM-related browser cookies but warns that the server-side IBM/AMS session may still be active and should be terminated via the identity provider. The default "logged_out" response is now returned only when IBM auth is not enabled. Also added a comment about not requiring secure=True for local development when setting the "ibm-auth-basic" cookie in ibm_login.
Introduce support for IBM Watsonx Data (AMS) embedded authentication via the ibm-lh-console-session cookie. Add IBM_AUTH_ENABLED and IBM_JWT_PUBLIC_KEY_URL settings and .env.example entries; pre-fetch and cache IBM JWT public key at startup. Implement JWT validation and key rotation handling in new auth.ibm_auth module. Wire IBM auth into dependencies (extract/validate cookie, set request.state.user), main service initialization, auth_service (prevent Google OAuth login when IBM auth is active and expose ibm_auth_mode in user info), and session_manager (use IBM JWT as-is). Update frontend auth context and login page to recognize IBM auth mode and disable local login/logout and redirect accordingly. Adjust logout endpoint to no-op when IBM auth is enabled.