New Device Notifications | PropelAuth BYO Documentation

New Device Notifications

You’ve probably received an email like this before:

New device notifications alert users when someone logs in from an unrecognized device. This helps users spot unauthorized access immediately and take action if needed.

How It Works

Device registration uses cryptographic keys to securely identify each device. When a user logs in from a new device, PropelAuth BYO automatically detects it and returns a newDeviceDetected flag. You can then notify the user about the new device login.

We provide a frontend library with a function fetchWithDevice that is a drop-in replacement for fetch, except it automatically handles device registration for you.

The process is simple:

  1. Initialize fetchWithDevice once when your app loads
  2. Use fetchWithDevice instead of regular fetch for authenticated requests
  3. Check the newDeviceDetected flag when creating sessions

PropelAuth BYO handles all the cryptographic complexity behind the scenes - generating keys, managing challenges, and verifying signatures.

Why Cryptographic Keys Over Fingerprinting?

Browser fingerprinting collects device attributes (user agent, screen resolution, installed fonts, etc.) to create a unique identifier. While it can work, it has significant drawbacks:

Our cryptographic approach is privacy-friendly, reliable, and provides stronger security. Plus, the same keys enable session theft protection for even better security.

Getting Started

  1. [Backend] Create a Device Challenge endpoint

Your frontend will need to fetch device challenges in order to prove its identity. Create a backend endpoint that uses the Create Device Challenge Command to generate a challenge.

app.get("/api/device-challenge", async (req: Request, res: Response) => {
    const response = await auth.session.device.createChallenge({
        ipAddress: req.socket.remoteAddress,
        userAgent: req.headers["user-agent"],
    });

if (response.ok) {
        res.status(200).json({
            deviceChallenge: response.data.deviceChallenge,
            expiresAt: response.data.expiresAt,
        });
    } else {
        console.error("Error creating device challenge:", response.error);
        return res.status(500).json({ error: "Failed to create device challenge" });
    }
});
  1. [Frontend] Install BYO Library

PropelAuth BYO comes with a JavaScript library to make adding device registration simple.

npm install @propelauth/byo-javascript
  1. [Frontend] Initialize BYO Library

Initialize the library using the initFetchWithDevice function. You’ll include three options:

import { initFetchWithDevice } from "@propelauth/byo-javascript";

initFetchWithDevice({
    fetchChallenge: async () => {
        const response = await fetch("/api/device-challenge");
        const jsonResponse = await response.json();
        return {
            deviceChallenge: jsonResponse.deviceChallenge,
            expiresAt: new Date(jsonResponse.expiresAt * 1000),
        };
    },
    getChallengeFromFailedResponse: async (response) => {
        return null;
    },
    fallbackBehavior: "fail",
});
  1. [Frontend] Update Fetch to Include Device Challenge

Update your login fetch request(s) to use the fetchWithDevice function. This will automatically include the required headers for device registration in the request.

import { fetchWithDevice } from "@propelauth/byo-javascript";

const response = await fetchWithDevice("/api/login", {
    method: "POST",
    body: JSON.stringify({ username, password }),
});
  1. [Backend] Add Device Registration to Session Creation

fetchWithDevice will automatically include a new header dpop in the request. You’ll want to grab that and add it to your Create Session call. This will register the device and return if the device has not been used previously by the user.

const getSignedDeviceChallenge = (req: Request): string | undefined => {
    const dpopHeader = req.headers["dpop"];
    if (!dpopHeader || typeof dpopHeader !== "string") {
        return undefined;
    }
    return dpopHeader;
};

app.post("/api/login", async (req: Request, res: Response) => {
    const signedDeviceChallenge = getSignedDeviceChallenge(req);
    if (!signedDeviceChallenge) {
        return res.status(401).json({ error: "Device challenge required" });
    }
    const result = await client.session.create({
        userId: user.id,
        ipAddress: req.socket.remoteAddress,
        userAgent: req.headers["user-agent"],
        deviceRegistration: {
            signedDeviceChallenge,
            rememberDevice: true,
        },
    });
    if (result.ok) {
        if (result.data.newDeviceDetected) {
            // send new device notification email
        }
        // Save session token in cookie and return success
    } else {
        res.status(500).json({ error: "Failed to create session" });
    }
});
  1. [Both] Handle Device Challenge Error

While rare, it’s possible that the device challenge could be invalid when you go to log the user in. This could happen, for example, if the user’s IP address changed between fetching the challenge and logging in. To account for this, you can check for the NewDeviceChallengeRequired error type, which will include a new device challenge that you can return to the frontend:

if (result.ok) {
    if (result.data.newDeviceDetected) {
        // send new device notification email
    }
} else if (result.error.type === "NewDeviceChallengeRequired") {
    res.status(425).json({
        errorType: "NewDeviceChallengeRequired",
        deviceChallenge: result.error.details.deviceChallenge,
        expiresAt: result.error.details.expiresAt,
    });
} else {
    res.status(500).json({ error: "Failed to create session" });
}
  1. [Backend] Add Session Theft Protection or Skip It

Now that you’ve added device registration to your login flow, BYO will expect all Validate Session calls to include device registration proof. This means you have two options: