Two-Factor Authentication

TOTP-based 2FA as a seamless second login step. Users with 2FA enabled complete authentication in two API calls instead of one.

How it works

When a user with 2FA enabled calls POST /auth/login, AuthWorx responds with { "requires2FA": true } and sets a short-lived partial_token cookie (5 minutes). No access token is issued yet.

The client then prompts for a 6-digit TOTP code and calls POST /auth/verify-2fa. AuthWorx validates the code against the partial_token, and on success issues the full access_token and refresh_token cookies.

Two-step login flow
Client                          AuthWorx
  │                                │
  │── POST /auth/login ───────────▶│
  │◀── { requires2FA: true } ──────│  (sets partial_token cookie, 5 min)
  │                                │
  │  [user opens authenticator app] │
  │                                │
  │── POST /auth/verify-2fa ───────▶│  (partial_token cookie sent automatically)
  │◀── { accessToken, ... } ───────│  (sets access_token + refresh_token cookies)
ℹ️
The partial_token cookie is httpOnly — the client never reads it directly. It is sent automatically with the verify request via the browser's cookie jar.

Verify a 2FA code

POST /auth/verify-2fa

Request body

FieldTypeDescription
code string required 6-digit TOTP code from the user's authenticator app.

Cookies required

CookieDescription
partial_token Set by POST /auth/login when 2FA is required. Expires in 5 minutes.
Request
curl -X POST https://authworx.uthings.io/api/v1/auth/verify-2fa \
  -H "X-Org-Slug: acme-corp" \
  -H "Content-Type: application/json" \
  -b "partial_token=eyJ..." \
  -d '{ "code": "482031" }'
Response — success (200)
{
  "status": "success",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "user": {
      "id": "usr_01HXYZ",
      "name": "Alice Chen",
      "email": "alice@acme.com",
      "role": "user",
      "isEmailVerified": true
    },
    "membership": {
      "role": "member",
      "status": "active"
    }
  }
}

Error responses

StatusCause
400Invalid or expired partial_token cookie, or missing code field.
401TOTP code is incorrect. User may retry until the partial_token expires.
429Too many incorrect codes. Wait before retrying.

Implementation example (Next.js)

This is how the included demo handles the two-step flow:

app/api/auth/2fa/route.ts
import { cookies } from 'next/headers';
import { authworxVerify2FA } from '@/lib/authworx';
import { setAuthCookies } from '@/lib/cookies';

export async function POST(req: Request) {
  const { code } = await req.json();
  const jar = await cookies();
  const partialToken = jar.get('partial_token')?.value;

  if (!partialToken) {
    return Response.json({ message: '2FA session expired. Please sign in again.' }, { status: 400 });
  }

  const { data, error } = await authworxVerify2FA(code, partialToken);
  if (error) return Response.json({ message: error.message }, { status: 401 });

  setAuthCookies(jar, data.accessToken, data.refreshToken);
  jar.delete('partial_token');
  return Response.json({ status: 'success' });
}
💡
After a successful 2FA verification, delete the partial_token cookie immediately — it has served its purpose and should not linger.