7.4 KiB
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.
- The client sends
POST /protected/tokenwithemail(asusername) andpasswordinOAuth2PasswordRequestFormformat (form-data). - The server looks up the user by email and verifies the password via bcrypt
(
hash_check(plain, hashed)). - If the email/password pair is correct — a JWT is issued with payload
{"sub": <user.id>}, signed with a secret key (ENV.SECRET_STRING, algorithmENV.ALGORITHM). - The client passes the token in the
Authorization: Bearer <token>header with every subsequent request. - On protected endpoints, the
get_current_userdependency:- 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.
- decodes the token (
Logout in a pure JWT scheme requires either a short token TTL + a refresh token, or a blacklist of revoked tokens (a
revoked_tokenstable). 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 |
| 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
adminpermission (full access) - 1 user with the
can_viewpermission (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 (empty, not yet created)
Running
poetry
make run
Rename env