Authentication API: registration, password login, and two-factor login with authenticator-app codes (TOTP).
Base URL: https://api.vkovalov.dev
When running the server locally, use http://localhost:8080 instead (or whichever PORT is configured).
Request and response bodies are JSON. Send Content-Type: application/json with every POST.
Every error has the same shape, with the HTTP status code carrying the meaning:
{ "error": "Invalid request payload" }
Protected endpoints need an access token in the Authorization header. The scheme is case-sensitive:
Authorization: Bearer <access_token>
"requires_2fa": false, it contains the access_token and you are done."requires_2fa": true, it contains a pre_auth_token instead. Ask the user for the 6-digit code from their authenticator app and call Verify 2FA to receive the access_token.| Token | Lifetime | Accepted by protected endpoints |
|---|---|---|
access_token | 24 hours | Yes |
pre_auth_token | 5 minutes | No (403) |
Both are JWTs signed with HS256. Their payload carries user_id, role, is_2fa and the standard exp, iat, nbf claims. The payload is signed, not encrypted, so a client can read user_id from it.
/auth/registerpublicCreates an account with the role user and returns the stored user. It does not log the user in.
| Field | Type | Required |
|---|---|---|
email | string | Yes |
password | string | Yes |
first_name | string | No |
last_name | string | No |
curl -X POST https://api.vkovalov.dev/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"correct horse battery","first_name":"Ada"}'
201{
"id": "3f1c2a9e-6f0b-4c1d-9a55-0d6c1f2b7e10",
"email": "ada@example.com",
"first_name": "Ada",
"role": "user",
"is_totp_enabled": false,
"is_active": true,
"created_at": "2026-10-02T09:30:00Z",
"updated_at": "2026-10-02T09:30:00Z"
}
| 201 | Account created. |
| 400 | Body is not valid JSON, or email or password is empty. |
| 409 | An account with this email already exists. |
| 500 | Unexpected server error. |
/auth/loginpublicChecks the email and password. The response has one of two shapes, depending on whether the account has 2FA enabled.
| Field | Type | Required |
|---|---|---|
email | string | Yes |
password | string | Yes |
curl -X POST https://api.vkovalov.dev/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"correct horse battery"}'
200, 2FA not enabled{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"requires_2fa": false,
"user": { "id": "3f1c2a9e-...", "email": "ada@example.com", "role": "user", ... }
}
200, 2FA enabled{
"requires_2fa": true,
"pre_auth_token": "eyJhbGciOiJIUzI1NiIs..."
}
| 200 | Password accepted. |
| 400 | Body is not valid JSON. |
| 401 | Unknown email or wrong password. The message is the same for both. |
| 500 | Unexpected server error. A deactivated account also returns this status. |
/auth/2fa/verifypublicSecond login step for accounts with 2FA enabled. Checks the 6-digit code and returns the access token.
| Field | Type | Required | Notes |
|---|---|---|---|
user_id | string (UUID) | Yes | The user_id claim of the pre_auth_token. |
code | string | Yes | Current 6-digit code from the authenticator app. |
curl -X POST https://api.vkovalov.dev/auth/2fa/verify \
-H "Content-Type: application/json" \
-d '{"user_id":"3f1c2a9e-6f0b-4c1d-9a55-0d6c1f2b7e10","code":"492817"}'
200{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"requires_2fa": false,
"user": { "id": "3f1c2a9e-...", "email": "ada@example.com", "role": "user", ... }
}
| 200 | Code accepted. |
| 400 | Body is not valid JSON, or user_id is not a UUID. |
| 401 | Wrong or expired code. |
| 500 | Unexpected server error. An unknown user_id and an account without 2FA configured also return this status. |
/protected/profileaccess tokenConfirms that the access token is valid and returns the ID of the user it belongs to.
curl https://api.vkovalov.dev/profile \
-H "Authorization: Bearer <access_token>"
200{
"message": "Access granted to protected route!",
"user_id": "3f1c2a9e-6f0b-4c1d-9a55-0d6c1f2b7e10"
}
| 200 | Token accepted. |
| 401 | Header missing, not in the form Bearer <token>, or the token is invalid or expired. |
| 403 | The token is a pre_auth_token; finish Verify 2FA first. |
Returned by Register, and inside the Login and Verify 2FA responses. Fields marked optional are left out when they have no value.
| Field | Type | Notes |
|---|---|---|
id | string (UUID) | |
email | string | |
first_name | string | Optional. |
last_name | string | Optional. |
avatar_url | string | Optional. |
role | string | user for accounts created through Register. |
is_totp_enabled | boolean | Whether login requires a 2FA code. |
is_active | boolean | Deactivated accounts cannot log in. |
last_login_at | string (RFC 3339) | Optional. |
created_at | string (RFC 3339) | |
updated_at | string (RFC 3339) |
The password hash and the 2FA secret are never included in any response.