Skip to content

Authentication

The Layota REST API uses JWT tokens for user authentication and API keys for server-to-server access.

JWT Authentication

Register

http
POST /api/auth/register/
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securepassword",
  "password_confirm": "securepassword",
  "full_name": "John Doe",
  "organization_name": "My Company"
}

Response:

json
{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "fullName": "John Doe",
    "date_joined": "2026-04-12T10:00:00Z",
    "avatarUrl": null
  },
  "tokens": {
    "access": "eyJhbGci...",
    "refresh": "eyJhbGci..."
  }
}

Login

http
POST /api/auth/login/
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securepassword"
}

Response:

json
{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "fullName": "John Doe",
    "date_joined": "2026-04-12T10:00:00Z",
    "avatarUrl": "https://...",
    "emailVerified": true,
    "twoFactorEnabled": false,
    "hasPassword": true
  },
  "tokens": {
    "access": "eyJhbGci...",
    "refresh": "eyJhbGci..."
  }
}

If the account has two-factor authentication enabled, login (and Google sign-in) does not return tokens. Instead it returns a challenge — complete it via /auth/2fa/login-verify/:

json
{
  "two_factor_required": true,
  "pending_token": "g7c2..."
}

Sign in with Google

http
POST /api/auth/google/
Content-Type: application/json

{
  "code": "4/0AeanS0b...",
  "code_verifier": "dBjftJeZ4CVP..."
}

Exchanges a Google OAuth 2.0 authorization code for a Layota session. The app uses the standard authorization-code redirect flow with state and PKCE (S256): the current tab navigates to Google's auth endpoint (no prompt parameter, so returning users skip the consent screen), Google redirects back to the SPA's /auth/google/callback, and that page POSTs the one-time code plus the PKCE code_verifier here. The backend rebuilds the exchange redirect_uri from the request's Origin header (<origin>/auth/google/callback — each environment's origin must be registered as an Authorized redirect URI on the Google OAuth client), exchanges the code with Google, verifies the returned ID token (signature, audience, issuer, expiry), and then:

  • matches a returning user by their stable Google account id (sub), otherwise
  • links to an existing account with the same verified email, otherwise
  • creates a new account (email pre-verified, no password, personal organization created automatically).

On success the response is identical to Login:

json
{
  "user": { "id": "...", "email": "user@example.com", "fullName": "John Doe", "hasPassword": false },
  "tokens": { "access": "eyJhbGci...", "refresh": "eyJhbGci..." }
}

Notable responses:

  • 400 — missing code / code_verifier, or no Origin header on the request.
  • 401 — Google authentication failed, invalid token, or unverified Google email.
  • 403 — the user's organization membership is blocked.
  • 503 — Google sign-in is not configured on this server (GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET unset).

The user.hasPassword field is false for accounts that only ever signed in with Google (no usable password). Such users can set a password via the Password Reset flow to additionally enable email/password login.

Two-Factor Authentication (2FA)

2FA is TOTP-based (RFC 6238) — compatible with Google Authenticator, Authy, 1Password, etc. When enabled, the user must supply a 6-digit code (or a one-time backup code) after their password.

Enrollment (authenticated, two steps):

http
POST /api/auth/2fa/setup/
Authorization: Bearer <access>

Returns a provisional secret and the otpauth:// URI to render as a QR code. It is not active until confirmed:

json
{
  "secret": "JBSWY3DPEHPK3PXP",
  "otpauth_uri": "otpauth://totp/Layota:user@example.com?secret=...&issuer=Layota"
}
http
POST /api/auth/2fa/enable/
Authorization: Bearer <access>

{ "code": "123456" }

Confirms a code against the provisional secret, switches 2FA on, and returns one-time backup codes (shown only once):

json
{ "enabled": true, "backup_codes": ["4d6b4-5ff04", "2cddc-634ad", "..."] }

Login challenge — when a 2FA user signs in, /auth/login/ (or /auth/google/) returns { "two_factor_required": true, "pending_token": "..." } instead of tokens. Exchange the pending token + a code for a real session:

http
POST /api/auth/2fa/login-verify/

{ "pending_token": "g7c2...", "code": "123456" }

code accepts either the current TOTP or one of the user's backup codes (each backup code works once). On success the response is identical to Login. The pending token expires after 5 minutes and is burned after 5 wrong attempts.

Disable (authenticated) — requires a current code so a hijacked session can't silently remove the second factor:

http
POST /api/auth/2fa/disable/
Authorization: Bearer <access>

{ "code": "123456" }

Notable responses:

  • 400 — wrong/expired code, or enrollment not started.
  • 401 — wrong code at the login challenge.
  • 429 — too many attempts (throttled).

Refresh Token

http
POST /api/auth/token/refresh/
Content-Type: application/json

{
  "refresh": "eyJhbGci..."
}

Response:

json
{
  "access": "eyJhbGci..."
}

Logout

http
POST /api/auth/logout/
Content-Type: application/json

{
  "refresh": "eyJhbGci..."
}

Blacklists the provided refresh token so it can no longer be exchanged for new access tokens. Always returns 200 OK — missing / malformed / already-blacklisted tokens are treated as success so clients can safely clear local state in all cases.

json
{
  "detail": "Logged out."
}

Using the Token

Include the access token in the Authorization header:

http
GET /api/projects/
Authorization: Bearer eyJhbGci...

Get Current User

http
GET /api/auth/me/
Authorization: Bearer eyJhbGci...

Response:

json
{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "fullName": "John Doe",
    "date_joined": "2026-04-12T10:00:00Z",
    "avatarUrl": "https://...",
    "emailVerified": true,
    "twoFactorEnabled": false,
    "hasPassword": true
  }
}

Update Profile

http
PATCH /api/auth/me/
Authorization: Bearer eyJhbGci...
Content-Type: application/json

{
  "fullName": "Jane Doe"
}

Also accepts PUT for full replacement.

Delete Account

http
DELETE /api/auth/me/
Authorization: Bearer eyJhbGci...

Permanently deletes the user account and all associated data. If the user is the sole owner of an organization, the organization and all its projects are also deleted.

Upload Avatar

http
POST /api/auth/me/avatar/
Authorization: Bearer eyJhbGci...
Content-Type: multipart/form-data

avatar=<file>

Delete Avatar

http
DELETE /api/auth/me/avatar/
Authorization: Bearer eyJhbGci...

Change Password

http
POST /api/auth/change-password/
Authorization: Bearer eyJhbGci...
Content-Type: application/json

{
  "current_password": "oldpassword",
  "new_password": "newpassword",
  "new_password_confirm": "newpassword"
}

Response:

json
{
  "message": "Password changed successfully."
}

Set Password

For accounts created via Sign in with Google that have no password yet (user.hasPassword is false). Adds email/password login alongside Google. Unlike Change Password it does not require a current password, and it only works while the account is still passwordless.

http
POST /api/auth/set-password/
Authorization: Bearer eyJhbGci...
Content-Type: application/json

{
  "new_password": "newpassword",
  "new_password_confirm": "newpassword"
}

Response (the returned user now has hasPassword: true):

json
{
  "message": "Password set successfully.",
  "user": { "id": "...", "email": "user@example.com", "hasPassword": true }
}

Returns 400 if the account already has a password (use Change Password instead). The current session is not invalidated — there is no old credential to rotate.

Password Reset

Request a reset link:

http
POST /api/auth/password-reset/
Content-Type: application/json

{
  "email": "user@example.com"
}

Confirm the reset:

http
POST /api/auth/password-reset/confirm/
Content-Type: application/json

{
  "uid": "...",
  "token": "...",
  "new_password": "newpassword",
  "new_password_confirm": "newpassword"
}

Response:

json
{
  "detail": "Password has been reset successfully."
}

API Key Authentication

For server-to-server access, use API keys instead of JWT tokens. See API Keys.

http
GET /api/projects/
Authorization: Bearer sk_...

Error Responses

400 Bad Request

json
{
  "email": ["This field is required."],
  "password": ["This password is too common."]
}

401 Unauthorized

json
{
  "detail": "Authentication credentials were not provided."
}

403 Forbidden

json
{
  "detail": "You do not have permission to perform this action."
}

Layota Documentation