KAHIBARO
Discord Login Register

14.7 Authorization Best Practices

Principle Based Overview

Authorization decides who can do what in your system. Good authorization design is about clear rules, least privilege, and predictable behavior under both normal use and attack.

You will meet specific mechanisms like roles, permissions, and ownership in other chapters. Here we focus on how to apply them safely and consistently.

Core authorization rule

Never trust the client. Always enforce authorization checks on the server for every protected action or resource.

We will walk through patterns, mistakes to avoid, and practical examples you can use in any backend stack.

Separate Authentication and Authorization

Authentication answers “Who are you?”. Authorization answers “What are you allowed to do?”. Keep them separate in both code and architecture.

Why separation matters

If you mix them, you tend to:

Instead:

Example identity context

For a request you might build a context object:

python
class AuthContext:
    def __init__(self, user_id: int, roles: list[str], permissions: list[str], is_admin: bool):
        self.user_id = user_id
        self.roles = roles
        self.permissions = permissions
        self.is_admin = is_admin

Every protected endpoint:

  1. Authenticates the user.
  2. Builds AuthContext.
  3. Passes it into authorization checks.

Do not put database queries or business logic inside your JWT or session creation step. Keep that for authorization layers.

Enforce Server Side Checks

Clients are untrusted. They can:

So your server must never rely on:

Example of bad vs good

Bad approach:

Good approach:

Rule

Protect every sensitive endpoint with a server side authorization check, even if the UI never shows it to normal users.

Use Least Privilege

Least privilege means: give users only the permissions they really need, and no more.

Practical least privilege guidelines

Example permission sets

RolePermissions example
usertask.read_own, task.create, task.update_own
managerAll user + task.read_team, task.update_team
adminuser.read_all, user.disable, task.read_all

Do not assign admin just because someone “might” need it in future. Promote only when there is a real requirement.

Prefer Centralized Authorization Logic

If you put checks everywhere, in random helper functions and controllers, it quickly becomes impossible to reason about who can do what.

Instead, centralize your authorization logic.

Where to centralize

You can centralize via:

Example: policy functions

python
from enum import Enum, auto
class Action(Enum):
    READ = auto()
    UPDATE = auto()
    DELETE = auto()
def can_access_task(user, task, action: Action) -> bool:
    if user.is_admin:
        return True
    if action == Action.READ:
        return task.owner_id == user.id or task.assignee_id == user.id
    if action in (Action.UPDATE, Action.DELETE):
        return task.owner_id == user.id
    return False

Then in your route:

python
if not can_access_task(auth_context.user, task, Action.UPDATE):
    raise HTTPException(status_code=403)

This keeps all task related authorization rules in one place and much easier to review and test.

Prefer “Deny by Default”

The safest default is no access unless explicitly allowed.

Design for explicit allow

Example pattern

python
def require_permission(user, permission: str):
    if permission not in user.permissions:
        raise HTTPException(status_code=403, detail="Forbidden")

Use this in your endpoints:

python
@app.post("/admin/create-user")
def create_user(auth=Depends(get_auth_context)):
    require_permission(auth.user, "user.create")
    ...

If you forget to call require_permission, that is a bug, but you design tests and patterns so this is obvious.

Rule

Fail closed, not open.
If something goes wrong in authorization, the request should be rejected, not granted.

Validate Ownership and Resource Level Access

Checking that someone has the right role is not enough. For many operations, you must also check that the resource actually belongs to them or to their scope.

Example: user reading a task

We have a user role that can read only their own tasks.

Bad version:

python
# /tasks/{task_id}
def get_task(task_id, auth=Depends(get_auth_context)):
    task = db.get_task(task_id)
    if "task.read" not in auth.permissions:
        raise HTTPException(403)
    return task

This allows any user with task.read permission to read any task.

Better version with ownership:

python
def get_task(task_id, auth=Depends(get_auth_context)):
    task = db.get_task(task_id)
    if auth.is_admin:
        return task
    if task.owner_id != auth.user_id:
        raise HTTPException(status_code=404)  # or 403
    return task

Now users can see only their own tasks, and admins can see all.

Using 403 vs 404

For security, many systems choose to return 404 for unauthorized resource access to avoid information leaks.

Use Consistent Rules for Collections and Single Resources

A common subtle bug:

Example:

python
# List tasks: correct filtering
GET /tasks
-> returns only tasks where owner_id = current_user.id
# Single task: missing ownership check
GET /tasks/{id}
-> returns the task without checking owner

This makes enumeration attacks easier. Consistency is key.

Good pattern

For collections and single resource endpoints:

Example

python
def list_tasks(auth):
    return db.list_tasks_visible_to_user(auth.user_id)
def get_task(task_id, auth):
    task = db.get_task(task_id)
    if not db.is_task_visible_to_user(task_id, auth.user_id):
        raise HTTPException(404)
    return task

Avoid Client Controlled Authorization Data

Never base access on anything that comes from the client if you can derive it from the server.

Bad ideas:

Server must determine authorization data

Instead:

Example: create task

Bad:

python
def create_task(payload, auth):
    # client sends owner_id they want
    task = Task(
        title=payload["title"],
        owner_id=payload["owner_id"],
    )
    db.save(task)

Any user could create tasks on behalf of others.

Good:

python
def create_task(payload, auth):
    task = Task(
        title=payload["title"],
        owner_id=auth.user_id,  # server decides
    )
    db.save(task)

Be Careful With “Admin” and “Superuser”

Admin accounts are powerful and attractive targets.

Best practices for admin privileges

Example: separating admin endpoints

Use a clear URL prefix and separate authorization checks:

python
@app.get("/admin/users")
def list_users(auth=Depends(get_auth_context)):
    require_permission(auth.user, "admin.view_users")

Do not give normal users access to admin endpoints even if they have similar permissions.

Design Clear Roles and Permissions

You will see role based and permission based models in their own chapters. Here, focus on how to design them cleanly.

Keep roles and permissions understandable

Good patterns:

Example mapping

PermissionDescription
task.read_ownRead tasks where the user is owner
task.read_allRead all tasks
task.createCreate new tasks
task.update_ownUpdate own tasks
task.update_allUpdate any task

Roles:

RolePermissions
usertask.read_own, task.create, task.update_own
adminall above + task.read_all, task.update_all

Clear naming helps review, audits, and tests.

Protect Sensitive Operations More Strictly

Some actions are more dangerous than others. These should have extra checks beyond normal authorization.

Examples:

Extra protection ideas

Example: changing password

Even if user has user.update_profile permission, you might still require a password confirmation:

python
def change_password(old_password, new_password, auth):
    if not verify_password(old_password, auth.user.hashed_password):
        raise HTTPException(403, "Invalid password")
    # continue with password change

Avoid Security Through Obscurity

Hiding endpoints or identifiers does not replace proper authorization.

Weak approaches:

UUIDs and secret URLs can reduce random guessing, but:

Rule

Obscurity is not authorization. Always enforce checks regardless of how hard the resource is to guess.

Use Proper HTTP Status Codes

Consistent responses help both clients and security reviewers.

Basic guidelines:

Example table

SituationStatus
No token or invalid token401
Valid token but missing permission403
Resource does not exist at all404
Resource exists but user should not know404 or 403 (depending on policy)

Return helpful but not over specific error messages, for example:

json
{
  "detail": "Forbidden"
}

Do not leak internal role names or permission systems in error messages.

Avoid Over Granting on Login or Registration

Be careful when users first join your system.

Common mistakes:

Better:

Example: safe default

python
def register_user(email, password):
    user = User(
        email=email,
        hashed_password=hash_password(password),
        role="user"  # minimal permissions
    )
    db.save(user)

Be Mindful With “Remember Me” and Long Lived Tokens

Long lived sessions and tokens increase the window in which stolen credentials can be used.

Authorization best practice here means:

Example policy

Rate Limit and Monitor Sensitive Authorization Paths

Even with correct authorization, attackers may attempt:

Best practice:

Example logs to keep

FieldExample
user_id123
endpointPOST /admin/change-role
outcomedenied or granted
reasonmissing permission: role.update
correlation_idrequest id for tracing
timestamp2026-08-27T12:34:56Z

Keep Authorization Data in Sync With Business Rules

Business rules change over time:

Authorization rules must be updated together with business logic.

Good practice

When implementing a new feature:

  1. Define what roles or users can do with it.
  2. Add corresponding permissions or policies.
  3. Add tests that enforce those rules.
  4. Update documentation for admins and developers.

Example: new “archive task” feature

Decide:

Then encode that in a policy function:

python
def can_archive_task(user, task) -> bool:
    if user.is_admin:
        return True
    return task.owner_id == user.id

And test it explicitly.

Test Authorization Thoroughly

Authorization bugs are easy to miss and very dangerous. You should test them systematically.

Types of tests

Example test matrix

For endpoint: DELETE /tasks/{id}

User typeOwn taskOthers taskExpected result
useryesno204 / success
usernoyes404 or 403
adminyesyes204 / success
anonymousanyany401

Automate these tests and run them in CI to prevent regressions.

Rule

Every critical authorization rule should have at least one automated test case.

Document Your Authorization Model

Future you and other developers need to understand how your system decides access.

Good documentation includes:

Example documentation snippet

Keep documentation near your code or in your API docs. When rules change, update both code and docs together.

Summary

Authorization best practices are about:

Once you internalize these principles, you can combine any specific model, role based, permission based, or attribute based, and still keep your backend safe and maintainable.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!