Custom auth HTTPException status_code is always converted to 403 #1086

Open
opened 2026-02-20 17:43:01 -05:00 by yindo · 1 comment
Owner

Originally created by @dokterbob on GitHub (Dec 8, 2025).

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and detailed title that summarizes the issue.
  • I read what a minimal reproducible example is.
  • I included a self-contained, minimal example that demonstrates the issue.

Example Code

from langgraph_sdk import Auth

auth = Auth()

@auth.authenticate
async def get_current_user(authorization: str | None):
    if not authorization:
        # This should return HTTP 401, but returns 403
        raise auth.exceptions.HTTPException(
            status_code=401, detail="No token provided."
        )
    
    # ... validate token ...
    
    if token_invalid:
        # This should also return HTTP 401, but returns 403
        raise auth.exceptions.HTTPException(
            status_code=401, detail="Invalid token"
        )
    
    return {"identity": "user-id"}

Error Message and Stack Trace (if applicable)

Expected HTTP response: 401 Unauthorized
Actual HTTP response: 403 Forbidden

Description

When using custom authentication with @auth.authenticate, any Auth.exceptions.HTTPException raised with status_code=401 is converted to a 403 Forbidden response.

Root Cause Analysis (from langgraph-api==0.5.3):

  1. In langgraph_api/auth/custom.py, the CustomAuthBackend.authenticate() method catches Auth.exceptions.HTTPException:
except Auth.exceptions.HTTPException as e:
    if e.status_code == 401 or e.status_code == 403:
        raise AuthenticationError(e.detail) from None  # <-- status_code is LOST here
  1. Starlette's AuthenticationError doesn't have a status_code attribute - it only carries the message.

  2. In langgraph_api/auth/middleware.py, the on_error handler always returns 403:

def on_error(conn: HTTPConnection, exc: AuthenticationError):
    return JSONResponse({"detail": str(exc)}, status_code=403)  # <-- Always 403!

Impact:

  • Security implications: 401 (Unauthorized) and 403 (Forbidden) have different semantic meanings
  • 401 indicates missing/invalid credentials and typically prompts re-authentication
  • 403 indicates the user is authenticated but lacks permission
  • This breaks standard HTTP semantics and client-side error handling logic

Suggested Fix:

Option 1: Preserve status code through the error chain

# In custom.py
class AuthenticationErrorWithStatus(AuthenticationError):
    def __init__(self, message: str, status_code: int = 401):
        super().__init__(message)
        self.status_code = status_code

# When catching HTTPException
raise AuthenticationErrorWithStatus(e.detail, e.status_code) from None

# In middleware.py
def on_error(conn: HTTPConnection, exc: AuthenticationError):
    status = getattr(exc, 'status_code', 403)
    return JSONResponse({"detail": str(exc)}, status_code=status)

Option 2: Re-raise HTTPException directly instead of converting to AuthenticationError

System Info

  • langgraph-api version: 0.5.3 (also likely affects newer versions up to 0.5.32)
  • langgraph-sdk version: Any version with Auth.exceptions.HTTPException
  • Python version: 3.12
  • OS: macOS / Linux
Originally created by @dokterbob on GitHub (Dec 8, 2025). ### Checked other resources - [x] This is a bug, not a usage question. - [x] I added a clear and detailed title that summarizes the issue. - [x] I read what a minimal reproducible example is. - [x] I included a self-contained, minimal example that demonstrates the issue. ### Example Code ```python from langgraph_sdk import Auth auth = Auth() @auth.authenticate async def get_current_user(authorization: str | None): if not authorization: # This should return HTTP 401, but returns 403 raise auth.exceptions.HTTPException( status_code=401, detail="No token provided." ) # ... validate token ... if token_invalid: # This should also return HTTP 401, but returns 403 raise auth.exceptions.HTTPException( status_code=401, detail="Invalid token" ) return {"identity": "user-id"} ``` ### Error Message and Stack Trace (if applicable) Expected HTTP response: `401 Unauthorized` Actual HTTP response: `403 Forbidden` ### Description When using custom authentication with `@auth.authenticate`, any `Auth.exceptions.HTTPException` raised with `status_code=401` is converted to a `403 Forbidden` response. **Root Cause Analysis** (from `langgraph-api==0.5.3`): 1. In `langgraph_api/auth/custom.py`, the `CustomAuthBackend.authenticate()` method catches `Auth.exceptions.HTTPException`: ```python except Auth.exceptions.HTTPException as e: if e.status_code == 401 or e.status_code == 403: raise AuthenticationError(e.detail) from None # <-- status_code is LOST here ``` 2. Starlette's `AuthenticationError` doesn't have a `status_code` attribute - it only carries the message. 3. In `langgraph_api/auth/middleware.py`, the `on_error` handler always returns 403: ```python def on_error(conn: HTTPConnection, exc: AuthenticationError): return JSONResponse({"detail": str(exc)}, status_code=403) # <-- Always 403! ``` **Impact:** - Security implications: 401 (Unauthorized) and 403 (Forbidden) have different semantic meanings - 401 indicates missing/invalid credentials and typically prompts re-authentication - 403 indicates the user is authenticated but lacks permission - This breaks standard HTTP semantics and client-side error handling logic **Suggested Fix:** Option 1: Preserve status code through the error chain ```python # In custom.py class AuthenticationErrorWithStatus(AuthenticationError): def __init__(self, message: str, status_code: int = 401): super().__init__(message) self.status_code = status_code # When catching HTTPException raise AuthenticationErrorWithStatus(e.detail, e.status_code) from None # In middleware.py def on_error(conn: HTTPConnection, exc: AuthenticationError): status = getattr(exc, 'status_code', 403) return JSONResponse({"detail": str(exc)}, status_code=status) ``` Option 2: Re-raise HTTPException directly instead of converting to AuthenticationError ### System Info - langgraph-api version: 0.5.3 (also likely affects newer versions up to 0.5.32) - langgraph-sdk version: Any version with `Auth.exceptions.HTTPException` - Python version: 3.12 - OS: macOS / Linux
Author
Owner

@vichu3d-dev commented on GitHub (Feb 3, 2026):

Hi @nfcampos I have raised a PR request for this issue. Expecting your feedback and further proposal of any on the same.

@vichu3d-dev commented on GitHub (Feb 3, 2026): Hi @nfcampos I have raised a PR request for this issue. Expecting your feedback and further proposal of any on the same.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#1086