Skip to content

feat: Add IBM Watsonx Data (AMS) auth support#1090

Closed
edwinjosechittilappilly wants to merge 16 commits into
mainfrom
IBM_AMS
Closed

feat: Add IBM Watsonx Data (AMS) auth support#1090
edwinjosechittilappilly wants to merge 16 commits into
mainfrom
IBM_AMS

Conversation

@edwinjosechittilappilly

Copy link
Copy Markdown
Collaborator

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.

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.
@github-actions github-actions Bot added frontend 🟨 Issues related to the UI/UX backend 🔷 Issues related to backend services (OpenSearch, Langflow, APIs) labels Mar 10, 2026
edwinjosechittilappilly and others added 6 commits March 11, 2026 11:59
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.
@edwinjosechittilappilly edwinjosechittilappilly marked this pull request as ready for review March 16, 2026 15:55
Copilot AI review requested due to automatic review settings March 16, 2026 15:55
@edwinjosechittilappilly edwinjosechittilappilly changed the title WIP: Add IBM Watsonx Data (AMS) auth support feat: Add IBM Watsonx Data (AMS) auth support Mar 16, 2026
@github-actions github-actions Bot added the enhancement 🔵 New feature or request label Mar 16, 2026

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

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/me response 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.

Comment thread src/api/auth.py
Comment on lines +152 to +162
# 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",
)

Comment thread src/dependencies.py
Comment on lines +123 to +141
# ── 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
Comment thread src/services/auth_service.py
Comment thread src/config/settings.py
Comment on lines +200 to +202
// 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");
Comment on lines 65 to 67
const data = await response.json();
console.log("[checkAuth] /api/auth/me response:", data);

Comment on lines +188 to +191
console.log(
"[loginWithIbm] response cookies after login:",
document.cookie,
);
Comment thread .env.example Outdated
Comment thread src/config/settings.py
Comment on lines 98 to 103
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
Comment thread src/auth/ibm_auth.py Outdated
"""
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.
@github-actions github-actions Bot added enhancement 🔵 New feature or request and removed enhancement 🔵 New feature or request labels Mar 16, 2026
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.
@github-actions github-actions Bot added enhancement 🔵 New feature or request and removed enhancement 🔵 New feature or request labels Mar 16, 2026
@github-actions github-actions Bot added enhancement 🔵 New feature or request and removed enhancement 🔵 New feature or request labels Mar 16, 2026
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.
@github-actions github-actions Bot added enhancement 🔵 New feature or request and removed enhancement 🔵 New feature or request labels Mar 16, 2026
@github-actions github-actions Bot added enhancement 🔵 New feature or request and removed enhancement 🔵 New feature or request labels Mar 16, 2026
@github-actions github-actions Bot added enhancement 🔵 New feature or request and removed enhancement 🔵 New feature or request labels Mar 16, 2026

@mpawlow mpawlow left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@edwinjosechittilappilly

Code Review 1

  • See PR comments: (1a) to (1i)

Comment thread src/api/auth.py
response = JSONResponse({"status": "ok"})

# Local dev fallback only — in production Traefik sets the session cookie.
if not request.cookies.get(IBM_SESSION_COOKIE_NAME):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1a) [Critical] Security: Authentication bypass via ibm-auth-basic cookie

  • Problem: The ibm_login endpoint stores the raw Authorization: Basic <base64> header in the ibm-auth-basic cookie. _get_ibm_user then 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/login and authenticate as any arbitrary username.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Proxy will be used to intercept the unautheticated requests. so Only authenticated requests will be available for BE.

cc. @zzzming

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

in a follow up PR we would be reading the relevant creds from aunthenticated header from Proxy. Which will be used to authenticate Opensearch.

Comment thread src/dependencies.py
return user

# ── Option 2: ibm-auth-basic cookie (local dev, no Traefik) ─────────
auth_header = request.cookies.get("ibm-auth-basic", "")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1b) [Critical] Authentication bypass via ibm-auth-basic cookie

  • See PR comment: (1a) [Critical] Security: Authentication bypass via ibm-auth-basic cookie

Comment thread src/auth/ibm_auth.py
return None


async def fetch_ibm_public_key(url: str):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(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, and validate_ibm_jwt + fetch_ibm_public_key are fully implemented.
    • However, the only function called in the actual request path (_get_ibm_user Option 1) is decode_ibm_jwt, which uses options={"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_URL misleads operators into believing verification is
      active.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed!
This was an initial approach, kept the legacy code for future tasks. we can remove it later.

Comment thread src/auth/ibm_auth.py
_cached_public_key = None


def decode_ibm_jwt(token: str) -> dict | None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(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

Comment thread src/dependencies.py
# ── 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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(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

Comment thread src/api/auth.py
if not request.cookies.get(IBM_SESSION_COOKIE_NAME):
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Basic "):
response.set_cookie(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1f) [Major] ibm-auth-basic cookie missing secure=True

  • Problem: The ibm_login endpoint sets the ibm-auth-basic cookie with httponly=True and samesite="lax" but no secure=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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This flow is only for local development

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

secure =True not needed for local development

Comment thread src/dependencies.py
# IBM AMS authentication helper
# ─────────────────────────────────────────────

def _get_ibm_user(request: Request, required: bool) -> Optional["User"]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(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_user does not appear to do this.
  • When downstream code calls get_effective_jwt_token(ibm_user_id, None), the get_user(user_id) lookup returns None for IBM users, causing get_effective_jwt_token to return None - 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 (?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Comment thread src/api/auth.py
{"status": "logged_out", "message": "Successfully logged out"}
)

if IBM_AUTH_ENABLED:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1h) [Normal] auth_logout deletes a Traefik-managed cookie (false logout confidence)

  • Problem: auth_logout calls response.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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added a partial logout and proper message for handling this too.

"ok:",
response.ok,
);
console.log(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(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.log statements and consider removing if done debugging
  • 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

@github-actions github-actions Bot removed the lgtm label Mar 17, 2026
@github-actions github-actions Bot added enhancement 🔵 New feature or request and removed enhancement 🔵 New feature or request labels Mar 17, 2026
@mpawlow

mpawlow commented Mar 17, 2026

Copy link
Copy Markdown
Collaborator

@edwinjosechittilappilly

Validate whether there isn't any impact on the current auth flow when the IBM_AUTH_ENABLED environment variable is set to false

  • Verified. There isn't any impact on the existing auth flow when IBM_AUTH_ENABLED=false
  • i.e. Every modified code path is properly gated:
    • src/dependencies.py - get_current_user / get_optional_user
    • src/session_manager.py - get_effective_jwt_token
    • src/config/settings.py - is_no_auth_mode
    • src/services/auth_service.py - init_oauth / get_user_info
    • src/api/auth.py -> auth_logout
    • src/api/auth.py -> ibm_login endpoint

@edwinjosechittilappilly

Copy link
Copy Markdown
Collaborator Author

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.
@github-actions github-actions Bot added enhancement 🔵 New feature or request and removed enhancement 🔵 New feature or request labels Mar 17, 2026
@github-actions github-actions Bot added enhancement 🔵 New feature or request and removed enhancement 🔵 New feature or request labels Mar 17, 2026
@mpawlow mpawlow removed their request for review April 15, 2026 14:03
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) enhancement 🔵 New feature or request frontend 🟨 Issues related to the UI/UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants