KAHIBARO
Discord Login Register

Authentication and Authorization

Overview

In your final project you already know how to build REST APIs and how authentication and authorization work in theory. In this chapter you will apply those ideas to a real, production-style backend.

You will design and implement a realistic authentication and authorization system that can safely run in production, integrate with PostgreSQL and Redis, and support your application’s needs as it grows.

The goal here is not to re-explain basic concepts like “what is a JWT” or “what is RBAC,” but to show how to wire everything together in a complete project, with production-ready decisions and trade-offs.


Requirements for Auth in a Production Project

Before choosing tools or writing code, define what your application actually needs.

Typical requirements for the final project backend:

List these needs explicitly in your project docs. It will guide your design and help you test the right things.


Designing Your Auth and AuthZ Architecture

Tokens vs Sessions

For this final project, a token-based architecture fits well with a REST API and modern frontends:

A common design:

Important rule:
Never store plaintext passwords, API keys, or other secrets inside tokens.

Where tokens live

Practical options for your project:

LocationProsConsRecommended for project
Local storageSimple for SPAs, no cookies neededVulnerable to XSS script accessAvoid for sensitive apps
In-memory (JS state)Not persisted across refresh, safer than LSRequires refresh token in cookie or storageUse for access token
HTTP-only cookieProtected from JS, convenient for browserNeeds CSRF protection if used as authGood for refresh tokens
Mobile secure storageVery good for native appsPlatform-specificOut of scope here

For a backend-only course project, you can:

Document this choice in your project README.


Data Model for Users, Roles and Permissions

User table

At minimum your users table needs:

ColumnTypeDescription
idUUID / bigserialPrimary key
emailtext, uniqueLogin identifier, indexed
password_hashtextHashed password (bcrypt, argon2, etc.)
is_activebooleanSoft-activation flag, for bans or deactivation
is_superuserbooleanFor full admin permissions
created_attimestamptzCreation time
updated_attimestamptzLast update
email_verifiedbooleanWhether email is verified
last_login_attimestamptz, nullLast successful login

Keep password_hash separate from any other user data. Never mix plain password with hash in code.

Roles and permissions

For a real production system you might have:

For this project you can choose between:

  1. Simple roles in the user table
    • Columns: is_superuser, maybe role enum: user, staff, admin.
    • Fast to implement, fine for small projects.
  2. Full RBAC tables
    • More flexible but more work and more queries.

For a beginner-friendly final project, a hybrid that uses:

is enough to demonstrate authorization patterns without lots of extra complexity.


Password Storage and Authentication Flow

Password hashing

Use a strong, adaptive password hashing algorithm like bcrypt or argon2id.

Implementation rules:

  • Never store passwords in plaintext.
  • Never use plain SHA-256 or MD5 for passwords.
  • Always use a library-designed password hashing function.

With Python you can use:

Pseudocode for hashing:

python
hashed = hash_password(plain_password)
user.password_hash = hashed

Pseudocode for verification:

python
if not verify_password(candidate_password, user.password_hash):
    raise InvalidCredentialsError()

Never log the plain password. You can log that a login failed, but not the credentials.

Login flow

The typical login endpoint:

  1. Receive email and password.
  2. Find user by email (case insensitive).
  3. If user not found or is_active is false, respond with a generic error.
  4. Verify password.
  5. If password correct:
    • Generate access and refresh tokens.
    • Optionally, store refresh token info (user id, device, expires at) in DB or Redis.
    • Update last_login_at.
  6. Return tokens to the client.

Return the same error message for "user not found" and "wrong password" to avoid revealing which emails exist.

Example response body:

json
{
  "access_token": "<jwt-access>",
  "refresh_token": "<opaque-or-jwt-refresh>",
  "token_type": "bearer",
  "expires_in": 900
}

If you use HTTP-only cookies for refresh tokens, the refresh_token might not appear in the JSON.


Access and Refresh Tokens in Your Project

Designing JWT payloads

Your JWT payload should include only what you need to check authorization quickly.

A minimal example:

json
{
  "sub": "user_id_here",
  "exp": 1700000000,
  "iat": 1699996400,
  "type": "access",
  "is_superuser": false,
  "role": "user"
}

Key fields:

Important rule:
Validate exp, iat, and type before trusting any other claim in the token.

Signing and verifying tokens

Use a strong secret key for HMAC signing (HS256) and store it in an environment variable, not in code.

For example:

Use environment variables and your configuration system, not hardcoded literals.

Token creation pseudocode:

python
def create_access_token(user_id: str, is_superuser: bool, role: str) -> str:
    now = datetime.utcnow()
    payload = {
        "sub": user_id,
        "type": "access",
        "is_superuser": is_superuser,
        "role": role,
        "iat": int(now.timestamp()),
        "exp": int((now + ACCESS_TOKEN_LIFETIME).timestamp())
    }
    return encode_jwt(payload, secret=JWT_SECRET_KEY, algorithm="HS256")

Always check:

Refresh tokens and rotation

Refresh tokens are more sensitive, because they live longer.

To make them safer:

If a refresh token leaks, you want to be able to revoke it quickly.

You can store refresh tokens in:

Example Redis key pattern:

Integrating Auth with PostgreSQL and Redis

Storing data in PostgreSQL

Use PostgreSQL for:

Example PostgreSQL tables:

Design email_verification_tokens like:

ColumnTypeDescription
idUUID / bigserialPrimary key
user_idFK to users.idWhich user
tokentext, uniqueRandom token or signed token id
expires_attimestamptzExpiration time
used_attimestamptzWhen it was used, null if unused

You can also make token a signed value that contains only the user id and expiry, verified using your secret key. In that case you do not need a separate table.

Storing ephemeral data in Redis

Use Redis for short-lived or frequent operations:

Examples:

Your FastAPI dependency that validates tokens can check Redis for jwt:blacklist:<jti>. If it exists, treat the token as invalid.


Implementing Authentication in the Final Project

Endpoints to implement

At minimum, your final project should include these auth endpoints:

  1. POST /auth/register
    • Input: email, password, maybe full_name.
    • Behavior:
      • Create user.
      • Hash password.
      • Set email_verified to false.
      • Send verification email (in background).
    • Response: success message or created user data (never password hash).
  2. POST /auth/login
    • Input: email, password.
    • Behavior:
      • Check credentials.
      • Create access and refresh tokens.
      • Maybe set refresh token as HTTP-only cookie.
    • Response: token pair.
  3. POST /auth/refresh
    • Input: refresh token (body or cookie).
    • Behavior:
      • Validate refresh token and its status.
      • Optionally rotate refresh token.
      • Issue new access token.
    • Response: new tokens.
  4. POST /auth/logout
    • Input: refresh token.
    • Behavior:
      • Invalidate refresh token in DB or Redis.
    • Response: success message.
  5. POST /auth/verify-email
    • Input: verification token (usually passed in URL query).
    • Behavior:
      • Validate token.
      • Mark user email as verified.
    • Response: success message.
  6. POST /auth/request-password-reset
    • Input: email.
    • Behavior:
      • If user exists, send reset email with one-time token.
    • Response: generic success message (same for existing or non-existing email).
  7. POST /auth/reset-password
    • Input: token, new_password.
    • Behavior:
      • Validate token.
      • Update password hash.
    • Response: success message.

These endpoints should be covered by tests in your final project.

FastAPI dependencies for auth

Centralize token decoding and user loading into reusable dependencies.

Common pattern:

Pseudocode:

python
async def get_current_user(token: str = Depends(oauth2_scheme),
                           session: AsyncSession = Depends(get_db)):
    payload = decode_jwt(token)
    user_id = payload["sub"]
    user = await get_user_by_id(session, user_id)
    if not user or not user.is_active:
        raise HTTPException(status_code=401, detail="Inactive or invalid user")
    return user

Use these dependencies in your routers to keep the code DRY.


Authorization Rules in a Real Project

Protecting endpoints

For each API endpoint in the final project, you need to decide:

Examples:

Implementing role checks

Use separate dependencies for role-based checks, for example:

Pseudocode:

python
async def get_current_admin(user: User = Depends(get_current_user)):
    if not user.is_superuser:
        raise HTTPException(status_code=403, detail="Not enough permissions")
    return user

Then annotate admin-only endpoints:

python
@router.get("/admin/users")
async def list_users(admin: User = Depends(get_current_admin)):
    ...

This keeps authorization logic at the edges of your API, not inside business logic.

Resource ownership

For operations on resources like tasks, orders, or files, you often need to ensure the user is the owner.

Pattern:

  1. Load resource from database by its id.
  2. Check if resource.owner_id == current_user.id.
  3. If not, return 403 Forbidden.

Example:

python
async def get_task_or_404(task_id: int, session: AsyncSession):
    task = await session.get(Task, task_id)
    if task is None:
        raise HTTPException(status_code=404, detail="Task not found")
    return task
@router.put("/tasks/{task_id}")
async def update_task(
    task_id: int,
    data: TaskUpdate,
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_db)
):
    task = await get_task_or_404(task_id, session)
    if task.owner_id != user.id and not user.is_superuser:
        raise HTTPException(status_code=403, detail="Not enough permissions")
    ...

Avoid exposing details about whether the resource exists for unauthorized users. You may choose between returning 404 or 403 based on your threat model, but be consistent.


Protecting Auth in a Production Environment

Rate limiting login and reset endpoints

Authentication endpoints are frequent targets for abuse, such as brute force attacks.

Use Redis-based rate limiting:

Example rule:

Allow at most 5 failed login attempts per IP per 15 minutes.

Implementation idea:

  1. When a login fails:
    • Increment auth:login:fail:<ip> in Redis.
    • Set TTL to 15 minutes if key was new.
  2. Before processing a login:
    • Check that counter is below your threshold.
    • If above, return 429 Too Many Requests.

You can use similar rules for:

Protect tokens in transit

Configure your production environment as follows:

When behind a reverse proxy like Nginx or Traefik:

Token invalidation and logout

In a purely stateless JWT setup, logout is not straightforward. For a production system you should support some level of invalidation.

Options:

  1. Short-lived access tokens, long-lived refresh tokens
    • Access tokens expire soon.
    • Logout simply deletes or revokes the refresh token on the server.
  2. Blacklist (denylist) JWT IDs in Redis
    • Each JWT has a unique jti claim.
    • On logout, add jti to Redis with TTL equal to token’s remaining life.
    • Check against Redis in the auth dependency.

Even if you choose a simpler approach for your project, document your token invalidation strategy clearly.


Email Verification and Password Reset

Email verification flow

In the final project, your verification flow could be:

  1. POST /auth/register creates user with email_verified = false.
  2. It creates a verification token:
    • Random string or signed JWT with user_id and expiry.
    • TTL, for example 24 hours.
  3. Send email with verification link:
    • URL: https://frontend-app/verify-email?token=<token>.
  4. Frontend calls POST /auth/verify-email with that token.
  5. Backend:
    • Verifies token.
    • Ensures not used or expired.
    • Sets email_verified = true.

Do not leak whether a token is valid or not in a way that helps an attacker enumerate users. Generic error messages are usually safer.

Password reset flow

Very similar idea:

  1. POST /auth/request-password-reset:
    • If user exists:
      • Generate one-time reset token.
      • Store it with TTL, for example 1 hour.
      • Send email with reset link.
    • Always return 200 OK with generic message.
  2. POST /auth/reset-password:
    • Input: token, new_password.
    • Validate token.
    • Update password hash.
    • Invalidate that token so it cannot be reused.

Tokens for reset should be:

Testing Authentication and Authorization

For the final project you should have automated tests that cover the critical paths.

What to test

Suggested test list:

Using fixtures and helpers

Create helper functions for tests:

This makes your tests easier to read and maintain.


Putting It All Together

In your final project repository, you should end up with:

Keep the design clear and documented. The goal is not only to make your app secure, but also maintainable and understandable for someone reading your code or reviewing your project as part of a hiring process.

Views: 5

Comments

Please login to add a comment.

Don't have an account? Register now!