Skip to content

Permissions (RBAC)

Two independent systems: roles (hierarchical) and scopes (flat strings). Both are embedded in the JWT at issue time — checking them is a pure in-memory operation, no extra database call.

Roles

A fixed hierarchy, low to high:

guest → user → moderator → admin → superadmin
from authwarden.permissions.roles import has_role, has_min_role, require_roles, require_min_role

require_roles(payload, "admin")                              # exactly "admin" present
require_roles(payload, "admin", "moderator")                  # any one of these
require_roles(payload, "admin", "verified", require_all=True) # all required
require_min_role(payload, "moderator")                        # "moderator" or higher in the hierarchy

Through the facade, in a route:

@app.delete("/admin/users/{user_id}")
async def delete_user(user_id: str, _=Depends(warden.require_roles("admin"))):
    ...

Scopes

Plain strings — no format enforced. "user:read", "admin:delete", "posts:write" are all just strings the library checks for membership in the token's scope list.

from authwarden.permissions.policies import has_scope, require_scopes

require_scopes(payload, "posts:write")
require_scopes(payload, "posts:read", "posts:write")                  # any one
require_scopes(payload, "posts:read", "posts:write", require_all=True) # both
@app.post("/posts")
async def create_post(_=Depends(warden.require_scopes("posts:write"))):
    ...

Setting roles and scopes on a user

There's no dedicated endpoint for this — it's a direct field on UserInDB (roles: list[str], scopes: list[str]), set however your application logic decides (an admin panel, a database migration, a Stripe webhook on subscription purchase, etc.). Whatever's on the user record at the time they log in gets embedded into their JWT.

user.roles = ["admin"]
user.scopes = ["posts:write", "posts:delete"]
await store.update(user)

Why no database re-fetch on every permission check

Roles/scopes come from the JWT payload, not a fresh lookup — this is the standard JWT tradeoff. If you revoke someone's admin role, it takes effect on their next token refresh, not instantly. With the default 15-minute access token TTL, that's a small window. If you need instant revocation for some specific action, fetch the user via warden.current_user instead and check user.roles directly in your route body.

Errors

Status Exception When
403 ForbiddenError Role or scope check failed. Message lists what was required vs. what the token actually has.