Add an SMS OTP channel
Add an SMS OTP channel
The package ships an email OTP channel but no SMS SDK — SMS is a contract
only. To offer "text me a code", implement Contracts\OtpChannel around your
provider's client (Twilio, MessageBird, Vonage, an internal gateway — whatever you
already use) and register it under a channel key.
This keeps the dependency graph clean: the platform never pulls a provider SDK you might not use, and you keep full control of the transport.
1. Implement the channel
A channel receives an OtpDelivery — the recipient, the plaintext code (once), the
purpose, and the expiry — and delivers it. It must not persist or log the code.
namespace App\Otp;
use Cbox\Id\Otp\Contracts\OtpChannel;
use Cbox\Id\Otp\ValueObjects\OtpDelivery;
class SmsOtpChannel implements OtpChannel
{
public function __construct(private readonly \Twilio\Rest\Client $twilio) {}
public function deliver(OtpDelivery $delivery): void
{
$this->twilio->messages->create($delivery->recipient, [
'from' => config('services.twilio.from'),
'body' => "Your code is {$delivery->code}. "
."It expires in {$delivery->ttlMinutes()} minute(s).",
]);
}
}
The example names a provider only to be concrete. Any transport works — the platform depends on the
OtpChannelcontract, never on a specific SDK.
2. Register it under a channel key
Add the key to config/cbox-id.php. The registry is deny-by-default: only keys
listed here (with a class that implements OtpChannel) are accepted.
'otp' => [
'channels' => [
'email' => \Cbox\Id\Otp\Channels\EmailOtpChannel::class,
'sms' => \App\Otp\SmsOtpChannel::class,
],
],
Resolve any constructor dependencies (the Twilio client above) the usual way — a binding in a service provider, or type-hints the container can autowire.
3. Issue over the channel
$challenge = app(Cbox\Id\Otp\Contracts\OtpService::class)->issue(
purpose: 'login',
recipient: '+15551234567',
channel: 'sms',
ip: $request->ip(),
);
Verification is identical regardless of channel — you hold $challenge->id and
call verify().
Notes
- Cost & abuse. SMS costs money per message, which makes issuance a DoS and
billing target. The per-recipient issue rate limit (
cbox-id.otp.issue.*) is your first defence; tune it down for SMS if needed, and consider a separate, stricter purpose for SMS flows. - Trust. SMS is susceptible to SIM-swap and interception — see security/otp.md. Prefer it as a step-up/recovery option behind a phishing-resistant primary factor.
- Phone-number validation and formatting (E.164) are the host's job before
calling
issue(); the channel receives whatever recipient you pass.