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’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).