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
| Field | Type | Description | |
|---|---|---|---|
| code | string | required | 6-digit TOTP code from the user's authenticator app. |
Cookies required
| Cookie | Description |
|---|---|
| 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
| Status | Cause |
|---|---|
| 400 | Invalid or expired partial_token cookie, or missing code field. |
| 401 | TOTP code is incorrect. User may retry until the partial_token expires. |
| 429 | Too 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.