# Auth & Authorization Service Backend application built with **FastAPI + SQLAlchemy**, implementing a custom authentication and authorization system without using the framework's built-in auth mechanisms. ## Stack - **FastAPI** — web framework, routing, dependency injection - **SQLAlchemy (ORM)** — database access, `sessionmaker`, declarative models - **python-jose (jwt)** — creating and validating JWT tokens - **bcrypt / passlib** — password hashing - **Pydantic v2** — request/response validation schemas - **Alembic** — database migrations ## Authentication Authentication is built on **JWT tokens** (Bearer), without server-side sessions. 1. The client sends `POST /protected/token` with `email` (as `username`) and `password` in `OAuth2PasswordRequestForm` format (form-data). 2. The server looks up the user by email and verifies the password via bcrypt (`hash_check(plain, hashed)`). 3. If the email/password pair is correct — a JWT is issued with payload `{"sub": }`, signed with a secret key (`ENV.SECRET_STRING`, algorithm `ENV.ALGORITHM`). 4. The client passes the token in the `Authorization: Bearer ` header with every subsequent request. 5. On protected endpoints, the `get_current_user` dependency: - decodes the token (`jwt.decode`), - extracts `sub` (user id), - loads the user from the DB along with their permissions, - on any error (invalid/expired token, user not found from the token) — **401 Unauthorized**. > Logout in a pure JWT scheme requires either a short token TTL + a refresh > token, or a blacklist of revoked tokens (a `revoked_tokens` table). > Alternatively, the token can simply be cleared on the frontend at logout, as > the simplest option. ## Authorization Model Instead of the classic `roles / business_elements / access_roles_rules` scheme (with separate `read/create/update/delete` × `_all`), a simpler **flat permission model** was chosen, which is easier to demonstrate but covers the same principles: a user is granted a set of string permissions, and each endpoint declares which permissions are required to call it. ### Database Tables **users** | field | type | description | |-------------------|-------------|-----------------------------------------------------------------------------| | id | int, PK | user identifier | | name | str(64) | first name | | last_name | str(64) | last name | | middle_name | str(64) | middle name | | email | str(255) | email, unique | | status | bool | whether the account is active (used instead of `is_active` for soft delete) | | hashed_password | str(255) | bcrypt password hash | **permissions** | field | type | description | |-------------|-----------|--------------------------------------------------------------------------| | id | int, PK | permission identifier | | permission | str(64) | permission code: `admin`, `can_view`, `can_create`, `can_edit`, `can_delete` | **user_permission** (association table, many-to-many) | field | type | description | |------------------|------|---------------------------| | user_id | FK | → users.id | | permission_id | FK | → permissions.id | A single user can hold several permissions at once (e.g. `can_view` + `can_edit`); the `admin` permission grants full access and allows granting/revoking any permissions, including `admin`, to/from other users. ### Access Check Rules Every business-logic method (`CrudActions`) explicitly declares the set of permitted permissions and checks it before performing the action: | Action | Required permission | |------------------------------|----------------------------------| | View user (by email/id) | `can_view` or `admin` | | Create user | `can_create` or `admin` | | Update user | `can_edit` or `admin` | | Delete user | `can_delete` or `admin` | Additionally, a **privilege escalation protection** rule applies: a user without the `admin` permission cannot grant the `admin` permission to themselves or any other user when creating/editing an account. ### Access Error Handling - **401 Unauthorized** — if the request cannot be matched to a valid logged-in user (no token, expired/invalid token, user from the token does not exist). - **403 Forbidden** — the user is identified, but lacks the required permission for the requested action/resource (including privilege escalation attempts, attempts to claim someone else's email). - **404 Not Found** — the requested resource (user, permission) does not exist. ## API | Method | Path | Required permission | Description | |--------|--------------------------------------|--------------------------|----------------------------------------| | POST | `/protected/token` | — | Login, JWT issuance | | POST | `/protected/logout` | — | Login, JWT issuance | | GET | `/protected/get_user_by_email` | `can_view` / `admin` | Get user by email | | GET | `/protected/get_user_by_id` | `can_view` / `admin` | Get user by id | | POST | `/protected/create_user` | `can_create` / `admin` | Create user | | PATCH | `/protected/update_user` | `can_edit` / `admin` | Update own profile, or (for admin/can_edit) someone else's via `target_id` | | DELETE | `/protected/delete_user` | `can_delete` / `admin` | Delete (deactivate) user | ## Test Data For demonstrating the system, the DB must contain at least: Passwords d@d.d, d1@d.d, d2@d.d 12345678 - 1 user with the `admin` permission (full access) - 1 user with the `can_view` permission (view only) - 1 user with no permissions (for testing 403) ## Project Structure ``` src/ db/ # ActionsDB — DB access (SQLAlchemy sessions, queries) model/ database_model/ # SQLAlchemy models (User, Permissions) user/ # Pydantic schemas (UserOut, UserCreate, UserUpdate, PermissionIn/Out) env_read/ # Reading env via pydantic settings service/ crud_actions/ # CrudActions — business logic + access permission checks JWT/ # JWT (token encoding/decoding), Hash (bcrypt) auth/ # JWT token creation and verification errors/ # centralized HTTP errors (401/403/404) web/ # FastAPI routers (endpoints) migrations/ # DB migrations tests/ # tests (unit, integration) ``` ## Running ```bash python3 -m venv .venv source .venv/bin/activate pip install poetry poetry install make run ``` Rename env and fill with correct data