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:
- Initialize
fetchWithDeviceonce when your app loads - Use
fetchWithDeviceinstead of regularfetchfor authenticated requests - Check the
newDeviceDetectedflag 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:
- Privacy concerns - Users and regulations like GDPR increasingly view fingerprinting as invasive
- False positives - Browser updates or privacy tools can change fingerprints unexpectedly
- Limited security - Fingerprints can be spoofed or copied
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
[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" });
}
});
[Frontend] Install BYO Library
PropelAuth BYO comes with a JavaScript library to make adding device registration simple.
npm install @propelauth/byo-javascript
[Frontend] Initialize BYO Library
Initialize the library using the initFetchWithDevice function. You’ll include three options:
fetchChallenge: A function that fetches a device challenge from your backend.getChallengeFromFailedResponse: A function that extracts a device challenge from a failed response (we’ll add this later).fallbackBehavior: What to do if the browser doesn’t support the required features. You can choose to either “fail” (throw an error) or “fallback” (continue without device registration).
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",
});
[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 }),
});
[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" });
}
});
[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" });
}
[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:
- Update all your authenticated requests to use
fetchWithDeviceinstead offetch. See Session Theft Protection for more details.