OTP API audit
Updated: 2026-05-14
Scope
This audit covers the current mobile auth OTP flows and the Golden Lotus Shop API endpoints they call:
- Registration OTP: create pending account, resend registration OTP, verify registration OTP.
- Forgot PIN / reset PIN by OTP: request reset PIN OTP, verify OTP, set the new 6 digit PIN.
- Legacy forgot password: Vendure password-reset email flow.
Backend changes were pushed to origin/develop before this audit:
a0d5b3e Add mobile reset PIN OTP APIfd4a736 Notify customers when bookings are updated
Runtime status
Local Docker runtime was rebuilt and recreated for server and worker.
Checked endpoints:
GET http://127.0.0.1:38104/health-> 200GET https://web-golden.azns.vn/health-> 200GET http://127.0.0.1:38104/dashboard/-> 200GET https://web-golden.azns.vn/dashboard/-> 200
Runtime GraphQL checks:
requestLotusRegisterOtpreturnedok: true.requestLotusResetPinOtpreturnedok: true,providerConfigured: true.resetLotusPinWithOtpexists in the schema and rejects an invalid OTP with the expected invalid/expired OTP error.- Vendure core
requestPasswordResetreturnedSuccessfor a non-existing audit email.
API inventory
Registration OTP
Backend:
registerCustomerAccount(input: RegisterCustomerInput!)requestLotusRegisterOtp(input: LotusRequestRegisterOtpInput!)verifyLotusRegisterOtp(input: VerifyLotusRegisterOtpInput!)
Mobile:
authService.startRegistration()authService.requestRegisterOtp()authService.verifyRegisterOtp()
Notes:
- Initial registration uses Vendure
registerCustomerAccount, which triggers the registration email handler. - Resend uses
requestLotusRegisterOtp. - Verify uses
verifyLotusRegisterOtpand creates a Vendure shop session when successful.
Forgot PIN / reset PIN OTP
Backend:
requestLotusResetPinOtp(input: LotusRequestResetPinOtpInput!)resetLotusPinWithOtp(input: ResetLotusPinWithOtpInput!)
Mobile:
authService.requestResetPinOtp()authService.resetPinWithOtp()LoginScreenforgot PIN steps.
Notes:
- Request accepts
identifierand optionalchannel. - Complete reset accepts
identifier,otpCode,newPin, optionalchannel, optionalrememberMe. - Successful reset updates the native password/PIN and creates a Vendure shop session.
Legacy forgot password
Backend:
- Vendure core
requestPasswordReset(emailAddress: String!)
Mobile:
authService.forgotPassword()ForgotPasswordScreen
Notes:
- This is not the new OTP reset PIN flow.
- It sends a Vendure password reset email/link.
- It only supports email even though the screen currently validates email or phone.
Security and behavior
Current good points:
- OTP codes are stored as HMAC hashes, not plain text.
- OTP verification uses
timingSafeEqual. - OTP purpose is scoped:
REGISTER,LOGIN, andRESET_PIN. - OTP lookup is channel-aware.
- Old active OTPs for the same purpose/email are consumed when a new OTP is created.
- Requesting login/reset PIN OTP does not reveal whether the account exists.
- Reset PIN checks the resolved user id before accepting an OTP.
- New PIN is normalized to digits and must be exactly 6 digits.
- OTP TTL, resend cooldown, and max attempts are configurable via env.
Observed limits:
- Cooldown is email/purpose based, not IP/device based.
requestRegistrationOtpdelegates to Vendure verification-token refresh; the response is generic, but the mobile UI assumes provider availability after initial registration.- Email provider detection checks
EMAIL_DEV_MODEorEMAIL_TRANSPORT/EMAIL_PROVIDER, while Vendure email config defaults dev mode fromAPP_ENV. Keep these envs aligned in deployed environments.
Findings
-
ForgotPasswordScreenaccepts email or phone, but the backend call is VendurerequestPasswordReset(emailAddress).Impact: entering a phone can pass mobile validation but fail or behave incorrectly against the email-only API.
Suggested fix: either restrict this screen to email only, or route phone/PIN recovery through the reset PIN OTP flow.
-
forgotPasswordis a legacy password reset link flow, not OTP.Impact: the screen copy says "code", but the API is Vendure password-reset email/link.
Suggested fix: update copy or retire this route in favor of the in-login "Quen ma PIN" flow.
-
Mobile registration start hardcodes
providerConfigured: trueandexpiresInSeconds: 300afterregisterCustomerAccount.Impact: if email provider is disabled or misconfigured, the UI can tell the customer that OTP was sent even when delivery is unavailable.
Suggested fix: expose a backend registration-start wrapper that returns the same
LotusMobileOtpResult, or checklotusMobileAuthConfig/provider state before showing success. -
Forgot PIN OTP screen shows a static resend timer and has no resend action on that step.
Impact: customer cannot request a new reset PIN OTP from that screen after waiting.
Suggested fix: wire a real countdown and call
requestResetPinOtpagain when the resend action is enabled. -
Public OTP request endpoints should add IP/device throttling before heavy public traffic.
Impact: current cooldown protects per email/purpose, but not broad enumeration or spam attempts across identifiers.
Suggested fix: add app-level throttle middleware or a small OTP rate-limit table keyed by IP/device/channel.
Source references
Backend:
docker/vendure_backend/apps/server/src/plugins/golden-lotus/services/lotus-email-otp.service.tsdocker/vendure_backend/apps/server/src/plugins/golden-lotus/api/shop/lotus-mobile-core.resolver.tsdocker/vendure_backend/apps/server/src/plugins/golden-lotus/api/shop-api-extensions.tsdocker/vendure_backend/apps/server/src/plugins/golden-lotus/email/lotus-register-otp-email.handler.tsdocker/vendure_backend/apps/server/src/plugins/golden-lotus/email/lotus-reset-pin-otp-email.handler.tsdocker/vendure_backend/apps/server/static/email/templates/lotus-register-otp/body.hbsdocker/vendure_backend/apps/server/static/email/templates/lotus-reset-pin-otp/body.hbs
Mobile:
api/services.tsfeatures/auth/screens/LoginScreen.tsxfeatures/auth/screens/RegisterScreen.tsxfeatures/auth/screens/ForgotPasswordScreen.tsxfeatures/auth/hooks.ts