Session Quick Start
Sessions are how your application remembers that a user is logged in. This guide will get you from zero to a working login/logout flow in just a few minutes.
By the end, you’ll have:
- A backend endpoint that creates sessions on login
- Session validation for protected routes
- A complete logout flow
Prerequisites
Section titled “Prerequisites”Before starting, make sure you have:
- PropelAuth BYO installed and running
- Backend client library set up
- A way to authenticate users (we’ll assume you have this - could be email/password, OAuth, etc.)
Step 1: Create Your First Session
Section titled “Step 1: Create Your First Session”When a user successfully logs in, create a session for them and store it as an HTTP-only cookie:
app.post("/api/login", async (req, res) => { // Verify the user's credentials (your existing logic) const userId = await verifyUserCredentials(req.body);
const result = await client.session.create({ userId: userId, });
if (!result.ok) { console.error("Session creation failed:", result.error); return res.status(500).json({ error: "Failed to create session" }); }
// Store the session token as an HTTP-only cookie res.cookie("sessionToken", result.data.sessionToken, { httpOnly: true, secure: true, sameSite: "lax", });
res.json({ success: true });});class LoginRequest(BaseModel): # Your login fields pass
@app.post("/api/login")async def login(request: LoginRequest, response: Response): # Verify the user's credentials (your existing logic) user_id = await verify_user_credentials(request)
result = await client.session.create(user_id=user_id)
if is_err(result): print("Session creation failed:", result.error) raise HTTPException(status_code=500, detail="Failed to create session")
# Store the session token as an HTTP-only cookie response.set_cookie( key="sessionToken", value=result.data.session_token, httponly=True, secure=True, samesite="lax" )
return {"success": True}@PostMapping("/api/login")public ResponseEntity<?> login(@RequestBody LoginRequest request, HttpServletResponse response) { // Verify the user's credentials (your existing logic) String userId = verifyUserCredentials(request);
try { CreateSessionResponse result = client.session.create( CreateSessionCommand.builder() .userId(userId) .build() );
// Store the session token as an HTTP-only cookie Cookie cookie = new Cookie("sessionToken", result.getSessionToken()); cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setPath("/"); response.addCookie(cookie);
return ResponseEntity.ok(Map.of("success", true)); } catch (CreateSessionException e) { System.err.println("Session creation failed: " + e.getMessage()); return ResponseEntity.status(500).body(Map.of("error", "Failed to create session")); }}[HttpPost("/api/login")]public async Task<IActionResult> Login([FromBody] LoginRequest request){ // Verify the user's credentials (your existing logic) var userId = await VerifyUserCredentials(request);
try { var result = await client.Session.CreateAsync(new CreateSessionCommand { UserId = userId });
// Store the session token as an HTTP-only cookie Response.Cookies.Append("sessionToken", result.SessionToken, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax });
return Ok(new { Success = true }); } catch (CreateSessionException ex) { Console.Error.WriteLine($"Session creation failed: {ex.Message}"); return StatusCode(500, new { Error = "Failed to create session" }); }}Now when the user logs in, they’ll receive a cookie that automatically gets sent with every request.
Step 2: Validate Sessions on Protected Routes
Section titled “Step 2: Validate Sessions on Protected Routes”For any route that requires authentication, validate the session token:
app.get("/api/protected-route", async (req, res) => { const sessionToken = req.cookies.sessionToken;
const validation = await client.session.validate({ sessionToken, });
if (!validation.ok) { console.error("Session validation failed:", validation.error); return res.status(401).json({ error: "Unauthorized" }); }
// The session is valid! You can access the user ID and metadata const { userId, metadata } = validation.data;
// Your protected route logic here res.json({ message: "You're authenticated!", userId, metadata, });});@app.get("/api/protected-route")async def protected_route(request: Request): session_token = request.cookies.get("sessionToken")
validation = await client.session.validate(session_token=session_token)
if is_err(validation): print("Session validation failed:", validation.error) raise HTTPException(status_code=401, detail="Unauthorized")
# The session is valid! You can access the user ID and metadata user_id = validation.data.user_id metadata = validation.data.metadata
# Your protected route logic here return { "message": "You're authenticated!", "userId": user_id, "metadata": metadata, }@GetMapping("/api/protected-route")public ResponseEntity<?> protectedRoute(@CookieValue(name = "sessionToken", required = false) String sessionToken) { try { ValidateSessionResponse validation = client.session.validate( ValidateSessionCommand.builder() .sessionToken(sessionToken) .build() );
// The session is valid! You can access the user ID and metadata String userId = validation.getUserId(); JsonValue metadata = validation.getMetadata();
// Your protected route logic here return ResponseEntity.ok(Map.of( "message", "You're authenticated!", "userId", userId, "metadata", metadata )); } catch (ValidateSessionException e) { System.err.println("Session validation failed: " + e.getMessage()); return ResponseEntity.status(401).body(Map.of("error", "Unauthorized")); }}[HttpGet("/api/protected-route")]public async Task<IActionResult> ProtectedRoute(){ var sessionToken = Request.Cookies["sessionToken"];
try { var validation = await client.Session.ValidateAsync(new ValidateSessionCommand { SessionToken = sessionToken });
// The session is valid! You can access the user ID and metadata var userId = validation.UserId; var metadata = validation.Metadata;
// Your protected route logic here return Ok(new { Message = "You're authenticated!", UserId = userId, Metadata = metadata }); } catch (ValidateSessionException ex) { Console.Error.WriteLine($"Session validation failed: {ex.Message}"); return StatusCode(401, new { Error = "Unauthorized" }); }}The Validate Session function checks if:
- The token exists and is valid
- The session hasn’t expired
- The session hasn’t been invalidated
If validation succeeds, you get back the user ID and any metadata you stored when creating the session. If validation fails, you get a detailed error explaining exactly why (e.g. IP address not allowed, session expired, etc.).
Step 3: Add a Frontend
Section titled “Step 3: Add a Frontend”Now let’s connect a frontend. Here’s a simple login form that sends credentials to your backend:
function LoginForm() { const [email, setEmail] = useState(""); const [password, setPassword] = useState("");
const handleLogin = async (e) => { e.preventDefault();
const response = await fetch("/api/login", { method: "POST", credentials: "include", // Important: include cookies headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), });
if (response.ok) { // Redirect to dashboard or protected area window.location.href = "/dashboard"; } else { // Handle error const error = await response.json(); alert(error.error); } };
return ( <form onSubmit={handleLogin}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" required /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" required /> <button type="submit">Log In</button> </form> );}Note: credentials: "include" ensures cookies are included when making requests to protected routes after login.
Step 4: Handle Logout
Section titled “Step 4: Handle Logout”When users log out, invalidate their session to ensure it can’t be used again:
app.post("/api/logout", async (req, res) => { const sessionToken = req.cookies.sessionToken;
await client.session.invalidateByToken({ sessionToken, });
// Clear the cookie res.clearCookie("sessionToken");
res.json({ success: true });});@app.post("/api/logout")async def logout(request: Request, response: Response): session_token = request.cookies.get("sessionToken")
await client.session.invalidate_by_token(session_token=session_token)
# Clear the cookie response.delete_cookie("sessionToken")
return {"success": True}@PostMapping("/api/logout")public ResponseEntity<?> logout(@CookieValue(name = "sessionToken", required = false) String sessionToken, HttpServletResponse response) { client.session.invalidateByToken( InvalidateSessionByTokenCommand.builder() .sessionToken(sessionToken) .build() );
// Clear the cookie Cookie cookie = new Cookie("sessionToken", ""); cookie.setMaxAge(0); response.addCookie(cookie);
return ResponseEntity.ok(Map.of("success", true));}[HttpPost("/api/logout")]public async Task<IActionResult> Logout(){ var sessionToken = Request.Cookies["sessionToken"];
await client.Session.InvalidateByTokenAsync(new InvalidateSessionByTokenCommand { SessionToken = sessionToken });
// Clear the cookie Response.Cookies.Delete("sessionToken");
return Ok(new { Success = true });}And the corresponding frontend:
function LogoutButton() { const handleLogout = async () => { const response = await fetch("/api/logout", { method: "POST", credentials: "include", // Include the session cookie });
if (response.ok) { // Redirect to login page window.location.href = "/login"; } };
return <button onClick={handleLogout}>Log Out</button>;}You’re Done!
Section titled “You’re Done!”You now have a working session management system. Users can log in, stay logged in across requests, and securely log out.
Adding Security Features
Section titled “Adding Security Features”Your basic session management is working, but you can enhance security with just a few additions:
Detect User Agent Changes
Section titled “Detect User Agent Changes”// When creating the sessionconst session = await auth.session.create({ userId: userId, userAgent: req.headers["user-agent"],});
// When validatingconst validation = await auth.session.validate({ sessionToken, userAgent: req.headers["user-agent"],});# When creating the sessionsession = await client.session.create( user_id=user_id, user_agent=request.headers.get("user-agent"))
# When validatingvalidation = await client.session.validate( session_token=session_token, user_agent=request.headers.get("user-agent"))// When creating the sessionCreateSessionResponse session = client.session.create( CreateSessionCommand.builder() .userId(userId) .userAgent(request.getHeader("User-Agent")) .build());
// When validatingValidateSessionResponse validation = client.session.validate( ValidateSessionCommand.builder() .sessionToken(sessionToken) .userAgent(request.getHeader("User-Agent")) .build());// When creating the sessionvar session = await client.Session.CreateAsync(new CreateSessionCommand{ UserId = userId, UserAgent = Request.Headers["User-Agent"].ToString()});
// When validatingvar validation = await client.Session.ValidateAsync(new ValidateSessionCommand{ SessionToken = sessionToken, UserAgent = Request.Headers["User-Agent"].ToString()});Some changes in the user agent are suspicious (like switching from Mac to Windows), whereas others aren’t (like a minor Chrome version update). PropelAuth BYO automatically detects suspicious changes that could indicate a stolen session and invalidates the session for you.
Add IP Address Checks / Logging
Section titled “Add IP Address Checks / Logging”// When creating the sessionconst session = await client.session.create({ userId: userId, ipAddress: req.ip,});
// When validatingconst validation = await client.session.validate({ sessionToken, ipAddress: req.ip,});# When creating the sessionsession = await client.session.create( user_id=user_id, ip_address=request.client.host)
# When validatingvalidation = await client.session.validate( session_token=session_token, ip_address=request.client.host)// When creating the sessionCreateSessionResponse session = client.session.create( CreateSessionCommand.builder() .userId(userId) .ipAddress(request.getRemoteAddr()) .build());
// When validatingValidateSessionResponse validation = client.session.validate( ValidateSessionCommand.builder() .sessionToken(sessionToken) .ipAddress(request.getRemoteAddr()) .build());// When creating the sessionvar session = await client.Session.CreateAsync(new CreateSessionCommand{ UserId = userId, IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()});
// When validatingvar validation = await client.Session.ValidateAsync(new ValidateSessionCommand{ SessionToken = sessionToken, IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()});Including IP addresses enables powerful features like IP allowlists/blocklists, better audit logging, and the ability to configure IP-based rules per customer.
Store User Data in Sessions
Section titled “Store User Data in Sessions”const session = await client.session.create({ userId: userId, metadata: { email: user.email, role: user.role, teamId: user.teamId, },});session = await client.session.create( user_id=user_id, metadata={ "email": user.email, "role": user.role, "teamId": user.team_id, })CreateSessionResponse session = client.session.create( CreateSessionCommand.builder() .userId(userId) .metadata(JsonValue.of(Map.of( "email", user.getEmail(), "role", user.getRole(), "teamId", user.getTeamId() ))) .build());var session = await client.Session.CreateAsync(new CreateSessionCommand{ UserId = userId, Metadata = JsonSerializer.SerializeToElement(new Dictionary<string, object> { { "email", user.Email }, { "role", user.Role }, { "teamId", user.TeamId } })});This data comes back when you validate the session, saving database lookups.
What’s Next?
Section titled “What’s Next?”Now that you have sessions working, you can:
- Configure session behavior - Adjust expiration times, concurrent session limits, and more in your session configuration
- Add device registration - Get alerts for new device logins and enable theft protection
- Use session tags - Create custom rules for specific customers
- Add JWTs - Support very high-scale applications with stateless tokens that don’t require network calls to validate
Check out the Session Overview for a complete guide to all features, or dive into the API Reference for detailed documentation.