Stateless Tokens (JWTs) | PropelAuth BYO Documentation
Stateless Tokens (JWTs)
Stateless tokens (JWTs) let you validate sessions without making any network calls - not to PropelAuth BYO, not to your database. You validate them using just a public key, making them incredibly fast for high-scale applications.
Think of them as a performance optimization for your regular sessions. While regular session validation adds a few milliseconds to check if a session is still valid, stateless tokens can be validated in microseconds with zero network overhead.
When to Use Stateless Tokens
For most applications, regular session validation is perfectly fine. The few milliseconds it takes to validate a session won’t impact your users. But there are scenarios where stateless tokens shine:
Use stateless tokens when:
- You have extremely high request volume (millions of requests per minute)
- You need to validate sessions in edge functions or serverless environments
- You have read-heavy APIs where the same session is validated hundreds of times per second
Keep using regular sessions when:
- You have low to moderate request volume
- You need instant revocation (stateless tokens can’t be revoked until they expire)
- You need to track session activity in real-time
The beauty is you don’t have to choose one or the other - you can use both. Many applications use regular sessions for sensitive operations and stateless tokens for read-heavy operations.
Getting Started
The key insight is that stateless tokens are created FROM sessions. You first create a regular session, then generate a short-lived JWT from it.
Node
app.post("/api/login", async (req, res) => {
// Authenticate your user however you normally do
const user = await authenticateUser(req.body.email, req.body.password);
// Step 1: Create a regular session
const session = await client.session.create({
userId: user.id,
ipAddress: req.ip,
userAgent: req.headers["user-agent"],
});
if (!session.ok) {
return res.status(500).json({ error: "Could not create session" });
}
// Step 2: Create a stateless token from the session
const token = await client.session.createStatelessToken({
userId: user.id,
sessionId: session.data.sessionId,
customClaims: {
email: user.email,
role: user.role,
teamId: user.teamId,
},
lifetimeSecs: 900, // 15 minutes
});
if (!token.ok) {
return res.status(500).json({ error: "Could not create token" });
}
res.cookie("sessionToken", session.data.sessionToken, COOKIE_OPTIONS);
res.cookie("statelessToken", token.data.statelessToken, COOKIE_OPTIONS);
res.json({ success: true });
});
Python
class LoginRequest(BaseModel):
email: str
password: str
@app.post("/api/login")
async def login(login_request: LoginRequest, request: Request, response: Response):
user = await authenticate_user(login_request.email, login_request.password)
# Step 1: Create a regular session
session = await client.session.create(
user_id=user.id,
ip_address=request.client.host,
user_agent=request.headers.get("user-agent")
)
if is_err(session):
raise HTTPException(status_code=500, detail="Could not create session")
# Step 2: Create a stateless token from the session
token = await client.session.create_stateless_token(
user_id=user.id,
session_id=session.data.session_id,
custom_claims={
"email": user.email,
"role": user.role,
"teamId": user.team_id
},
lifetime_secs=900 # 15 minutes
)
if is_err(token):
raise HTTPException(status_code=500, detail="Could not create token")
response.set_cookie("sessionToken", session.data.session_token, **COOKIE_OPTIONS)
response.set_cookie("statelessToken", token.data.stateless_token, **COOKIE_OPTIONS)
return {"success": True}
Java
@PostMapping("/api/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) {
// Authenticate your user however you normally do
User user = authenticateUser(loginRequest.getEmail(), loginRequest.getPassword());
// Step 1: Create a regular session
CreateSessionResponse session;
try {
session = client.session.create(
CreateSessionCommand.builder()
.userId(user.getId())
.ipAddress(request.getRemoteAddr())
.userAgent(request.getHeader("User-Agent"))
.build()
);
} catch (CreateSessionException e) {
return ResponseEntity.status(500).body(Map.of("error", "Could not create session"));
}
// Step 2: Create a stateless token from the session
CreateStatelessTokenResponse token;
try {
token = client.session.createStatelessToken(
CreateStatelessTokenCommand.builder()
.userId(user.getId())
.sessionId(session.getSessionId())
.customClaims(JsonValue.of(Map.of(
"email", user.getEmail(),
"role", user.getRole(),
"teamId", user.getTeamId()
)))
.lifetimeSecs(900) // 15 minutes
.build()
);
} catch (CreateStatelessTokenException e) {
return ResponseEntity.status(500).body(Map.of("error", "Could not create token"));
}
Cookie sessionCookie = new Cookie("sessionToken", session.getSessionToken());
sessionCookie.setHttpOnly(true);
sessionCookie.setSecure(true);
sessionCookie.setPath("/");
response.addCookie(sessionCookie);
Cookie statelessCookie = new Cookie("statelessToken", token.getStatelessToken());
statelessCookie.setHttpOnly(true);
statelessCookie.setSecure(true);
statelessCookie.setPath("/");
response.addCookie(statelessCookie);
return ResponseEntity.ok(Map.of("success", true));
}
Validating Stateless Tokens
To validate stateless tokens, you need the public key from PropelAuth BYO. Here’s what NOT to do:
Node
app.get("/api/data", async (req, res) => {
const token = req.cookies.accessToken;
// DON'T DO THIS - fetches JWKs on every request!
// This network call defeats the purpose of stateless tokens
const jwks = await client.session.getJwks({});
try {
// Using your favorite JWT library
const dataInStatelessToken = jwt.verify(token, jwks);
// do something with the data
} catch (error) {
return res.status(401).json({ error: "Invalid token" });
}
});
The Token Refresh Pattern
Stateless tokens should be short-lived for security - typically 5 to 15 minutes. When they expire, use the session token to generate a new one:
Node
app.post("/api/refresh", async (req, res) => {
// Validate the session is still active
const validation = await client.session.validate({
sessionToken: req.cookies.sessionToken,
ipAddress: req.ip,
});
if (!validation.ok) {
return res.status(401).json({ error: "Session expired" });
}
// Generate a new stateless token
const token = await client.session.createStatelessToken({
userId: validation.data.userId,
sessionId: validation.data.sessionId,
customClaims: validation.data.metadata,
lifetimeSecs: 900,
});
res.cookie("accessToken", token.data.statelessToken, COOKIE_OPTIONS);
res.json({ success: true });
});
Putting It Together
Stateless tokens are a powerful optimization for specific scenarios. They’re not a replacement for sessions - they’re a complement to them. Use regular sessions for your core authentication flow and stateless tokens when you need extreme performance.
Start with regular sessions. When you hit scale issues or need better performance for specific endpoints, add stateless tokens to those hot paths. Your architecture stays simple while gaining the performance exactly where you need it.