Skip to content

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.

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.

Session theft protection extends device registration by verifying the device on every authenticated request, not just at login. This means:

  1. When a user logs in, their device is registered (covered in New Device Notifications)
  2. On every subsequent request, the device must prove its identity
  3. If an attacker steals the session cookie and tries to use it from a different device, the request is blocked

Since you’ve already set up device registration for new device notifications, you only need to:

  1. Replace all your authenticated fetch calls with fetchWithDevice. This automatically includes the device verification headers.

    // Before
    const response = await fetch("/api/user-data", {
    method: "GET",
    credentials: "include",
    });
    // After
    import { fetchWithDevice } from "@propelauth/byo-javascript";
    const response = await fetchWithDevice("/api/user-data", {
    method: "GET",
    credentials: "include",
    });
  2. Add Device Verification to Session Validation

    Section titled “Add Device Verification to Session Validation”

    Update your backend session validation to verify the device. The deviceVerification parameter requires the signed device challenge from the dpop header.

    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 data
    res.json({ user: result.data.user });
    } else if (result.error.type === "NewDeviceChallengeRequired") {
    // Return new challenge just like in login
    res.status(425).json({
    errorType: "NewDeviceChallengeRequired",
    deviceChallenge: result.error.details.deviceChallenge,
    expiresAt: result.error.details.expiresAt,
    });
    } else {
    // Session invalid or device verification failed
    res.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" });
    }
    }

To verify session theft protection is working:

  1. Log in from one browser and save the session cookie
  2. Try using that cookie from a different browser or incognito window
  3. The request should be rejected with a DeviceVerificationFailed error