IDfier Developers

Next.js example

A complete Login with IDfier for the Next.js App Router, with the browser binding and the server-side result.

Next.js App Router, with the qrcode package for the QR image. Set IDFIER_CLIENT_ID and IDFIER_CLIENT_SECRET as server-only environment variables. Each step is explained in the integration guide.

Server: the IDfier client

// lib/idfier.ts (server only)
const IDFIER_API = "https://api.idfier.com/api/v3";

async function idfier<T>(path: string, body: unknown): Promise<T> {
  const response = await fetch(`${IDFIER_API}${path}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      clientid: process.env.IDFIER_CLIENT_ID!,
      clientsecret: process.env.IDFIER_CLIENT_SECRET!,
    },
    body: JSON.stringify(body),
    cache: "no-store",
  });
  const json = await response.json().catch(() => null);
  if (!response.ok) {
    throw new Error(`IDfier ${response.status}: ${json?.data?.error ?? "request failed"}`);
  }
  return json.data as T;
}

export type IdfierSession = { referenceId: string; qrReferenceSecret: string; createdAt: string };

export type IdfierResult = {
  // id is always there; the other fields depend on your scopes
  user: ({ id: string } & Record<string, unknown>) | null;
  session: {
    referenceId: string;
    status: "pending" | "started" | "approved" | "denied" | "error";
    method: "idfier" | "eudi_wallet" | null;
    expiresAt: string;
    createdAt: string;
  };
};

export const createLoginSession = () => idfier<IdfierSession>("/business/auth/request", {});
export const getLoginResult = (referenceId: string) =>
  idfier<IdfierResult>("/business/auth/result", { referenceId });

export const LOGIN_COOKIE = "idfier_login_ref";

Server: start, poll and finish

// app/api/idfier/start/route.ts
import { NextResponse } from "next/server";
import { createLoginSession, LOGIN_COOKIE } from "@/lib/idfier";

export async function POST() {
  const session = await createLoginSession();
  const response = NextResponse.json(session);
  // Only this browser may finish this login (see /finish)
  response.cookies.set(LOGIN_COOKIE, session.referenceId, {
    httpOnly: true,
    sameSite: "lax",
    secure: process.env.NODE_ENV === "production",
    path: "/api/idfier",
    maxAge: 60 * 10,
  });
  return response;
}
// app/api/idfier/status/route.ts: what the browser polls. Returns only the
// status; the customer's data stays on the server.
import { NextRequest, NextResponse } from "next/server";
import { getLoginResult, LOGIN_COOKIE } from "@/lib/idfier";

export async function GET(request: NextRequest) {
  const referenceId = request.cookies.get(LOGIN_COOKIE)?.value;
  if (!referenceId) return NextResponse.json({ status: "missing" }, { status: 400 });
  const { session } = await getLoginResult(referenceId);
  const expired = new Date(session.expiresAt) < new Date() && session.status !== "approved";
  return NextResponse.json({ status: expired ? "expired" : session.status });
}
// app/api/idfier/finish/route.ts: logs the customer in once approved
import { NextRequest, NextResponse } from "next/server";
import { getLoginResult, LOGIN_COOKIE } from "@/lib/idfier";

export async function POST(request: NextRequest) {
  const referenceId = request.cookies.get(LOGIN_COOKIE)?.value;
  if (!referenceId) {
    return NextResponse.json({ error: "Start the login again" }, { status: 400 });
  }
  // Read the result here, never from the browser
  const { user, session } = await getLoginResult(referenceId);
  if (session.status !== "approved" || !user) {
    return NextResponse.json({ error: "The login is not approved" }, { status: 401 });
  }

  // Find or create the customer by user.id, then start your own session.
  // await signIn(await upsertCustomer({ idfierId: user.id, name: user.fullname }));

  const response = NextResponse.json({ ok: true });
  response.cookies.delete({ name: LOGIN_COOKIE, path: "/api/idfier" });
  return response;
}

Browser: the QR code and polling

// components/idfier-login.tsx
"use client";

import QRCode from "qrcode";
import { useEffect, useState } from "react";

type Session = { referenceId: string; qrReferenceSecret: string; createdAt: string };

// The QR payload (see /docs/identity/login/qr-code)
async function payload(session: Session) {
  const qrTime = String(Math.floor((Date.now() - new Date(session.createdAt).getTime()) / 1000));
  const encoder = new TextEncoder();
  const keyBytes = await crypto.subtle.digest("SHA-256", encoder.encode(session.qrReferenceSecret));
  const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(qrTime)));
  const code = Array.from(mac, (byte) => byte.toString(16).padStart(2, "0")).join("");
  return `idfier.${session.referenceId}.${qrTime}.${code}`;
}

export function IdfierLogin({ onLoggedIn }: { onLoggedIn: () => void }) {
  const [session, setSession] = useState<Session | null>(null);
  const [qr, setQr] = useState<string>("");
  const [appLink, setAppLink] = useState<string>("");
  const [status, setStatus] = useState("starting");

  async function start() {
    setStatus("pending");
    setSession(await (await fetch("/api/idfier/start", { method: "POST" })).json());
  }

  useEffect(() => {
    void start();
  }, []);

  // Refresh the QR code every 2 seconds until it has been scanned
  useEffect(() => {
    if (!session || status !== "pending") return;
    const render = async () => {
      const data = await payload(session);
      setQr(await QRCode.toDataURL(`idfier://auth?authData=${encodeURIComponent(data)}`));
      setAppLink(`https://www.idfier.com/auth?authData=${encodeURIComponent(data)}`);
    };
    void render();
    const timer = setInterval(render, 2000);
    return () => clearInterval(timer);
  }, [session, status]);

  // Poll with backoff: 2 s, growing to 10 s
  useEffect(() => {
    if (!session) return;
    let delay = 2000;
    let timer: ReturnType<typeof setTimeout>;
    let stopped = false;
    const poll = async () => {
      const { status: next } = await (await fetch("/api/idfier/status")).json();
      if (stopped) return;
      setStatus(next);
      if (next === "approved") {
        const finished = await fetch("/api/idfier/finish", { method: "POST" });
        if (finished.ok) onLoggedIn();
        return;
      }
      if (next === "pending" || next === "started") {
        delay = Math.min(delay * 1.5, 10000);
        timer = setTimeout(poll, delay);
      }
    };
    timer = setTimeout(poll, delay);
    return () => {
      stopped = true;
      clearTimeout(timer);
    };
  }, [session, onLoggedIn]);

  if (status === "started") return <p>Scanned. Approve the login in the IDfier app.</p>;
  if (status === "denied" || status === "error" || status === "expired") {
    return <button onClick={start}>The login didn&rsquo;t complete. Try again</button>;
  }
  return (
    <div>
      {qr ? <img src={qr} alt="Scan with the IDfier app" width={240} height={240} /> : null}
      {/* On a phone: open the IDfier app directly */}
      {appLink ? <a href={appLink}>Open the IDfier app</a> : null}
    </div>
  );
}

crypto.subtle needs a secure context (https, or localhost in development).

On this page