{"record":{"id":"0e5e56ac14b68581","repo":"tiangolo/fastapi","slug":"not-enough-permissions","errorCode":null,"errorMessage":"Not enough permissions","messagePattern":"Not enough permissions","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"docs_src/security/tutorial005_an_py310.py","lineNumber":135,"sourceCode":"        detail=\"Could not validate credentials\",\n        headers={\"WWW-Authenticate\": authenticate_value},\n    )\n    try:\n        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n        username = payload.get(\"sub\")\n        if username is None:\n            raise credentials_exception\n        scope: str = payload.get(\"scope\", \"\")\n        token_scopes = scope.split(\" \")\n        token_data = TokenData(scopes=token_scopes, username=username)\n    except (InvalidTokenError, ValidationError):\n        raise credentials_exception\n    user = get_user(fake_users_db, username=token_data.username)\n    if user is None:\n        raise credentials_exception\n    for scope in security_scopes.scopes:\n        if scope not in token_data.scopes:\n            raise HTTPException(\n                status_code=status.HTTP_401_UNAUTHORIZED,\n                detail=\"Not enough permissions\",\n                headers={\"WWW-Authenticate\": authenticate_value},\n            )\n    return user\n\n\nasync def get_current_active_user(\n    current_user: Annotated[User, Security(get_current_user, scopes=[\"me\"])],\n):\n    if current_user.disabled:\n        raise HTTPException(status_code=400, detail=\"Inactive user\")\n    return current_user\n\n\n@app.post(\"/token\")\nasync def login_for_access_token(\n    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/tiangolo/fastapi/blob/3e8d1526d83a90aaf7d6eb6dc682bf150f180b25/docs_src/security/tutorial005_an_py310.py#L117-L153","documentation":"The OAuth2 scope-enforcement gate in tutorial005. get_current_user receives SecurityScopes and, after decoding the JWT, iterates security_scopes.scopes: if any required scope is absent from the token's space-delimited 'scope' claim, it raises HTTP 401 with WWW-Authenticate: Bearer scope=\"<required>\" so the client knows which scope is missing. Required scopes come from Security(get_current_user, scopes=[...]) on each route (e.g. read_own_items requires 'items').","triggerScenarios":"Call GET /users/me/items/ (requires scope 'items') holding a token whose 'scope' claim is only 'me' — because at POST /token the form's scope field requested only 'me'. Any route guarded by Security(..., scopes=['X']) called without X in the token trips line 135.","commonSituations":"Client requested the wrong scopes at token issuance; scope naming mismatch ('read:items' vs 'items'); token minted before scopes were introduced; the scope field sent as JSON instead of a form field on /token.","solutions":["At POST /token, request the scope the route needs: send form field scope=items (or 'me items' for both).","Re-login to mint a fresh JWT that contains the required scope.","Verify the scope string is space-delimited and the names exactly match the scopes declared on OAuth2PasswordBearer (here 'me' and 'items')."],"exampleFix":"// before\ncurl -X POST /token -d 'username=johndoe&password=...&scope=me'\ncurl /users/me/items/   # 401 Not enough permissions\n\n// after\ncurl -X POST /token -d 'username=johndoe&password=...&scope=me items'\ncurl /users/me/items/   # 200","handlingStrategy":"validation","validationCode":"# Decode the JWT locally and confirm the required scope before calling\nimport jwt\nREQUIRED = {\"items\"}\ndef has_required_scopes(token: str, required: set[str]) -> bool:\n    payload = jwt.decode(token, SECRET_KEY, algorithms=[\"HS256\"])\n    token_scopes = set(payload.get(\"scope\", \"\").split())\n    return required.issubset(token_scopes)","typeGuard":"from typing import TypeGuard\ndef token_has_scopes(token: str, need: set[str]) -> TypeGuard[str]:\n    import jwt\n    p = jwt.decode(token, SECRET_KEY, algorithms=[\"HS256\"])\n    return need.issubset(set(p.get(\"scope\", \"\").split()))","tryCatchPattern":"import httpx\ntry:\n    r = httpx.get(\"/users/me/items/\", headers={\"Authorization\": f\"Bearer {token}\"})\n    r.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 401:\n        www = e.response.headers.get(\"www-authenticate\", \"\")\n        # parse scope=\"...\" and re-auth requesting those scopes\n        need = parse_required_scopes(www)\n        token = login_with_scopes(need)","preventionTips":["Request every scope you might need upfront at POST /token via the 'scope' form field.","Use space-delimited scope strings matching the names declared on OAuth2PasswordBearer.","Decode the JWT locally before calls to fail fast on missing scopes."],"tags":["fastapi","authentication","oauth2","scopes","jwt","authorization"],"backgroundTag":null,"analyzedSha":"3e8d1526d83a90aaf7d6eb6dc682bf150f180b25","analyzedAt":"2026-08-11T02:34:52.986Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}