# OAuth, social login and magic links | Nolgard

> Verify GitHub, Google and other social sign-ins and magic-link requests with Nolgard: token on click, check in the callback.

Source: https://nolgard.com/docs/oauth/
Last updated: 2026-10-25

[Docs](https://nolgard.com/docs/) / OAuth and magic links

Guide

# OAuth, social login and magic links

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

## How it works

- On the click, before redirecting to the provider, call `execute()`. The visitor is still on your page, so the browser checks run there.
- Carry the token through the redirect in a short-lived cookie, or in the OAuth `state` if it fits your provider's URL limit.
- In the callback, send it to `/v1/verify` with the provider's user id and email in `account`.
- 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](https://nolgard.com/docs/#errors) 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](https://nolgard.com/docs/nextjs/). 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](https://nolgard.com/docs/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.

## Magic links

Verify when the link is requested, not when it is clicked: that is when the browser is on your page. Send `account.email` so repeat accounts link by email. This also stops bots flooding your mail sender with link requests.

```
// app/api/magic-link/route.ts
import { NextResponse } from 'next/server';
import { createNolgard } from '@nolgard/node';

const nolgard = createNolgard({ secret: process.env.NOLGARD_SECRET! });

export async function POST(request: Request) {
  const { email, token } = await request.json(); // token from execute() on submit, or none
  const existing = await findUserByEmail(email);
  const result = await nolgard.verify({
    token: token ?? '',
    action: existing ? 'login' : 'signup',
    remoteIp: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim(),
    account: { email },
  });
  // Bots asking for links get the same answer and no email. Blocks are not billed.
  if (result.success && (result.decision === 'block' || result.decision === 'shadow')) {
    return NextResponse.json({ ok: true });
  }
  // missing-token: the link itself is the email confirmation; grant 0.2 when it is clicked.
  const share = result.success ? result.grant.share : 0.2;
  await sendMagicLink(email, { trialShare: existing ? undefined : share });
  return NextResponse.json({ ok: true });
}
```

On the form, call `execute()` on submit with the action that matches, the same way as the button above, and send no token if the script is blocked.

## 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](https://nolgard.com/docs/php/). 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]);
```

## Related

- [Login protection](https://nolgard.com/docs/login-protection/): challenge risky logins.
- [Warm up existing referrers](https://nolgard.com/docs/referral-abuse/#warm-up) with login verifies.
- [Supabase](https://nolgard.com/docs/supabase/) and [Clerk](https://nolgard.com/docs/clerk/) hosted auth.
