Understanding Login, JWT, Access Tokens, Refresh Tokens & JWK in Adarsh Autho Forge
A deep dive into how authentication actually works — access tokens, refresh tokens, JWKs, and secure login flows used by real-world identity systems.
Modern authentication systems — whether it’s Google, Auth0, Okta, or AWS Cognito — all use the same fundamental architecture for user login and token lifecycle: short-lived access tokens, long-lived refresh tokens, public verification keys, and secure password hashing.
In Adarsh Autho Forge, I built the same model but in a lightweight, self-hosted, developer-friendly form designed specifically for microservices.
This article explains in depth how the login flow works and why these concepts matter.
🔐 Why Authentication is Designed This Way
A login system needs to be:
- Secure (no plaintext passwords, no leaked secrets)
- Stateless (microservices should not call the auth server for every request)
- Scalable (100k+ token verifications / second)
- Extendable (roles, permissions, multi-device sessions)
- Language-agnostic (other services may be in Go, Node, Rust, etc.)
To achieve all this, modern systems rely on:
- JWT Access Tokens
- Refresh Tokens
- RSA Signing & JWK Publishing
- Hashed credentials and tokens
- Zero shared secrets between services
Let’s break this down in a simple way.
🔥 1. What is a JWT Access Token?
A JWT Access Token is:
✔ A digitally signed token
✔ Given to a user after login
✔ Sent with every request
✔ Short-lived (5–15 minutes)
✔ Verifiable without calling auth server
✔ Impossible to tamper with
Think of it like a boarding pass:
- It proves who you are
- What you’re allowed to do (roles)
- When it expires
- Who issued it
- It has a signature microservices can verify
🧬 Anatomy of a JWT
A JWT has three parts:
- Header, Payload and Signature
Header example:
{
"alg": "RS256",
"kid": "key_2025_01"
}
Payload example:
{
"sub": 1,
"username": "adarsh",
"roles": ["ADMIN"],
"exp": 1734550000,
"iat": 1734548200,
"iss": "https://autho-forge"
}
Signature:
Signed with the private RSA key stored ONLY in the auth server. Microservices validate it using the public JWK.
🔑 2. What is a Refresh Token?
A refresh token is:
- ✔ A long-lived, secure random string
- ✔ NOT a JWT
- ✔ NOT signed
- ✔ Impossible to guess
- ✔ Stored hashed in the database
- ✔ Used to issue new access tokens
- ✔ Used to maintain long-term session
If your access token expires, the client can request:
POST /auth/refresh
{
refreshToken: "raw-random-token"
}
The refresh token provides:
-
A new access token
-
A new refresh token (rotation)
This allows users to stay logged in for weeks/months without re-entering credentials.
🤔 Why do we need BOTH Access & Refresh Tokens?
Because they serve different purposes:
🟣 Access Token — Short-lived, high-speed security
-
Used in every API call
-
Verified instantly by microservices
-
No DB checks, no network calls
-
If stolen, damage is limited
🔵 Refresh Token — Long-lived session
-
Used rarely (only when the access token expires)
-
Allows issuing new tokens without re-login
-
Stored securely in a hashed format
-
Revocable
Together, they give:
✔ Security ✔ Performance ✔ Convenience ✔ Scalability
This is exactly how Auth0, Cognito, Google OAuth, and Okta work.
🗝️ 3. Why Use RSA Keys to Sign JWT?
JWTs are signed using the private RSA key inside the auth service.
Microservices verify tokens using the public RSA key, provided as JWK:
-
No shared secrets
-
No central dependency
-
No DB calls for each API request
-
Tokens can be verified locally in microservices
-
Perfect for distributed architectures
This makes the system truly scalable.
🌍 4. What is a JWK?
A JWK (JSON Web Key) is simply the public RSA key in JSON format, served at:
GET /.well-known/jwks.json
Example:
{
"keys": [
{
"kid": "key_2025_01",
"kty": "RSA",
"alg": "RS256",
"n": "base64EncodedModulus",
"e": "AQAB"
}
]
}
Microservices:
-
Fetch this once
-
Cache it
-
Use it to validate all tokens
This makes each microservice independent of the auth service.
🧱 5. Why Store Refresh Tokens Hashed?
If your DB gets hacked and someone sees:
refreshToken = "raw_token_123"
They can impersonate users.
So instead, you do:
hashedRefreshToken = bcrypt("raw_token_123")
Just like passwords.
Even if the DB leaks, sessions remain safe.
🔄 6. Complete Login Flow — Step by Step
Let’s tie everything together.
1️⃣ Client sends username + password
POST /auth/login
2️⃣ System fetches user
If user does not exist → throw InvalidCredentialsException
3️⃣ Verify password
Using:
passwordEncoder.matches(raw, hash)
4️⃣ Issue JWT Access Token
Signed with RSA private key
Contains roles
Includes expiry
5️⃣ Generate Refresh Token
Long-lived
Secure random
Stored hashed
6️⃣ Save refresh token in DB
Supports rotation + revocation.
7️⃣ Respond with TokenResponse
Contains:
-
accessToken
-
refreshToken
-
accessTokenExpiry
-
refreshTokenExpiry
📦 Summary Table
| Token Type | Format | Lifespan | Stored In | Purpose |
|---|---|---|---|---|
| Access Token | JWT (signed) | Short (5–15 min) | Client only | Authorize API calls |
| Refresh Token | Random string | Long (days/months) | Database (hashed) | Get new access tokens |
| JWK | Public key | Always valid | Auth service | Validate JWT signatures |
🎯 Final Thoughts
Understanding how login, JWT, access tokens, refresh tokens, and JWKs work gives you the foundation for secure, scalable authentication systems.
With this knowledge, you now understand:
Why JWTs exist
Why we sign with RSA
Why JWK is published
Why access tokens must be short-lived
Why refresh tokens must be hashed
Why microservices validate tokens independently
How modern auth systems achieve both security and scalability
This is exactly the architecture used by:
-
Google
-
Facebook
-
Auth0
-
Okta
-
AWS Cognito