Session Theft Protection
Session theft occurs when an attacker steals a user’s session cookies, typically through phishing, malware, or network attacks. Once they have a user’s session token, they can impersonate the user and access their account.
PropelAuth BYO’s session theft protection uses cryptographic device verification to detect and block these attacks. Even if an attacker steals your session cookies, they can’t use them without the cryptographic keys stored on your device.
Prerequisites
Section titled “Prerequisites”Before implementing session theft protection, you need to have completed the setup for New Device Notifications. That guide covers:
- Installing the BYO JavaScript library
- Creating a device challenge endpoint
- Initializing
fetchWithDevice - Registering devices during login
If you haven’t set that up yet, start there first.
How It Works
Section titled “How It Works”Session theft protection extends device registration by verifying the device on every authenticated request, not just at login. This means:
- When a user logs in, their device is registered (covered in New Device Notifications)
- On every subsequent request, the device must prove its identity
- If an attacker steals the session cookie and tries to use it from a different device, the request is blocked
Implementation
Section titled “Implementation”Since you’ve already set up device registration for new device notifications, you only need to:
-
Update All Authenticated Fetches
Section titled “Update All Authenticated Fetches”Replace all your authenticated
fetchcalls withfetchWithDevice. This automatically includes the device verification headers.// Beforeconst response = await fetch("/api/user-data", {method: "GET",credentials: "include",});// Afterimport { fetchWithDevice } from "@propelauth/byo-javascript";const response = await fetchWithDevice("/api/user-data", {method: "GET",credentials: "include",}); -
Add Device Verification to Session Validation
Section titled “Add Device Verification to Session Validation”Update your backend session validation to verify the device. The
deviceVerificationparameter requires the signed device challenge from thedpopheader.const getSignedDeviceChallenge = (req: Request): string | undefined => {const dpopHeader = req.headers["dpop"];if (!dpopHeader || typeof dpopHeader !== "string") {return undefined;}return dpopHeader;};app.get("/api/user-data", async (req: Request, res: Response) => {const sessionToken = req.cookies.sessionToken;const signedDeviceChallenge = getSignedDeviceChallenge(req);if (!signedDeviceChallenge) {return res.status(401).json({error: "Device verification required"});}const result = await client.session.validate({sessionToken: sessionToken,ipAddress: req.socket.remoteAddress,userAgent: req.headers["user-agent"],deviceVerification: {signedDeviceChallenge,},});if (result.ok) {// Return user datares.json({ user: result.data.user });} else if (result.error.type === "NewDeviceChallengeRequired") {// Return new challenge just like in loginres.status(425).json({errorType: "NewDeviceChallengeRequired",deviceChallenge: result.error.details.deviceChallenge,expiresAt: result.error.details.expiresAt,});} else {// Session invalid or device verification failedres.status(401).json({ error: result.error });}});from propelauth_byo.generated.device_verification import DeviceVerification@app.get("/api/user-data")async def get_user_data(request: Request):session_token = request.cookies.get("sessionToken")signed_device_challenge = request.headers.get("dpop")result = await client.session.validate(session_token=session_token,ip_address=request.client.host,user_agent=request.headers.get("user-agent"),device_verification=DeviceVerification(signed_device_challenge=signed_device_challenge))if is_ok(result):return {"user": result.data.user}elif result.error.type == "NewDeviceChallengeRequired":return JSONResponse(status_code=425,content={"errorType": "NewDeviceChallengeRequired","deviceChallenge": result.error.details.device_challenge,"expiresAt": result.error.details.expires_at})else:print("Session validation failed:", result.error)raise HTTPException(status_code=401, detail="Unauthorized")@GetMapping("/api/user-data")public ResponseEntity<?> getUserData(@CookieValue(name = "sessionToken", required = false) String sessionToken, HttpServletRequest request) {String signedDeviceChallenge = request.getHeader("dpop");try {ValidateSessionResponse result = client.session.validate(ValidateSessionCommand.builder().sessionToken(sessionToken).ipAddress(request.getRemoteAddr()).userAgent(request.getHeader("User-Agent")).deviceVerification(DeviceVerification.builder().signedDeviceChallenge(signedDeviceChallenge).build()).build());return ResponseEntity.ok(Map.of("user", result.getUser()));} catch (ValidateSessionException.NewDeviceChallengeRequired e) {return ResponseEntity.status(425).body(Map.of("errorType", "NewDeviceChallengeRequired","deviceChallenge", e.getDetails().getDeviceChallenge(),"expiresAt", e.getDetails().getExpiresAt()));} catch (ValidateSessionException e) {System.err.println("Session validation failed: " + e.getMessage());return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));}}[HttpGet("/api/user-data")]public async Task<IActionResult> GetUserData(){var sessionToken = Request.Cookies["sessionToken"];var signedDeviceChallenge = Request.Headers["dpop"].ToString();try{var result = await client.Session.ValidateAsync(new ValidateSessionCommand{SessionToken = sessionToken,IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),UserAgent = Request.Headers["User-Agent"].ToString(),DeviceVerification = new DeviceVerification{SignedDeviceChallenge = signedDeviceChallenge}});return Ok(new { User = result.User });}catch (ValidateSessionException.NewDeviceChallengeRequired e){return StatusCode(425, new{ErrorType = "NewDeviceChallengeRequired",DeviceChallenge = e.Details.DeviceChallenge,ExpiresAt = e.Details.ExpiresAt});}catch (ValidateSessionException ex){Console.Error.WriteLine($"Session validation failed: {ex.Message}");return StatusCode(401, new { Error = "Unauthorized" });}}
Testing
Section titled “Testing”To verify session theft protection is working:
- Log in from one browser and save the session cookie
- Try using that cookie from a different browser or incognito window
- The request should be rejected with a
DeviceVerificationFailederror