OTP Authentication
OTP Authentication
Section titled “OTP Authentication”This page explains how one-time passwords (OTPs) work in the Payload service, which API endpoints are available, and how to test the flows through the Admin dashboard or Postman.
What OTP Does
Section titled “What OTP Does”The project uses an emailed, six-digit OTP for two user-facing flows:
- OTP login: a user requests a code, then verifies it to log in without entering a password.
- Password reset: a user requests a code, then submits it with a new password.
Important: OTP requests are only processed and sent if the user has already verified their email address. This prevents abuse and spam.
Both flows use the same stored OTP fields on the user record, so requesting a new code replaces the previous code. A code expires after 10 minutes and is cleared after it is successfully used.
User Flows
Section titled “User Flows”OTP Login
Section titled “OTP Login”- The user opens
/admin/loginand chooses Or login with OTP. - The user enters the email address associated with an account and requests an OTP.
- Payload generates a six-digit code, stores it with a 10-minute expiration, and emails it to the account address.
- The user enters the code in the login form.
- If the code is valid and has not expired, Payload clears the code and returns a login token. It also sets the
payload-tokenHTTP-only cookie, then the Admin UI navigates to/admin.
OTP login is an alternative to the existing password login; it does not replace it.
Password Reset
Section titled “Password Reset”- The user opens
/admin/forgot(or chooses the forgot-password link from the Admin login page). - The user enters their account email and requests an OTP.
- The user submits the emailed code and a new password.
- Payload validates the code and password, updates the password, and clears the OTP. The user then returns to the login page and signs in again.
The new password must be 8 to 64 characters and contain at least one uppercase letter, one lowercase letter, one number, and one symbol. Password reset does not automatically log the user in.
API Reference
Section titled “API Reference”The endpoints are registered on the users collection. For a local Payload server, set baseUrl to http://localhost:3000/api. The development deployment uses https://p27payload.benb.au/api.
| Method | Endpoint | Purpose | Request body |
|---|---|---|---|
POST | /users/request-otp | Creates and emails a new OTP for an existing, verified account. Used by both OTP login and password reset. | { "email": "[email protected]" } |
POST | /users/validate-otp | Verifies the OTP and starts an authenticated session. | { "email": "[email protected]", "otp_code": "123456" } |
POST | /users/reset-password | Verifies the OTP and sets a new password. | { "email": "[email protected]", "otp_code": "123456", "new_password": "NewPassword1!" } |
All OTP endpoints accept JSON with the Content-Type: application/json header. Email addresses are trimmed and converted to lowercase before lookup.
Common Responses
Section titled “Common Responses”| Endpoint | Success | Common errors |
|---|---|---|
/users/request-otp | 200 with a confirmation message. | 400 missing email; 403 email not verified; 404 no account for that email; 429 request limit reached; 500 server/email error. |
/users/validate-otp | 200 with a login message and token; also sets the payload-token cookie. | 400 missing email or code; 401 invalid, expired, or unknown email/code; 500 server error. |
/users/reset-password | 200 with a password-reset confirmation. | 400 missing fields or password does not meet requirements; 401 invalid or expired code; 429 too many reset attempts; 500 server error. |
The OTP value is delivered by email, not included in the request-OTP success response. A successful validation consumes the code, so the same code cannot be used for both login and password reset.
Testing in the Admin Dashboard
Section titled “Testing in the Admin Dashboard”Test OTP Login
Section titled “Test OTP Login”- Start the Payload application and open
http://localhost:3000/admin/login. - Choose Or login with OTP and enter the email for an existing user.
- Request the code and retrieve it from the mailbox.
- Enter the code and submit. A successful flow redirects to
/admin.
Test Password Reset
Section titled “Test Password Reset”- Open
http://localhost:3000/admin/forgot. - Enter the email for an existing user and request a code.
- Retrieve the code from the mailbox.
- Enter the code and a password that meets the requirements above.
- After the success message, sign in from
/admin/loginwith the new password.
OTP login and password reset use different screens, but they share the request-OTP endpoint. Request a fresh code for each test because successful use clears it.
Testing with Postman or third parties
Section titled “Testing with Postman or third parties”Before testing, start the Payload service and configure email delivery. If SMTP is configured, check the account mailbox. In development without SMTP configuration, Payload falls back to logging the email to the server terminal; this behavior is described in the Emails documentation. Use a test account and do not share OTP values or password-reset requests against a production account.
Create a Postman environment variable named baseUrl, for example:
http://localhost:3000/api
For the OTP login flow:
Send POST {{baseUrl}}/users/request-otp with this JSON body:
JSON{ "email": "[email protected]"}Retrieve the OTP from the email or the development server terminal.
Send POST {{baseUrl}}/users/validate-otp with the email and received code:
JSON{ "email": "[email protected]", "otp_code": "123456"}Confirm the response status is 200 and inspect the response for the login token. Postman can also retain the payload-token cookie for requests that need the authenticated session.
For the password-reset flow, request a new OTP, then send POST {{baseUrl}}/users/reset-password:
JSON{ "email": "[email protected]", "otp_code": "654321", "new_password": "NewPassword1!"}A 200 response confirms the password was changed. Sign in again with the new password to verify it. Do not reuse a code that was already accepted by the login or reset endpoint.
Expiration and Request LimitsOTPs are six-digit numeric codes and expire 10 minutes after being requested.
Requesting another OTP replaces the stored code and expiration for that user.
OTP login and password reset clear the stored code after successful use. Expired codes are rejected even if the cleanup script has not run.
OTP requests are limited to 3 requests per email and 10 requests per IP address in a 15-minute window.
Password-reset submissions are limited to 5 attempts per email in a 15-minute window.
The request and reset counters are currently held in process memory. They are not shared between separate server instances and reset when the process restarts.
There is a separate cleanup script for removing expired OTP values from user records. Its operation and testing belong in the dedicated scripts documentation, not in this workflow guide.
Implementation FilesEndpoint registration: frontend/payload/src/collections/Users.ts
OTP request and email delivery: frontend/payload/src/endpoints/requestOtp.ts
OTP login logic: frontend/payload/src/endpoints/validateOtp.ts
Password reset: frontend/payload/src/endpoints/resetPassword.ts
Admin OTP login UI: frontend/payload/src/components/AdminOTPLogin.tsx
Admin password-reset UI: frontend/payload/src/components/AdminOTPForgotPassword.tsx
Email adapter configuration: frontend/payload/src/payload.config.ts