> ## Documentation Index
> Fetch the complete documentation index at: https://stytch.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Remembered device

> A recipe to implement a remembered device flow with Stytch Device Fingerprinting

<Tip>
  You can see a hosted demo of this flow at our demo application [here](https://www.stytchdemo.com/recipes/remembered-device-integrated). On the [stytchdemo.com](https://www.stytchdemo.com) homepage, it is the "Remembered Device" recipe card. The relevant code is in GitHub [here](https://github.com/stytchauth/stytch-nextjs-integration).
</Tip>

In this guide we'll walk through how to use Stytch's Device Fingerprinting (DFP) product to implement a remembered device flow. Remembered device flows are a form of adaptive MFA, or triggering MFA only on unrecognized logins.

For this guide, we'll use the [Visitor ID](/fraud-risk/device-fingerprinting/fingerprints) as the known device identifier. Visitor ID is a cookie-based identifier, and is a good choice for allowlists because good actors don't often intentionally clear their cookies, though it can happen naturally - for example, if they use a different browser. Other good examples for trigger points are login attempts from a new IP address or a new country.

We'll be using a Stytch Consumer project in a NextJS application for the purposes of this guide, but the same basic steps apply to any auth implementation. You will:

<Steps>
  <Step title="Generate device fingerprints">
    Generate device fingerprints and signals from the frontend and submit them as part of the authentication request.
  </Step>

  <Step title="Evaluate device recognition">
    Evaluate whether this is a recognized device using either the Stytch Device Fingerprinting [Lookup response](/api-reference/fraud/api/fingerprint-lookup) or the `user_device` authentication response field, depending on which approach you choose in step 2, and Stytch [User metadata](/api-reference/consumer/api/resources/metadata).
  </Step>

  <Step title="Conditionally show data">
    Conditionally show super-secret data within the application if either:

    * The login request is from a known device, or
    * The request is from a new device and the user completes Step-up MFA.
  </Step>

  <Step title="Store context and update devices">
    Store this context in [Session custom claims](/consumer-auth/manage-sessions/custom-claims) via a claim called `authorized_for_secret_data` for future reference throughout your application, and update the Known Devices list.
  </Step>
</Steps>

## Before you start

In order to complete this guide, you'll need:

1. A Stytch project (either Consumer or B2B).
   * If you don't have one already, in the Dashboard, click on your existing project name in the top left corner of the Dashboard. Click Create Project, and then select B2B Authentication or Consumer Authentication.
2. Access to Device Fingerprinting. If you don't have access, you can [request a trial](https://offers.stytch.com/dfp-30-day-trial?utm%5Fsource=stytch%5Fdocs\&utm%5Fmedium=direct\&utm%5Fcontent=dfp%5F30%5Fday%5Ftrial).
3. A basic authentication flow on which to add adaptive MFA.
   * If you don't have one set up already, you can use one of our example apps. The snippets in this guide will follow [a recipe from our Next.js demo application](https://www.stytchdemo.com/recipes/remembered-device-integrated) (code [here](https://github.com/stytchauth/stytch-nextjs-integration) - look for "remembered device" files).
   * This guide uses Stytch for authentication, but any backend authentication implementation can work.
   * If you are using Stytch for authentication, this guide assumes you're using a Consumer project. The same steps would apply to B2B projects as well if you don't want to use Stytch's [built-in B2B MFA offerings](/multi-tenant-auth/authentication/mfa/overview).

## Step-by-step walkthrough

<Steps>
  <Step title="Generating signals">
    The first step of this guide is step 1 and 2 of the [Getting Started guide](/fraud-risk/getting-started/dfp-api#step-1%3A-add-the-device-fingerprinting-script-to-your-app).

    Add the Stytch telemetry script to your login page:

    ```js theme={null}
    <head>
        <script src="https://elements.stytch.com/telemetry.js"></script>
    </head>
    ```

    Add  a method or event handler to call the async function `GetTelemetryID()`, which gathers signals from the device and resolves to a `telemetry_id`.

    ```js theme={null}
    const authenticateWithTelemetry = async () => {

    // Get telemetry_id from the telemetry script
    let telemetryId: string | undefined;

    try {
      const publicToken = process.env.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN;
      telemetryId = await (window as any).GetTelemetryID({ publicToken });
    } catch (telemetryError) {
      console.warn('Could not get telemetry ID:', telemetryError);
    }
    ```

    When you call your backend authenticate endpoint, send the `telemetry_id` along as well.

    In the snippet below from the example app flow, we're using Email Magic Links as our authentication method, so the `telemetry_id` is generated and sent along in the request to authenticate the Magic Link token.

    ```js theme={null}
    // Call our authenticate API
    const response = await fetch('/api/authenticate_eml_remembered_device', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...(telemetryId && { 'X-Telemetry-ID': telemetryId }),
      },
      body: JSON.stringify({ token }),
    });
    ```
  </Step>

  <Step title="Authentication and remembered device evaluation">
    <Tabs>
      <Tab title="Stytch integrated approach">
        The Stytch integrated approach assumes you are using Stytch for authentication and Device Fingerprinting and relies on specific Stytch features. If you are not using Stytch for auth, switch to the Standalone DFP tab.

        Your backend endpoint will:

        1. Authenticate the Email Magic Link `token`.
        2. Get the Visitor ID from the `user_device` response.
        3. Compare this against the known devices list to determine whether this is a known device or not.
        4. Store the remembered device context in `session.custom_claims` for reference downstream.

        ```js theme={null}
        // First, authenticate the magic link token and include the telemetry_id
        let authenticateResponse = await stytchClient.magicLinks.authenticate({
          token: token,
          session_duration_minutes: 10080,
          telemetry_id: telemetryId,
        });
        ```

        You can grab the `visitor_id` from the `user_device` response field. Determine whether MFA is required based on whether the request is from a known device:

        * If known:
          * Set `authorized_for_secret_data: true` in the Session claims
          * Return super secret data already, or optionally return it elsewhere in your application, gated on `session.custom_claims.authorized_for_secret_data: true`
        * If not known:
          * Store the pending Visitor ID in session custom claims for reference later on (to add to the known devices list in the case of a successful MFA flow)
          * Set `authorized_for_secret_data: false` in the Session claims
          * Let the frontend know that MFA is required

        Note that the `user_device` in the response could be null if the fingerprint lookup failed for any reason, such as an invalid `telemetry_id`. If the `user_device` is null, you should fail closed and always require MFA for security reasons.

        ```js theme={null}
        // Function to determine if a device is known
        function isKnownDevice(visitorID: string, knownDevices: string[]) {
            return knownDevices.includes(visitorID);
        }

        // Get the list of already known devices and the visitor ID of this request
        const knownDevices = authenticateResponse.user.trusted_metadata?.known_devices || [];
        // Fail closed: if the `visitor_id` is not present in the response, require MFA
        const visitorID = authenticateResponse.user_device?.visitor_id ?? '';

        // Check if the Visitor ID is in the known devices list
        const knownDevice = isKnownDevice(visitorID, knownDevices) && visitorID !== '';

        // Set the session cookie in the response
        cookies.set('api_sms_remembered_device_session', authenticateResponse.session_token, {
          httpOnly: true,
          maxAge: 1000 * 60 * 30,
        });

        // Update the session
        if (knownDevice) {
          // Update session with custom claims to mark this session as authorized
          await stytchClient.sessions.authenticate({
            session_token: authenticateResponse.session_token,
            session_custom_claims: {
              authorized_for_secret_data: true,
              authorized_device: visitorID,
            },
          });
        } else {
          // Store pending visitorID in session custom claims for later retrieval during OTP auth
          await stytchClient.sessions.authenticate({
            session_token: authenticateResponse.session_token,
            session_custom_claims: {
              authorized_for_secret_data: false,
              pending_device: visitorID,
            },
          });
        }
        ```
      </Tab>

      <Tab title="Standalone DFP">
        Your backend endpoint will:

        1. Authenticate the Email Magic Link `token`.
        2. Make the DFP `/lookup` call to determine the Visitor ID.
        3. Compare this against the known devices list to determine whether this is a known device or not.
        4. Store the remembered device context in `session.custom_claims` for reference downstream.

        ```js theme={null}
        // First, authenticate the magic link token
        const authenticateResponse = await stytchClient.magicLinks.authenticate({
          token: token,
          session_duration_minutes: 10080,
        });

        // Get the list of already known devices:

        const knownDevices = authenticateResponse.user.trusted_metadata?.known_devices || [];
        ```

        Perform the `/lookup` call to check the Visitor ID for this login request:

        ```js theme={null}
        // Fail closed: require MFA if no telemetry ID provided
        if (!telemetryId) {
          console.log('No telemetry ID provided, requiring MFA (fail closed)');
          requiresMfa = true;
        } else {
          try {
            // Lookup the telemetry ID response to get the Visitor ID
            const fingerprintResponse = await stytchClient.fraud.fingerprint.lookup({
              telemetry_id: telemetryId,
            });

          // Logic for decisioning is in the following snippets

          } catch (err) {
            console.error('Error checking remembered device:', telemetryError);
            // Fail closed: if telemetry lookup fails, require MFA for security
            requiresMfa = true;
          }
        }
        ```

        In the block where you successfully get a lookup response, determine whether MFA is required based on whether the request is from a known device:

        * If the device is **known**:
          * Set `authorized_for_secret_data: true` in the Session claims
          * Return super secret data in the response
          * Or, optionally return super secret data elsewhere in your application, gated on `session.custom_claims.authorized_for_secret_data: true`
        * Otherwise, the device is **not known**:
          * Store the pending Visitor ID in session custom claims for reference later on (to add to the known devices list in the case of a successful MFA flow)
          * Set `authorized_for_secret_data: false` in the Session claims
          * Let the frontend know that MFA is required

        ```js theme={null}
        // Function to determine if a device is known
        function isKnownDevice(visitorID: string, knownDevices: string[]) {
          return knownDevices.includes(visitorID);
        }

        // Default to closed
        let requiresMfa = true;

            ... // Inside the try/catch from above:

            // Check if the Visitor ID is in the known devices list
            if (isKnownDevice(visitorID, knownDevices) && visitorID !== '') {
              console.log('Visitor ID is known, no MFA required');
              requiresMfa = false;
              // Update session with custom claims to mark this session as authorized
              await stytchClient.sessions.authenticate({
                session_token: authenticateResponse.session_token,
                session_custom_claims: {
                  authorized_for_secret_data: true,
                  authorized_device: visitorID,
                },
              });
            } else {
              console.log('Visitor ID is not known, MFA required');
              requiresMfa = true;
              // Store pending visitorID in session custom claims for later retrieval during OTP auth
              await stytchClient.sessions.authenticate({
                session_token: authenticateResponse.session_token,
                session_custom_claims: {
                  authorized_for_secret_data: false,
                  pending_device: visitorID,
                },
              });
            }
        ```
      </Tab>
    </Tabs>

    Finally, return a response to your frontend with the Session. In this example, we go ahead and include the super secret data in the response if MFA isn't required.

    ```js theme={null}
    return res.status(200).json({
      session_token: authenticateResponse.session_token,
      visitorID: visitorID,
      user_id: authenticateResponse.user_id,
      super_secret_data: knownDevice ? SUPER_SECRET_DATA.REMEMBERED_DEVICE : undefined,
    });
    ```
  </Step>

  <Step title="Conditional access and MFA enforcement">
    **Trusted Device** → Grant Access

    **Untrusted Device** → Prompt MFA

    Now your application enters the Conditional Access and MFA Enforcement step: Trusted devices receive access to the protected data; unrecognized devices must complete MFA before access is granted.

    Below are two different approaches - the first uses a dedicated backend endpoint, the other uses React's `getServerSideProps`:

    <Tabs>
      <Tab title="Backend">
        In our [integrated example application recipe](https://github.com/stytchauth/stytch-nextjs-integration/blob/main/pages/recipes/api-sms-remembered-device-integrated/profile.tsx), we implement that logic in [a backend endpoint](https://github.com/stytchauth/stytch-nextjs-integration/blob/main/pages/api/known_devices_integrated.ts) called `/api/known_devices_integrated`.

        ```js theme={null}
        export async function handler(req: NextApiRequest, res: NextApiResponse<ErrorData | SuccessData>) {
          if (req.method === 'POST') {
            // Lookup the session to get the user id
            const cookies = new Cookies(req, res);
            const storedSession = cookies.get('api_sms_remembered_device_session');
            if (!storedSession) {
              return res.status(400).json({ errorString: 'No session provided' });
            }
            const stytchClient = loadStytch();
            const { session, user } = await stytchClient.sessions.authenticate({
              session_token: storedSession,
            });
            const hasRegisteredPhone = user.phone_numbers.length > 0;

            const phoneNumber = user.phone_numbers[0]?.phone_number ?? '';

            // Server-side authorization check based on session authentication factors and custom claims
            const hasEmailFactor = session.authentication_factors.find((i: any) => i.delivery_method === 'email');
            const hasSmsFactor = session.authentication_factors.find((i: any) => i.delivery_method === 'sms');
            const visitorID = session.custom_claims?.authorized_device || session.custom_claims?.pending_device || 'no visitor ID';

            let superSecretData = null;
            let requiresMfa = true; // Default to requiring MFA unless session proves otherwise
            const isRememberedDevice = session.custom_claims?.authorized_for_secret_data;

            if (hasEmailFactor && hasSmsFactor) {
              // User has completed full MFA - authorized for super secret data
              superSecretData = SUPER_SECRET_DATA.FULL_MFA;
              requiresMfa = false;
            } else if (isRememberedDevice) {
              // User is in a remembered device location (authorized during EML auth via session claims)
              superSecretData = SUPER_SECRET_DATA.REMEMBERED_DEVICE;
              requiresMfa = false;
            } else {
              // User needs MFA - either no email factor or not in trusted location
              requiresMfa = true;
            }

            // Get the known devices for the user
            const deviceList = user.trusted_metadata?.known_devices || [];

            // Return success
            return res.status(200).json({
              deviceList,
              user,
              session,
              hasRegisteredPhone,
              phoneNumber,
              superSecretData,
              isRememberedDevice,
              requiresMfa,
              visitorID,
            });
          } else {
            return res.status(405).end();
          }
        }
        ```
      </Tab>

      <Tab title="React">
        In our [standalone example application recipe](https://github.com/stytchauth/stytch-nextjs-integration/blob/main/pages/recipes/api-sms-remembered-device/profile.tsx), we implement that logic in `getServerSideProps`. Notably, all of the variables used in the logic here cannot be changed or accessed by a user:

        ```js theme={null}
        export const getServerSideProps: GetServerSideProps = async (context) => {
          const cookies = new Cookies(context.req, context.res);
          const storedSession = cookies.get('api_sms_remembered_device_session');

          ...

            // Get the session data
            const { session } = await stytchClient.sessions.authenticate({ session_token: storedSession });

            let superSecretData = null;
            let isRememberedDevice = false;
            let requiresMfa = true; // Default to requiring MFA unless session proves otherwise
            let visitorID = '';

            // Server-side authorization check based on session authentication factors
            const hasEmailFactor = session.authentication_factors.find((i: any) => i.delivery_method === 'email');
            const hasSmsFactor = session.authentication_factors.find((i: any) => i.delivery_method === 'sms');

            if (hasEmailFactor && hasSmsFactor) {
              // User has completed full MFA - authorized for super secret data
              superSecretData = SUPER_SECRET_DATA.FULL_MFA;
              requiresMfa = false;
            } else if (hasEmailFactor && session.custom_claims?.authorized_for_secret_data) {
              // User is in a remembered device location (authorized during EML auth via session claims)
              superSecretData = SUPER_SECRET_DATA.REMEMBERED_DEVICE;
              isRememberedDevice = true;
              requiresMfa = false;
              visitorID = session.custom_claims.authorized_device as string || '';
            } else {
              // User needs MFA - either no email factor or not in trusted location
              requiresMfa = true;
              visitorID = session.custom_claims?.pending_device as string || '';
            }

           return {
              props: {
                user: JSON.parse(JSON.stringify(user)),
                session: JSON.parse(JSON.stringify(session)),
                ...
                superSecretData,
                isRememberedDevice,
                requiresMfa,
                visitorID,
              },
        }
        ```
      </Tab>
    </Tabs>

    Use this known device data to determine frontend behavior; for example, the React component in our example app looks like:

    ```js theme={null}
    // Fetch the known devices data from the endpoint above:
      useEffect(() => {
        const fetchKnownDevices = async () => {
          const resp = await fetch('/api/known_devices_integrated', { method: 'POST' });
          const data = await resp.json();
          if (data.errorString) {
            setError(data.errorString);
          } else {
            setDeviceResponse(data as SuccessData);
          }
        };
        fetchKnownDevices();
      }, []);

    ...

    <div style={styles.secretBox}>
      <h3>Super secret area</h3>
      {deviceResponse.superSecretData ? (
        <div>
          <p>{deviceResponse.superSecretData}</p>
          {deviceResponse.isRememberedDevice && (
            <p style={styles.rememberedDeviceNote}>
              🎉 <strong>Device remembered!</strong>
            </p>
          )}
        </div>
      ) : (
        <>
          <Image alt="Lock" src={lock} width={100} />
          <p>
            {deviceResponse.requiresMfa
              ? `Additional authentication required. This appears to be a new device (${deviceResponse.visitorID || 'unknown device'}). Please complete SMS verification to continue.`
              : 'Super secret profile information is secured by two factor authentication. To unlock this area complete the SMS OTP flow.'
            }
          </p>
          {deviceResponse.hasRegisteredPhone && deviceResponse.phoneNumber ? (
            <SMSOTPButton phoneNumber={deviceResponse.phoneNumber} />
          ) : (
            <SMSRegister />
          )}
        </>
      )}
    </div>
    ```
  </Step>

  <Step title="Update known devices list">
    If a user logs in from a new Visitor ID and successfully completes MFA, add that new visitor ID to the known devices list so they can skip MFA in subsequent logins.

    This should be done on your backend and require a Session with both factors (primary and MFA):

    ```js theme={null}
    export async function handler(req: NextApiRequest, res: NextApiResponse<ErrorData | SuccessData>) {
      const stytchClient = loadStytch();

      // Get session from cookie
      const cookies = new Cookies(req, res);
      const storedSession = cookies.get('api_sms_remembered_device_session');

      // Authenticate the session to get user ID and verify factors
      const { session } = await stytchClient.sessions.authenticate({ session_token: storedSession });

      // Verify that the session has both EML and SMS factors (proving MFA completion)
      const hasEmailFactor = session.authentication_factors.some(f => f.delivery_method === 'email');
      const hasSmsFactor = session.authentication_factors.some(f => f.delivery_method === 'sms');

      if (!hasEmailFactor || !hasSmsFactor) {
        return res.status(403).json({ errorString: 'MFA not completed. Both email and SMS factors required.' });
      }

      // Get the pending device from session custom claims that was stored during EML authentication
      const pendingDevice = session.custom_claims?.pending_device;

      // Get user to access existing trusted metadata
      const user = await stytchClient.users.get({ user_id: session.user_id });

      // Get existing known devices or initialize empty array
      const existingKnownDevices = user.trusted_metadata?.known_devices || [];

      // Add new device if it's not already in the list
      if (!existingKnownDevices.includes(pendingDevice)) {
        const updatedKnownDevices = [...existingKnownDevices, pendingDevice];

        // Update the user's trusted metadata and clean up pending device
        await stytchClient.users.update({
          user_id: session.user_id,
          trusted_metadata: {
            ...user.trusted_metadata,
            known_devices: updatedKnownDevices,
            pending_device: undefined, // Remove the pending device
          },
        });

    ...
    }
    ```
  </Step>
</Steps>

## What's next?

<Card title="Want to try Stytch Device Fingerprinting?" href="https://offers.stytch.com/dfp-30-day-trial?utm_source=stytch_docs&utm_medium=direct&utm_content=dfp_30_day_trial" cta="Start your trial ">
  Find out why Stytch's device intelligence is trusted by Calendly, Replit, and many more.
</Card>
