A robust, token-versioned authentication project born from real-world development challenges.
While working on my recent NoteNest project, I ran into significant hurdles related to authentication and authorization.
At the time, I was manually decoding refresh tokens and handling all validation logic myself. This approach made the project:
- ❌ Unnecessarily complex
- ❌ Harder to maintain
- ❌ Bloated with excessive lines of code
Additionally, I noticed that Django’s default authentication system works best with its built-in user model. Modifying it (adding fields, changing behavior) required extra effort, workarounds, and custom handling.
So, I decided to rebuild the authentication system from the ground up—properly.
To modernize and secure the setup, I implemented several structural improvements:
- Replaced the default user model with a custom user model.
- Adopted JWT-based authentication to eliminate manual token handling.
- Leveraged built-in permission classes like
IsAuthenticated. - Centralized all security logic using a strictly scoped custom
authentication.py. - Introduced Token Versioning to fix a critical session security vulnerability.
Standard JWT implementation features a major flaw. Even after a user clicks "Logout":
- ✅ Refresh Token gets successfully blacklisted.
- ❌ Access Token remains fundamentally valid until its time expiry runs out.
That means a user (or an attacker with a stolen token) can still successfully call private APIs even after logging out. This was the biggest security hole in the project.
To fix this decisively, I introduced a token_version field to the custom user model.
This integer value is:
- Stored dynamically in the database.
- Embedded instantly inside the JWT token's payload upon generation.
Now, every single API request performs a strict check:
👉 "Does the
token_versioninside the token match thetoken_versionstored in the database?"
Here is a step-by-step breakdown of how the versioning works across a session lifecycle:
1️⃣ Register / Login
→ token_version = 0 (stored in DB)
→ Token payload embeds version '0'
🟢 DB = 0, Token = 0 → MATCH ✅ (Request Allowed)
2️⃣ Logout
→ DB version increments to 1
→ The attacker's old token still embeds version '0'
🔴 DB = 1, Token = 0 → MISMATCH ❌ (Request Blocked Instantly)
3️⃣ Login Again
→ New token is created
→ New token payload embeds version '1'
🟢 DB = 1, Token = 1 → MATCH ✅ (Request Allowed)
4️⃣ Logout Again
→ DB version increments to 2
→ The previous token still embeds version '1'
🔴 DB = 2, Token = 1 → MISMATCH ❌ (Request Blocked Instantly)
5️⃣ Login Again
→ New token is created embedding version '2'
🟢 DB = 2, Token = 2 → MATCH ✅ (Request Allowed)
👉 The Impact:
- Old tokens die instantly.
- We no longer have to wait for the standard token expiry window.
- Logout becomes truly, cryptographically secure.
Each API is kept extremely clean and heavily utilizes serializers for validation and structured response handling:
- 🟢
POST /register/→ Creates a new user and natively returns fresh access/refresh tokens. - 🟢
POST /login/→ Verifies user credentials and generates new tokens encoding the latest version. - 🔴
POST /logout/→ Blacklists the refresh token and increments the user'stoken_version. - 🔵
GET /profile/→ Returns the user's secure profile data (strictly protected, requires valid auth).
Instead of checking tokens manually in every view, I offloaded everything to a custom authentication class.
It handles validation under the hood before the view ever runs. It:
- Reads the token from the request header.
- Unpacks and mathematically validates the signature.
- Compares the packed
token_versionwith the target Database. - Allows or blocks the request autonomously.
This keeps the codebase incredibly clean, DRY, and completely reusable.
- Authentication is handled globally using the custom JWT authentication class.
- Permissions are controlled fundamentally using
IsAuthenticated. - Public-facing APIs (like Login or Register) intentionally override this using
AllowAny.
Because of this specific design, I never have to write raw authentication logic repeatedly.
By implementing this architecture:
- 📈 Authentication became drastically simpler.
- 🧹 Code became cleaner and shorter.
- 🛡️ Security improved significantly.
- 🔒 Logout now actually invalidates all old tokens instantly across all devices.
Many developers learn by reading real projects on GitHub. This repository represents an implementation where I solved a real-world architectural problem I personally faced.
If you're building a JWT-based framework stack, borrowing this approach can save you days of confusion, spaghetti code, and security bugs.
If you'd like to see more, feel free to request API request/response examples or explore the codebase firsthand! 🚀