Appearance
MFA and OTP
The account security module provides MFA challenge storage and OTP code handling. The package owns challenge/token infrastructure. The app owns MFA policy, method selection, and delivery providers.
Create an MFA Challenge
php
use Sopheak\JwtAuth\DTO\TokenContext;
use Sopheak\JwtAuth\Services\MfaChallengeBroker;
$challenge = app(MfaChallengeBroker::class)->create(
$user,
TokenContext::make()->scopes(['profile.read']),
);The challenge stores the token context that should be resumed after successful MFA.
Create an OTP Code
php
use Sopheak\JwtAuth\DTO\OtpDestination;
use Sopheak\JwtAuth\Services\OtpChallengeBroker;
$dispatch = app(OtpChallengeBroker::class)->createOtp(
$challenge,
new OtpDestination(
channel: 'email',
normalizedDestination: 'user@example.com',
maskedDestination: 'u***@example.com',
),
);Plaintext OTP codes are available only on the dispatch object. The database stores HMAC hashes.
Deliver OTP Codes
Bind OtpChannelSender in the application:
php
use Sopheak\JwtAuth\Contracts\OtpChannelSender;
$this->app->bind(OtpChannelSender::class, AppOtpSender::class);The app can send via email, SMS, voice, WhatsApp, or another channel.
To make a failed send abort the OTP instead of silently creating one, implement OtpDeliveryAwareSender as well and return OtpDeliveryResult::failure(...). The broker then deletes the MfaOtpCode it just created, dispatches OtpDeliveryFailed, and throws OtpDeliveryFailedException (HTTP 502). The parent MfaChallenge stays alive, so the client can retry delivery or select another factor. See the first-factor OTP guide for the full sender example.
Verify OTP Codes
php
$context = app(OtpChallengeBroker::class)->verifyOtp(
challengeId: $challenge->id,
code: $request->input('otp'),
);Verification:
- Rejects expired challenges.
- Rejects expired OTP rows.
- Increments failed attempts.
- Locks after max attempts.
- Marks the challenge completed.
- Returns the original
TokenContext.
See also First-Factor OTP