OTP configuration
OTP behavior is controlled inconfig/auth.php under the otp key:
| Parameter | Default | Description |
|---|---|---|
AUTH_OTP_EXPIRY_MINUTES | 10 | Minutes before a code expires |
AUTH_OTP_MAX_REQUESTS_PER_HOUR | 3 | Generation requests allowed per identifier per hour |
AUTH_OTP_LENGTH | 6 | Digit length of the code |
AUTH_OTP_MAX_ATTEMPTS | 5 | Failed verifications before the OTP is invalidated |
AUTH_OTP_IDEMPOTENCY_TTL | 86400 | Seconds to retain idempotency records |
OTP login flow
Client requests an OTP
The user enters their email address or international phone number. The client sends a The
POST request to generate an OTP.identifier must be either a valid email address or an international phone number starting with + (E.164 format, e.g. +50499887766).The Idempotency-Key header (optional) prevents duplicate codes from being sent if the network retries the request. The key is cached for AUTH_OTP_IDEMPOTENCY_TTL seconds.Server generates and delivers the code
The application creates an
OtpEntity with a hashed code stored in the one_time_passwords table, then fires an OtpGenerated domain event. The SendOtpNotificationListener handles delivery via email or SMS depending on the identifier format.The response includes the expiry timestamp:User submits the code
The client prompts the user for the 6-digit code and submits it along with the UUID of the identifier returned from the OTP record (stored by the client after step 1).
Generate OTP endpoint
throttle:api.otp-generation, idempotency
Email address or E.164 phone number (
+504...). Maximum 255 characters.throttle:api.otp-generation middleware (route-level) and an in-controller check that enforces a maximum of 3 generation attempts per identifier per hour. Exceeding the limit returns a validation error with a cooldown time in minutes.
Verify OTP endpoint
throttle:api.verification
UUID of the OTP record, as stored after the generate call. Must be a valid UUID v4.
The 6-digit numeric code delivered to the user. Must be exactly 6 digits.
OtpEntity also tracks attempt counts and invalidates the code once max_attempts is reached.
Error responses
| Scenario | HTTP status | Error field |
|---|---|---|
| Invalid identifier format | 422 | identifier |
| Too many generate requests | 422 | identifier |
| OTP code is wrong | 422 | otp_code |
| OTP has expired | 422 | otp_code |
| Too many verify attempts | 422 | identifier |
Security: maintenance commands
Two Artisan commands handle OTP hygiene. Schedule both inroutes/console.php.
auth:clear-otps
Deletes OTP records that expired more than 1 hour ago:
auth:prune-idempotency
Prunes old idempotency records from the processed_commands table using a chunked “nibbling” strategy to avoid gap locks:
| Option | Default | Description |
|---|---|---|
--days | 30 | Retain records created within this many days |
--chunk | 1000 | Batch size per deletion cycle |