[GH-ISSUE #23973] issue: three backend 500s that should be 200/200/403 (auths session, auths signout, users primary-admin guard) #35665

Closed
opened 2026-04-25 09:51:08 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @crbender on GitHub (Apr 22, 2026).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/23973

Summary

Static code review of backend/open_webui/routers/auths.py and
backend/open_webui/routers/users.py at tag v0.9.1 (commit 0a8a620fb)
surfaced three backend bugs where authenticated or authorized requests
return HTTP 500 instead of the correct response. No authentication or
authorization bypass; these are availability / error-correctness bugs.

Note on reproduction: these were found by reading the code paths,
not by running the app. Reproduction below is given as the exact inputs
and code paths that trigger each bug, so a maintainer can confirm with a
single curl or a focused test. This was not submitted as a PR to
respect the repo's "no AI-authored PRs without human review + manual
testing" rule; happy to turn it into a PR if a maintainer wants to
adopt the diff.

Affected version / environment

  • Open WebUI version: v0.9.1 (tag), commit 0a8a620fb
  • Branch reviewed: main
  • Install method: N/A — static review of the repo
  • OS / Browser / Ollama: N/A (server-side Python, no runtime required
    to observe the bugs)

Location

backend/open_webui/routers/auths.py, get_session_user

Expected behavior

get_current_user accepts a JWT from the Authorization header, the
token cookie, or request.state.token (set by middleware, e.g. for
x-api-key). Any of those should produce a valid session response.

Actual behavior

The handler unconditionally dereferences the result of
get_http_authorization_cred():

auth_header = request.headers.get('Authorization')
auth_token = get_http_authorization_cred(auth_header)
token = auth_token.credentials          # <-- AttributeError when None
data = decode_token(token)

get_http_authorization_cred returns None when the header is missing
or malformed (see
utils/auth.py, get_http_authorization_cred).
So any authenticated request that reached this endpoint via cookie (or
with a malformed Authorization header) raises AttributeError
HTTP 500 instead of the session payload.

Reproduction

  1. Run Open WebUI (:v0.9.1) against any backing store; sign in normally
    so you have the token cookie.
  2. In a fresh curl (no Authorization header), send the cookie only:
    curl -i http://localhost:8080/api/v1/auths/ \
      -H "Cookie: token=$YOUR_JWT_COOKIE"
    
  3. Expected: 200 with SessionUserInfoResponse.
  4. Actual: 500 (AttributeError: 'NoneType' object has no attribute 'credentials' in server logs).

A second reproduction variant: send a malformed header:

curl -i http://localhost:8080/api/v1/auths/ -H "Authorization: Bearer"

Same 500.

Suggested fix

Mirror the token-resolution order used by get_current_user (header → cookie → request.state.token) and only decode when a token is available:

token = None
auth_header = request.headers.get('Authorization')
if auth_header:
    auth_token = get_http_authorization_cred(auth_header)
    if auth_token is not None:
        token = auth_token.credentials
if token is None:
    token = request.cookies.get('token')
if token is None and getattr(request.state, 'token', None):
    token = request.state.token.credentials
data = decode_token(token) if token else None

Bug 2 — GET /api/v1/auths/signout returns 500 for malformed Authorization headers

Location

backend/open_webui/routers/auths.py, signout

Expected behavior

Signout should revoke the current token (if present), clear auth cookies,
and return success even when the request has no/invalid Authorization
header but a valid token cookie.

Actual behavior

token = None
auth_header = request.headers.get('Authorization')
if auth_header:
    auth_cred = get_http_authorization_cred(auth_header)
    token = auth_cred.credentials       # <-- AttributeError when None
else:
    token = request.cookies.get('token')

If the Authorization header is present but malformed, auth_cred is
None, the .credentials access raises, and the fallback to the cookie
never runs — so the user gets a 500 and their token is not revoked / the
cookies are not cleared.

Reproduction

  1. Sign in so the token cookie is set.
  2. Issue a signout with a malformed bearer header:
    curl -i http://localhost:8080/api/v1/auths/signout \
      -H "Cookie: token=$YOUR_JWT_COOKIE" \
      -H "Authorization: Bearer"
    
  3. Expected: 200 / JSON signout response; cookies cleared.
  4. Actual: 500; cookies still set on the client; the JWT jti is not
    added to the revocation set in Redis.

Suggested fix

Guard the None and always fall back to the cookie:

token = None
auth_header = request.headers.get('Authorization')
if auth_header:
    auth_cred = get_http_authorization_cred(auth_header)
    if auth_cred is not None:
        token = auth_cred.credentials
if token is None:
    token = request.cookies.get('token')

Bug 3 — Primary-admin protection returns 500 instead of 403

Location

Expected behavior

When an admin tries to edit/delete the primary admin, or when the primary
admin tries to change their own role away from admin, the server should
return 403 ACTION_PROHIBITED (matches the inner raise HTTPException
and the message text).

Actual behavior

The guard lives inside a try / except Exception block. Because
HTTPException is an Exception subclass, the intentional

raise HTTPException(
    status_code=status.HTTP_403_FORBIDDEN,
    detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)

is caught by the very next handler and rewritten as

raise HTTPException(
    status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    detail='Could not verify primary admin status.',
)

So the client sees a generic 500 for what is actually an authorization
decision, and the server logs contain a misleading
Error checking primary admin status: 403: ... line. Note signup
already uses the correct pattern (except HTTPException: raise before
the generic catch) — see
routers/auths.py, signup.

Reproduction (update)

  1. Seed two admin users A (primary, id returned by get_first_user)
    and B (second admin).
  2. Authenticate as B.
  3. Call:
    curl -i -X POST http://localhost:8080/api/v1/users/$A_ID/update \
      -H "Authorization: Bearer $B_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "x"}'
    
  4. Expected: 403 {"detail": "Action prohibited"}.
  5. Actual: 500 {"detail": "Could not verify primary admin status."}.

Same reproduction for the self-demotion path: as A, POST
/{A_ID}/update with {"role": "user"}.

Reproduction (delete)

  1. As B, call:
    curl -i -X DELETE http://localhost:8080/api/v1/users/$A_ID \
      -H "Authorization: Bearer $B_TOKEN"
    
  2. Expected: 403. Actual: 500.

Suggested fix

Re-raise HTTPException before the generic catch in both routes:

except HTTPException:
    raise
except Exception as e:
    log.error(f'Error checking primary admin status: {e}')
    raise HTTPException(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        detail='Could not verify primary admin status.',
    )

Proposed diff (if helpful)

A candidate diff exists on this branch:
https://github.com/crbender/open-webui/tree/fix/auth-session-signout-and-primary-admin-guard

It was originally submitted as PR #23972 but auto-closed by
pr-validator-bot (missing CLA section in the body — my mistake). I did
not reopen it because the fixes were drafted from a static review only
and the repo's PR policy asks for PRs to be human-reviewed and manually
tested; filing this as an issue instead so a maintainer can pick up
whichever fix they want in the way they prefer.

Logs / screenshots

Not applicable — these are code-path bugs; the repro commands above are
the logs. I can attach a stack trace from an exercising unit test if a
maintainer wants one.

  • No prior open/closed issues found for get_session_user / auth_token.credentials / Could not verify primary admin status.
  • See routers/auths.py::signup for the correct except HTTPException: raise pattern already used in this file.
Originally created by @crbender on GitHub (Apr 22, 2026). Original GitHub issue: https://github.com/open-webui/open-webui/issues/23973 ## Summary Static code review of `backend/open_webui/routers/auths.py` and `backend/open_webui/routers/users.py` at tag `v0.9.1` (commit `0a8a620fb`) surfaced three backend bugs where authenticated or authorized requests return HTTP 500 instead of the correct response. No authentication or authorization bypass; these are availability / error-correctness bugs. > **Note on reproduction:** these were found by reading the code paths, > not by running the app. Reproduction below is given as the exact inputs > and code paths that trigger each bug, so a maintainer can confirm with a > single curl or a focused test. This was **not** submitted as a PR to > respect the repo's "no AI-authored PRs without human review + manual > testing" rule; happy to turn it into a PR if a maintainer wants to > adopt the diff. ## Affected version / environment - **Open WebUI version:** `v0.9.1` (tag), commit `0a8a620fb` - **Branch reviewed:** `main` - **Install method:** N/A — static review of the repo - **OS / Browser / Ollama:** N/A (server-side Python, no runtime required to observe the bugs) --- ## Bug 1 — `GET /api/v1/auths/` returns 500 for cookie-only sessions or malformed `Authorization` headers ### Location [`backend/open_webui/routers/auths.py`, `get_session_user`](https://github.com/open-webui/open-webui/blob/v0.9.1/backend/open_webui/routers/auths.py#L168-L183) ### Expected behavior `get_current_user` accepts a JWT from the `Authorization` header, the `token` cookie, or `request.state.token` (set by middleware, e.g. for `x-api-key`). Any of those should produce a valid session response. ### Actual behavior The handler unconditionally dereferences the result of `get_http_authorization_cred()`: ```python auth_header = request.headers.get('Authorization') auth_token = get_http_authorization_cred(auth_header) token = auth_token.credentials # <-- AttributeError when None data = decode_token(token) ``` `get_http_authorization_cred` returns `None` when the header is missing or malformed (see [`utils/auth.py`, `get_http_authorization_cred`](https://github.com/open-webui/open-webui/blob/v0.9.1/backend/open_webui/utils/auth.py#L287-L294)). So any authenticated request that reached this endpoint via cookie (or with a malformed `Authorization` header) raises `AttributeError` → HTTP 500 instead of the session payload. ### Reproduction 1. Run Open WebUI (`:v0.9.1`) against any backing store; sign in normally so you have the `token` cookie. 2. In a fresh curl (no `Authorization` header), send the cookie only: ```bash curl -i http://localhost:8080/api/v1/auths/ \ -H "Cookie: token=$YOUR_JWT_COOKIE" ``` 3. Expected: 200 with `SessionUserInfoResponse`. 4. Actual: 500 (`AttributeError: 'NoneType' object has no attribute 'credentials'` in server logs). A second reproduction variant: send a malformed header: ```bash curl -i http://localhost:8080/api/v1/auths/ -H "Authorization: Bearer" ``` Same 500. ### Suggested fix Mirror the token-resolution order used by `get_current_user` (header → cookie → `request.state.token`) and only decode when a token is available: ```python token = None auth_header = request.headers.get('Authorization') if auth_header: auth_token = get_http_authorization_cred(auth_header) if auth_token is not None: token = auth_token.credentials if token is None: token = request.cookies.get('token') if token is None and getattr(request.state, 'token', None): token = request.state.token.credentials data = decode_token(token) if token else None ``` --- ## Bug 2 — `GET /api/v1/auths/signout` returns 500 for malformed `Authorization` headers ### Location [`backend/open_webui/routers/auths.py`, `signout`](https://github.com/open-webui/open-webui/blob/v0.9.1/backend/open_webui/routers/auths.py#L770-L782) ### Expected behavior Signout should revoke the current token (if present), clear auth cookies, and return success even when the request has no/invalid `Authorization` header but a valid `token` cookie. ### Actual behavior ```python token = None auth_header = request.headers.get('Authorization') if auth_header: auth_cred = get_http_authorization_cred(auth_header) token = auth_cred.credentials # <-- AttributeError when None else: token = request.cookies.get('token') ``` If the `Authorization` header is *present but malformed*, `auth_cred` is `None`, the `.credentials` access raises, and the fallback to the cookie never runs — so the user gets a 500 and their token is not revoked / the cookies are not cleared. ### Reproduction 1. Sign in so the `token` cookie is set. 2. Issue a signout with a malformed bearer header: ```bash curl -i http://localhost:8080/api/v1/auths/signout \ -H "Cookie: token=$YOUR_JWT_COOKIE" \ -H "Authorization: Bearer" ``` 3. Expected: 200 / JSON signout response; cookies cleared. 4. Actual: 500; cookies still set on the client; the JWT `jti` is not added to the revocation set in Redis. ### Suggested fix Guard the `None` and always fall back to the cookie: ```python token = None auth_header = request.headers.get('Authorization') if auth_header: auth_cred = get_http_authorization_cred(auth_header) if auth_cred is not None: token = auth_cred.credentials if token is None: token = request.cookies.get('token') ``` --- ## Bug 3 — Primary-admin protection returns 500 instead of 403 ### Location - [`backend/open_webui/routers/users.py`, `update_user_by_id`](https://github.com/open-webui/open-webui/blob/v0.9.1/backend/open_webui/routers/users.py#L534-L558) - [`backend/open_webui/routers/users.py`, `delete_user_by_id`](https://github.com/open-webui/open-webui/blob/v0.9.1/backend/open_webui/routers/users.py#L625-L640) ### Expected behavior When an admin tries to edit/delete the primary admin, or when the primary admin tries to change their own role away from `admin`, the server should return `403 ACTION_PROHIBITED` (matches the inner `raise HTTPException` and the message text). ### Actual behavior The guard lives inside a `try` / `except Exception` block. Because `HTTPException` is an `Exception` subclass, the intentional ```python raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACTION_PROHIBITED, ) ``` is caught by the very next handler and rewritten as ```python raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Could not verify primary admin status.', ) ``` So the client sees a generic 500 for what is actually an authorization decision, and the server logs contain a misleading `Error checking primary admin status: 403: ...` line. Note `signup` already uses the correct pattern (`except HTTPException: raise` before the generic catch) — see [`routers/auths.py`, `signup`](https://github.com/open-webui/open-webui/blob/v0.9.1/backend/open_webui/routers/auths.py#L751-L756). ### Reproduction (update) 1. Seed two admin users `A` (primary, id returned by `get_first_user`) and `B` (second admin). 2. Authenticate as `B`. 3. Call: ```bash curl -i -X POST http://localhost:8080/api/v1/users/$A_ID/update \ -H "Authorization: Bearer $B_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "x"}' ``` 4. Expected: `403 {"detail": "Action prohibited"}`. 5. Actual: `500 {"detail": "Could not verify primary admin status."}`. Same reproduction for the self-demotion path: as `A`, POST `/{A_ID}/update` with `{"role": "user"}`. ### Reproduction (delete) 1. As `B`, call: ```bash curl -i -X DELETE http://localhost:8080/api/v1/users/$A_ID \ -H "Authorization: Bearer $B_TOKEN" ``` 2. Expected: 403. Actual: 500. ### Suggested fix Re-raise `HTTPException` before the generic catch in both routes: ```python except HTTPException: raise except Exception as e: log.error(f'Error checking primary admin status: {e}') raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Could not verify primary admin status.', ) ``` --- ## Proposed diff (if helpful) A candidate diff exists on this branch: https://github.com/crbender/open-webui/tree/fix/auth-session-signout-and-primary-admin-guard It was originally submitted as PR #23972 but auto-closed by `pr-validator-bot` (missing CLA section in the body — my mistake). I did not reopen it because the fixes were drafted from a static review only and the repo's PR policy asks for PRs to be human-reviewed and manually tested; filing this as an issue instead so a maintainer can pick up whichever fix they want in the way they prefer. ## Logs / screenshots Not applicable — these are code-path bugs; the repro commands above are the logs. I can attach a stack trace from an exercising unit test if a maintainer wants one. ## Related - No prior open/closed issues found for `get_session_user` / `auth_token.credentials` / `Could not verify primary admin status`. - See `routers/auths.py::signup` for the correct `except HTTPException: raise` pattern already used in this file.
Author
Owner

@tjbck commented on GitHub (Apr 24, 2026):

Addressed in dev.

<!-- gh-comment-id:4311333265 --> @tjbck commented on GitHub (Apr 24, 2026): Addressed in dev.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#35665