Guide

OAuth, social login and magic links

Get the token before the redirect, verify it in the callback.

How it works

  1. On the click, before redirecting to the provider, call execute(). The visitor is still on your page, so the browser checks run there.
  2. Carry the token through the redirect in a short-lived cookie, or in the OAuth state if it fits your provider's URL limit.
  3. In the callback, send it to /v1/verify with the provider's user id and email in account.
  4. Tokens expire after 5 minutes. A user who takes longer at the provider (2FA, a new device) comes back with expired: take the missing-token path, not a rejection.

You only learn in the callback whether the user is new, and the token action must match the verify action. So mint one signup and one login token on the click and send the matching one. A new GitHub user is a signup and counts toward your quota. A returning user is a login and is free. The unused token just expires.

Next.js: GitHub button

// app/login/github-button.tsx
'use client';

declare global {
  interface Window {
    Nolgard?: { ready: Promise<void>; execute(o: { action: string }): Promise<string> };
  }
}

// One token per possible outcome: you only learn new or returning in the callback.
async function saveTokens() {
  for (const action of ['signup', 'login']) {
    let token = '';
    try {
      if (window.Nolgard) {
        await window.Nolgard.ready;
        token = await window.Nolgard.execute({ action });
      }
    } catch {} // script blocked: no token, the callback takes the missing-token path
    document.cookie = `nf_${action}=${encodeURIComponent(token)}; Max-Age=300; Path=/; SameSite=Lax; Secure`;
  }
}

export function GitHubButton() {
  async function onClick() {
    await saveTokens(); // before the redirect, while the visitor is still on your page
    window.location.href = '/api/auth/github'; // your existing OAuth start route
  }
  return <button onClick={onClick}>Continue with GitHub</button>;
}

Load https://nolgard.com/v1/client.js on the page as in the Next.js guide. The button is always enabled: without the script the cookies are empty and the callback still runs.

Next.js: callback route

// app/api/auth/github/callback/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { createNolgard } from '@nolgard/node';

const nolgard = createNolgard({ secret: process.env.NOLGARD_SECRET! }); // failOpenShare defaults to 0.2

export async function GET(request: Request) {
  // Your existing code: check state, exchange the code, read the GitHub user and verified email.
  const { githubId, email } = await exchangeGitHubCode(request);
  const existing = await findUserByGitHubId(githubId);
  const action = existing ? 'login' : 'signup';

  const jar = await cookies();
  const token = decodeURIComponent(jar.get(`nf_${action}`)?.value ?? '');
  jar.delete('nf_signup');
  jar.delete('nf_login');

  const result = await nolgard.verify({
    token, // empty: missing-token
    action,
    remoteIp: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim(),
    account: { id: `github:${githubId}`, email },
  });

  if (result.error === 'invalid-secret' || result.error === 'invalid-input') {
    return NextResponse.redirect(new URL('/login?error=try-again', request.url)); // fail closed, alert yourself
  }
  if (!existing) {
    // missing-token or a stale token (expired after 5 minutes, replayed): not a bot verdict.
    // GitHub already verified the email, so give the reduced grant.
    const share = result.success ? result.grant.share : 0.2;
    if (result.success && (result.decision === 'block' || result.decision === 'shadow')) {
      return NextResponse.redirect(new URL('/login?error=signup-failed', request.url));
    }
    await createUser({ githubId, email, trialShare: share });
  } else if (result.decision === 'challenge') {
    return NextResponse.redirect(new URL('/login/confirm-email', request.url)); // your own step-up
  }
  await startSession(githubId);
  return NextResponse.redirect(new URL('/dashboard', request.url));
}

exchangeGitHubCode, findUserByGitHubId, createUser and startSession are your own functions. Use the same account id on every verify for that user, so logins link to the signup. With the Node SDK, a 400 such as missing-token comes back as a result with success: false, and timeouts or over-quota come back as decision: "limit" with share 0.2.

PHP

Same flow. Set the two cookies from the click handler above, then in the callback read the one that matches and pass it to nolgard_verify() from the PHP guide. An empty token returns missing-token.

// callback.php, after the code exchange
$action = $existing ? 'login' : 'signup';
$token = $_COOKIE["nf_$action"] ?? '';
setcookie('nf_signup', '', time() - 3600, '/');
setcookie('nf_login', '', time() - 3600, '/');
$result = nolgard_verify($token, $action, ['id' => "github:$githubId", 'email' => $email]);