Phase 2 Done - AI Tutor Web APP
Custom Authentication and Unit testing

Phase 2: Authentication System:
After setting up the project last week, this week I have completed my custom authentication system for my AI tutor web app.
Reinventing the wheel?
First thing that comes into your mind if you have work in development is why you are creating your own custom auth, why not using supabase or some other auth system? Well, you may be right in some cases, and I even first used the supabase auth, where I just have to call two function one for sign up and one for sign in, simple. But as I am doing it for the first time, I want to understand what is happening behind the scense so if some error or my app auth crashes at some point in the future, I will visit my code, not the Supabase documentation.
Key Components:
Password Hashing
JWT Token system
Authentication Endpoints
Protected Routes
Email Verification flow
Database Integration
Configurations
Unit Testing
1. Password Hashing:
Password hashing is a specific one-way cryptographic process that transforms a password into a unique, fixed-length string of characters (a "hash") using complex mathematical algorithms.
In Password hashing, we used two layered hashing system:
SHA-256 - pre-hashing to make sure the password is within 256 bits or 64 characters.
Bcrypt with salt for final hash storage. ( Salt is a random string added to the SHA-256 hash value)
import bcrypt
import jwt
def get_password_hash(password: str):
password_bytes = password.encode("utf-8")
digest = hashlib.sha256(password_bytes).hexdigest().encode("utf-8")
salt = bcrypt.gensalt()
hashed_bytes = bcrypt.hashpw(digest, salt)
return hashed_bytes.decode("utf-8")
def verify_password(plain_password: str, hashed_password: str) -> bool:
password_bytes = plain_password.encode("utf-8") # this convert to byte format
digest = hashlib.sha256(password_bytes).hexdigest().encode("utf-8")
return bcrypt.checkpw(digest, hashed_password.encode("utf-8"))
2. JWT Token System:
JWT is used to exchange information between parties securely.
Structure: Payload(user_data) + Header( Algorithm and token type) + Signature( JWT secret key)
JSON Web Tokens(JWTs) are used to do several things, like:
Identify User - By extracting the user ID from the token.
Maintain State - No server-side sessions needed( stateless).
Prevent Tampering - Signature proves the token wasn't modified.
Expire automatically - After a certain fixed time duration, the token becomes invalid.
Carry data - Can embed user info without DB lookup.
We created two types of JWT tokens :
JWT Access Token - Expires after a short period of time.
JWT Refresh Token - Expires after a long period.
Why two types of tokens? well this is because we can protect from the hackers if someone gets access to the access token, which will not be useful after some time, and even if they somehow get access to the refresh token, then when the original user logout we will remove it from the databse this way, we can make sure the authenticated user is using our app.
For both tokens, we used different JWT secret keys because even if an attacker gets one key, they cannot format the other key.
3. Authentication Endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
| /api/v1/register | POST | Triggers email verification (sends Resend email) |
| /api/v1/verify | POST | Verifies token and creates a user in Supabase |
| /api/v1/login | POST | Returns access + refresh tokens and save refresh token in db |
| /api/v1/refresh | POST | Issues new access token from a refresh token |
| /api/v1/logout | POST | Log out the user and remove the refresh token from the database |
4. Protected Routes:
We use fastapi dependency injections to make sure only authenrized user get access to certain endpoints like get_user_info, updating user info, or changing password.
http_bearer = HTTPBearer()
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(http_bearer)):
token = credentials.credentials
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = decode_jwt_token(token)
user_id = payload.get("sub")
print("*" * 20)
print(payload, " : Did we reach here?")
print("*" * 20)
if user_id is None:
raise credentials_exception
except JWTError as e:
raise credentials_exception from e
client = get_supabase_client()
try:
response = client.table("users").select("*").eq("id", user_id).single().execute()
if not response.data:
raise HTTPException(status_code=404, detail="User not found")
return response.data
except Exception as e:
raise credentials_exception from e
Uses FastAPI's HTTPBearer for automatic Bearer token extraction
get_current_user() dependency validates JWT and fetches user from Supabase
Returns 401 Unauthorized if the token is invalid/expired
5. Email Verification Flow:
User registers → JWT token created with user data
Resend service sends verification email with link
Frontend clicks link →
/verifyendpoint calledToken decoded and user inserted into Supabase
6. Database Integration(Supabase)
User table stores: id, email, password, refresh_token, created_at
Admin client used for registration (bypasses RLS)
User client used for other queries
7. Configuration:
Config.py file contains environment variables such as:
ALGORITHM (typically "HS256")
RESEND_API_KEY (for email verification)
SUPABASE_URL, SUPABASE_SERVICE_KEY
With the help of these, we can connect to the database or get access to Groq and other actions. Although it is not actual part of auth but we need it.
8. Unit Testing:
I have added unit testing for all the auth endpoints using pytest, TestClient, patch, and fixtures. In testing, we have to mock the database calls, client, api responses, and dependencies using patch.
How Mocking Works (The Core Concept)
WITHOUT Mocking (REAL DATABASE):
@router.post("/users/register")
def register_user(body: UserRegister):
client = get_supabase_client() # ← REAL connection to Supabase!
existing = client.table("users").select("email").eq("email", body.email).execute()
# ... actual database calls ...
Problems:
Test needs real Supabase connection
Test creates real data in database
Test is slow (requires internet)
Test might fail due to network issues, not code bugs
Difficult to test error scenarios
WITH Mocking (FAKE DATABASE):
# Create a fake Supabase client
fake_client = Mock()
fake_client.table("users").select(...).find() = [] # Pretend no user exists
# Replace real Supabase with fake one
with patch("src.database.database.get_supabase_client", return_value=fake_client):
response = client.post("/api/v1/users/Registeration", json=student_data)
# Test runs with fake data, no real database calls!
Benefits:
Fast (no network calls)
No real data created
Deterministic (predictable results)
Easy to test error scenarios
Isolated (tests don't interfere with each other)
Current Status of Backend:
| Component | Status | Details |
|---|---|---|
| Custom Auth System | Fully Built | SHA256 + bcrypt password hashing, JWT tokens (access + refresh) |
| JWT Tokens | Working | Two-token system with different secret keys, auto-expiry |
| Login/Register | Working | Email registration with JWT verification, login with tokens |
| Logout | Working | Refresh token revocation from the database |
| Protected Routes | Working | Dependency injection (get_current_user) protecting endpoints |
| Database | Connected | Supabase integration with users/universities tables |
| Email Verification | Working | Resend the email service for account verification |
| Unit Tests | Setup | pytest fixtures, mocking (registration, login, updates) |
| CORS | Configured | Frontend URLs whitelisted (localhost + Vercel) |
| API Docs | Available | Swagger UI at /docs |
Yes, there are still some improvements needed. I will look into it later.

