Security & Privacy

Authentication

How sign-in works in BookYourPTO — login, JWT access and refresh tokens with rotation, force-password-change, the password policy, forgot/reset, and email verification.

Authentication

This page covers how you sign in, how your session stays alive, and the rules around passwords and email verification. It applies to every user; admins should read the Force password change and Password policy sections.

Logging in

Sign in with your email and password.

POST /api/auth/login
{ "email": "you@example.com", "password": "••••••••", "rememberMe": true }
FieldRequiredNotes
emailYesYour work email.
passwordYesYour account password.
rememberMeNoDefaults to true. Controls how long the session lasts (see below).

Account enumeration is prevented by design

A wrong password and a non-existent or deactivated account both return the same generic error: Invalid email or password. We intentionally return identical responses regardless of which case applies, so a sign-in attempt never reveals whether an email address is in use.

Why? If "wrong password" and "no such user" looked different, an attacker could harvest a list of valid emails. Identical responses close that hole — so a 401 here never tells you which half failed.

What happens on a successful login

  1. A seat-limit check runs. If the organization is over its plan's user cap, login is blocked with 403 — except for Executive accounts, which are exempt so an owner can always get in to fix billing.
  2. You receive an access token and a refresh token (on web, the refresh token is also set as an HttpOnly cookie).
  3. The submitted password is checked against known breached-password lists in the background. If it appears in a known breach, you get a security alert (sent occasionally, not on every login).

Every login attempt, success or failure, is recorded (see Audit & Monitoring). Repeated failures feed a failed-login-burst tripwire.

JWT tokens & rotation

TokenLifetimeStored where
Access tokenShort-livedHeld by the client; sent as Authorization: Bearer.
Refresh token (remember me)Longer-livedDatabase + HttpOnly cookie (web).
Refresh token (session only)Shorter-livedDatabase + HttpOnly cookie (web).

The access token's payload includes: userId, organizationId, role, email, emailVerified, onboardingCompleted, forcePasswordChange, and requiresTwoFactorSetup.

Refresh rotates everything

POST /api/auth/refresh
{ "refreshToken": "..." }

On every refresh, both tokens are rotated atomically — the old pair is deleted and a fresh pair issued in one transaction.

SituationResponseMeaning
Normal refreshNew access + refresh tokenOld pair is now invalid.
Reusing an already-rotated refresh tokenToken already rotated - use the new tokenTreated as token theft — use the latest token your client received.
Two refreshes racing409 Token refresh in progress - please retryAnother refresh is mid-flight; retry shortly.
Why rotation? If a refresh token leaks, it's only valid until the real client next refreshes — at which point the leaked copy becomes a "reused" token and is rejected as theft.

Force password change

Users created by an administrator are created with forcePasswordChange = true. Until they set their own password, they are blocked (403) on everything except auth, profile, settings, and security pages, and are redirected to set a password. Setting the new password clears the flag and issues fresh tokens.

Password policy

The policy is org-configurable. The defaults are:

RuleDefault
Minimum length8 characters (clamped to the 8–32 range)
Uppercase letterRequired
Lowercase letterRequired
NumberRequired
Special characterNot required
Breached-password checkNew passwords found in known breached-password lists are rejected

New passwords are checked against known breached-password lists; the password itself never leaves our systems.

Changing your password logs you out everywhere

When you change your password, all of your refresh tokens are deleted (signing you out on every device), then a fresh pair is issued for your current session. The new password must be different from your current one.

Forgot & reset password

POST /api/auth/forgot-password
{ "email": "you@example.com" }

This always returns the same generic message — it never reveals whether the email exists. Behind the scenes it issues two credentials, both hashed in the database and valid for a limited time:

  • a 6-digit code (handy for mobile entry), and
  • a long URL token (embedded in the email link).
POST /api/auth/reset-password
{ "token": "<long token>", "password": "NewPass1", "email": "you@example.com" }

You may supply either the 6-digit code (in which case email is required) or the long token. On success:

  • the password is updated,
  • your email is marked verified (the reset link doubles as proof you control the inbox — useful for first-time setup),
  • all refresh tokens are deleted, and
  • a confirmation email is sent.

Email verification

POST /api/auth/verify-email
{ "token": "..." }

Verification is idempotent — clicking the link twice is harmless. Until you verify, you're blocked (403) on most endpoints and sent to the verification screen. A new verification email can be requested again after a short wait.

Troubleshooting

ErrorCauseFix
401 Invalid email or passwordWrong credentials or a deactivated account (deliberately indistinguishable).Re-check your password; if it's correct, contact your administrator — the account may be deactivated.
403 Email verification requiredYour email isn't verified yet.Click the link in your verification email; resend after a short wait if needed.
403 Password change requiredYou were created with a forced password change.Set a new password when prompted.
403 over the seat limit (non-executive)The org has more users than its plan allows.An admin/executive must reduce users or upgrade the plan. See Plans.
Token already rotatedAn old refresh token was reused.Use the most recent token your client received; sign in again if unsure.
409 Token refresh in progressTwo refreshes ran at once.Retry the request.
429 with retryAfterRate limit hit (e.g. too many logins).Wait the indicated time, then retry.

The forgot/reset credentials are valid for a limited time — request a fresh one if it has expired.