affaan-m/ECC · error · HTTPException

Not authorized

Error message

Not authorized

What it means

In `update_user`, before delegating to the service, the handler checks `current_user.id != user_id` and raises `HTTPException(403, "Not authorized")`. This is an ownership check: a user may only patch their own record. There is no admin override in this snippet.

Source

Thrown at skills/fastapi-patterns/SKILL.md:276

    db: DbDep,
    current_user: ActiveUserDep,
    skip: Annotated[int, Query(ge=0)] = 0,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
) -> UserListResponse:
    service = UserService(db)
    users, total = await service.list(skip=skip, limit=limit)
    return UserListResponse(total=total, items=users)


@router.patch("/{user_id}", response_model=UserResponse)
async def update_user(
    user_id: int,
    payload: UserUpdate,
    db: DbDep,
    current_user: ActiveUserDep,
) -> UserResponse:
    if current_user.id != user_id:
        raise HTTPException(status_code=403, detail="Not authorized")
    service = UserService(db)
    try:
        user = await service.update(user_id, payload)
    except DuplicateUserError:
        raise HTTPException(status_code=400, detail="Email already registered")
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return user


@router.post("/token")
async def login(
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
    db: DbDep,
) -> dict[str, str]:
    service = UserService(db)
    token = await service.authenticate(form_data.username, form_data.password)
    if token is None:

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Front-end: derive `{user_id}` from the JWT-decoded `current_user.id`, never from a route param the user can tamper with.
  2. If admins must edit others, add a role check: `if current_user.id != user_id and not current_user.is_admin:`.
  3. Return 403 silently — do not leak whether `user_id` exists.
  4. Consider `PATCH /me` as the primary self-edit route and restrict `/{user_id}` to admins.

Example fix

# before
if current_user.id != user_id:
    raise HTTPException(status_code=403, detail="Not authorized")

# after — allow admins, otherwise restrict to self
if current_user.id != user_id and not current_user.is_admin:
    raise HTTPException(status_code=403, detail="Not authorized")
Defensive patterns

Strategy: validation

Validate before calling

# derive the route from the JWT, never from user input
const me = await client.get('/users/me')
await client.patch(`/users/${me.id}`, payload)

Type guard

null

Try / catch

from fastapi import HTTPException
try:
    await client.patch(f'/users/{uid}', json=payload)
except HTTPException as e:
    if e.status_code == 403:
        show_error('You can only edit your own account.')
    raise

Prevention

When it happens

Trigger: Authenticated user calls `PATCH /users/{user_id}` with a `user_id` that is not their own (e.g. `/users/5` when their JWT subject is `12`). No admin role exists in this handler to bypass the check.

Common situations: Front-end constructs the URL from a stale user id (cached after re-login). Client tries to edit another user. Tests reuse one token for many user ids.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/34c37b51d0c566a9. Report an issue: GitHub.