Authdog
Log In

Python · Access control

Basic access control

A signed-in user is not an allowed user.

After authentication, check the role or permission on the user object and return 403 when it is missing. Authdog resolved who they are. This guide is the FastAPI layer, on top of authdog.require_auth, that decides what they may do.

Where roles and permissions come from

require_auth returns the user from userinfo. Roles and permissions travel on that object. Print one real user from your environment before you trust the keys below:

{
  "id": "usr_123",
  "email": "[email protected]",
  "roles": ["admin"],
  "permissions": ["posts:read", "posts:write"]
}

Claim names depend on the environment. If your sample user has different keys, change the accessors. A check against roles does nothing when the claim is named something else, and every request will 403.

A `require_role` dependency

The factory takes the roles you accept and returns a dependency. It depends on require_auth first, so a missing session is 401 before this code runs. Call /admin as a signed-in user who lacks admin. You want 403, not the dashboard.

import os
from typing import Any
from fastapi import Depends, HTTPException, status
from authdog.fastapi import Authdog

authdog = Authdog(public_key=os.environ["PK_AUTHDOG"])

def require_role(*roles: str):
    async def _dependency(user: Any = Depends(authdog.require_auth)) -> Any:
        if not set(user.get("roles", [])).intersection(roles):
            raise HTTPException(status.HTTP_403_FORBIDDEN, "Insufficient role")
        return user
    return _dependency
@app.get("/admin", dependencies=[Depends(require_role("admin"))])
async def admin_dashboard():
    return {"ok": True}

Permission-based checks

Same factory, checking a permission instead of a role. A signed-in user missing posts:write gets 403. They do not get a created post.

def require_permission(*required: str):
    async def _dependency(user: Any = Depends(authdog.require_auth)) -> Any:
        missing = set(required) - set(user.get("permissions", []))
        if missing:
            raise HTTPException(status.HTTP_403_FORBIDDEN, f"Missing: {', '.join(sorted(missing))}")
        return user
    return _dependency

@app.post("/posts", dependencies=[Depends(require_permission("posts:write"))])
async def create_post():
    ...

Resource-level (ownership) checks

Role and permission checks can run before the handler, because they only need the user. Ownership needs the row. Load it inside the handler, return 404 when it is missing, and 403 when the caller is neither the author nor an admin.

@app.patch("/posts/{post_id}")
async def update_post(post_id: str, user=Depends(authdog.require_auth)):
    post = await db.get_post(post_id)
    if post is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND)
    if not (post.author_id == user.get("id") or "admin" in user.get("roles", [])):
        raise HTTPException(status.HTTP_403_FORBIDDEN, "Not your post")
    return await db.update_post(post_id, ...)

Choosing status codes

401 means no valid session. require_auth raises it for you. 403 means the session is valid and the action is not allowed. Your role and permission checks raise that. Keep them distinct so the frontend can send a 401 to sign-in and show access denied on a 403.

Next steps