Documentation
¶
Overview ¶
Package sms provides phone-based OTP (One-Time Password) verification and authentication.
This plugin enables:
- Phone number verification via SMS OTP codes
- Phone+password registration and login
- Multi-factor authentication (MFA) via SMS
- Password reset via phone verification
OTP Flow:
- User requests OTP via SendOTP endpoint (requires authentication)
- Plugin generates 6-digit code (configurable length)
- Code sent via configured SMS provider (Twilio, AWS SNS, etc.)
- Code stored in database with expiry (default: 10 minutes)
- User submits code via VerifyOTP endpoint
- Plugin validates code and marks phone as verified
Phone+Password Flow:
- User registers with phone+password via /register endpoint
- Password hashed with bcrypt and stored
- User can login with phone+password via /login endpoint
- Session created on successful authentication
Route Structure:
- POST /sms/send - Send SMS OTP code (protected)
- POST /sms/verify - Verify SMS OTP code (public)
- POST /sms/login - Login with phone+password (public)
- POST /sms/register - Register with phone+password (public)
Provider Integration: Implement the Provider interface to use your SMS service:
type MyTwilioProvider struct { accountSID, authToken, from string }
func (p *MyTwilioProvider) SendOTP(to, code string) error {
// Send SMS with OTP code via Twilio API
}
Index ¶
- Constants
- func GetMigrations(dialect plugins.Dialect) ([]plugins.Migration, error)
- func GetSchema(dialect plugins.Dialect) (*plugins.Schema, error)
- func ValidatePhoneNumber(phone string) error
- type Config
- type DefaultSMSStore
- type Handlers
- func (h *Handlers) LoginWithPhoneHandler(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) RegisterWithPhoneHandler(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) SendOTPHandler(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) VerifyOTPHandler(w http.ResponseWriter, r *http.Request)
- type LoginWithPhoneRequest
- type Plugin
- func (p *Plugin) CreateUserWithPhoneAndPassword(ctx context.Context, name, phone, password string) (*User, error)
- func (p *Plugin) Dependencies() []plugins.Dependency
- func (p *Plugin) Description() string
- func (p *Plugin) GetMigrations() []plugins.Migration
- func (p *Plugin) GetSchemas() []plugins.Schema
- func (p *Plugin) GetUserByPhone(ctx context.Context, phone string) (*auth.User, error)
- func (p *Plugin) Init(_ context.Context, a plugins.Aegis) error
- func (p *Plugin) MountRoutes(router router.Router, prefix string)
- func (p *Plugin) Name() string
- func (p *Plugin) ProvidesAuthMethods() []string
- func (p *Plugin) RequiresTables() []string
- func (p *Plugin) SendOTP(ctx context.Context, phoneNumber, purpose string) error
- func (p *Plugin) VerifyOTP(ctx context.Context, phoneNumber, code string) (bool, error)
- func (p *Plugin) Version() string
- type Provider
- type RegisterWithPhoneRequest
- type SendOTPRequest
- type Store
- type User
- type VerifyOTPRequest
Constants ¶
const ( // Request schemas SchemaLoginWithPhoneRequest = "LoginWithPhoneRequest" SchemaRegisterWithPhoneRequest = "RegisterWithPhoneRequest" SchemaSendOTPRequest = "SendOTPRequest" SchemaVerifyOTPRequest = "VerifyOTPRequest" )
Schema names for OpenAPI specification generation. These constants define the OpenAPI schema names for SMS request/response types. They are used in route metadata to generate accurate API documentation with typed request/response examples.
Variables ¶
This section is empty.
Functions ¶
func GetMigrations ¶
GetMigrations returns all database migrations for the SMS plugin.
This function loads migrations from embedded SQL files and returns them in version order. The initial schema is always treated as version 001.
Version Numbering:
- Version 001: Initial schema from internal/sql/<dialect>/schema.sql
- Version 002+: Additional migrations from migrations/<dialect>/<version>_<description>.<up|down>.sql
Migration File Format:
- Up migration: 002_add_phone_verification.up.sql
- Down migration: 002_add_phone_verification.down.sql
Parameters:
- dialect: Database dialect (postgres, mysql, sqlite)
Returns:
- []plugins.Migration: Sorted list of migrations (oldest first)
- error: If schema files cannot be read or parsed
func GetSchema ¶
GetSchema returns the database schema for the SMS plugin.
The schema extends the 'user' table with phone-specific columns:
- phone_number (VARCHAR, unique): User phone number in E.164 format
- phone_verified (BOOLEAN): Phone verification status (default: false)
These extensions enable phone+password authentication and phone verification.
Parameters:
- dialect: Database dialect (postgres, mysql)
Returns:
- *plugins.Schema: Schema definition with SQL DDL
- error: If dialect is not supported
func ValidatePhoneNumber ¶
ValidatePhoneNumber validates a phone number format using libphonenumber.
This function performs comprehensive phone number validation:
- Basic format check (regex)
- International format validation (E.164)
- Country code verification
- Number length validation
Parameters:
- phone: Phone number to validate (can include country code prefix)
Returns:
- error: If phone number is empty or invalid
Example:
err := ValidatePhoneNumber("+14155551234") // Valid US number
err := ValidatePhoneNumber("+442071838750") // Valid UK number
Types ¶
type Config ¶
type Config struct {
Provider Provider // SMS sending provider (required for production)
OTPExpiry time.Duration // OTP expiry duration (default: 10 minutes)
OTPLength int // OTP code length (default: 6)
}
Config holds SMS plugin configuration.
Example:
cfg := &sms.Config{
Provider: myTwilioProvider,
OTPExpiry: 15 * time.Minute,
OTPLength: 8,
}
type DefaultSMSStore ¶
type DefaultSMSStore struct {
// contains filtered or unexported fields
}
DefaultSMSStore implements SMSStore using a SQL database.
This implementation uses sqlc-generated type-safe queries to manage users with phone numbers in PostgreSQL, MySQL, or SQLite.
Database Schema: Extends the 'user' table with:
- phone_number (VARCHAR, unique): User phone number in E.164 format
- phone_verified (BOOLEAN): Phone verification status
Thread Safety: Safe for concurrent use through database transactions.
func NewDefaultSMSStore ¶
func NewDefaultSMSStore(db *sql.DB) *DefaultSMSStore
NewDefaultSMSStore creates a new DefaultSMSStore backed by SQL.
The provided database connection must have the SMS schema applied.
Parameters:
- db: Active SQL database connection
Returns:
- *DefaultSMSStore: Configured store ready for use
func (*DefaultSMSStore) CreateUser ¶
CreateUser creates a new user with phone number.
func (*DefaultSMSStore) GetUserByPhone ¶
GetUserByPhone retrieves a user by phone number
func (*DefaultSMSStore) UpdateUserPhone ¶
func (s *DefaultSMSStore) UpdateUserPhone(ctx context.Context, userID, phone string, verified bool) error
UpdateUserPhone updates a user's phone number and verification status
type Handlers ¶
type Handlers struct {
// contains filtered or unexported fields
}
Handlers encapsulates SMS plugin HTTP handlers.
func NewHandlers ¶
NewHandlers creates SMS plugin handlers.
Parameters:
- plugin: Initialized SMS plugin
Returns:
- *Handlers: Handler instance ready for route registration
func (*Handlers) LoginWithPhoneHandler ¶
func (h *Handlers) LoginWithPhoneHandler(w http.ResponseWriter, r *http.Request)
LoginWithPhoneHandler handles phone+password login.
Authentication Flow:
- Validate phone number format (E.164)
- Retrieve user by phone number
- Verify password with bcrypt
- Create session
- Set session cookie
Endpoint:
- Method: POST
- Path: /sms/login
- Auth: Public
Request Body:
{
"phoneNumber": "+14155551234",
"password": "SecurePassword123!"
}
Response (200 OK):
{
"success": true,
"message": "Login successful",
"data": {
"user": {"id": "user_123", "phone": "+14155551234", ...}
}
}
func (*Handlers) RegisterWithPhoneHandler ¶
func (h *Handlers) RegisterWithPhoneHandler(w http.ResponseWriter, r *http.Request)
RegisterWithPhoneHandler handles phone+password registration.
Registration Flow:
- Validate phone number format (E.164)
- Check if phone already exists
- Create user with hashed password
- Create session (auto-login)
- Set session cookie
Endpoint:
- Method: POST
- Path: /sms/register
- Auth: Public
Request Body:
{
"name": "John Doe",
"phoneNumber": "+14155551234",
"password": "SecurePassword123!",
"avatar": "https://example.com/avatar.jpg" // Optional
}
Response (201 Created):
{
"success": true,
"message": "Registration successful",
"data": {
"user": {"id": "user_123", "phone": "+14155551234", "phoneVerified": false}
}
}
func (*Handlers) SendOTPHandler ¶
func (h *Handlers) SendOTPHandler(w http.ResponseWriter, r *http.Request)
SendOTPHandler handles sending OTP via SMS.
This endpoint is protected to prevent spam/abuse and SMS cost attacks. Only authenticated users can request OTP codes.
Endpoint:
- Method: POST
- Path: /sms/send
- Auth: Required (session)
Request Body:
{
"phoneNumber": "+14155551234",
"userId": "user_123",
"purpose": "phone_verification" // or "password_reset", "login_mfa"
}
Response (200 OK):
{
"success": true,
"message": "OTP sent successfully"
}
func (*Handlers) VerifyOTPHandler ¶
func (h *Handlers) VerifyOTPHandler(w http.ResponseWriter, r *http.Request)
VerifyOTPHandler handles verifying SMS OTP codes.
This endpoint is public to allow users to verify their phone numbers without requiring prior authentication.
Endpoint:
- Method: POST
- Path: /sms/verify
- Auth: Public
Request Body:
{
"phoneNumber": "+14155551234",
"code": "123456",
"purpose": "phone_verification"
}
Response (200 OK):
{
"success": true,
"message": "OTP verified successfully"
}
Response (400 Bad Request):
{
"success": false,
"error": "Invalid or expired OTP"
}
type LoginWithPhoneRequest ¶
type LoginWithPhoneRequest struct {
PhoneNumber string `json:"phoneNumber"` // Phone number in E.164 format
Password string `json:"password"` // User password
}
LoginWithPhoneRequest represents phone+password login request.
Example:
{
"phoneNumber": "+14155551234",
"password": "SecurePassword123!"
}
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin provides phone-based OTP verification and authentication.
This plugin integrates with SMS service providers to send OTP codes and supports phone+password authentication as an alternative auth method.
Features:
- Configurable OTP length and expiry
- Pluggable SMS providers (Twilio, AWS SNS, Vonage, etc.)
- International phone number validation (E.164 format)
- Password hashing with bcrypt
- Session management integration
func New ¶
New creates a new SMS plugin instance.
Parameters:
- cfg: Plugin configuration (can be nil for defaults)
- store: Custom SMSStore implementation (can be nil, will use DefaultSMSStore)
- dialect: Database dialect (optional, defaults to PostgreSQL)
Returns:
- *Plugin: Configured plugin ready for initialization
Example:
plugin := sms.New(&sms.Config{
Provider: myTwilioProvider,
OTPExpiry: 15 * time.Minute,
OTPLength: 8,
}, nil, plugins.DialectPostgres)
func (*Plugin) CreateUserWithPhoneAndPassword ¶
func (p *Plugin) CreateUserWithPhoneAndPassword(ctx context.Context, name, phone, password string) (*User, error)
CreateUserWithPhoneAndPassword creates a new user with name, password account, and phone number.
func (*Plugin) Dependencies ¶
func (p *Plugin) Dependencies() []plugins.Dependency
Dependencies returns external package dependencies
func (*Plugin) Description ¶
Description returns a human-readable description for logging.
func (*Plugin) GetMigrations ¶
GetMigrations returns the plugin migrations
func (*Plugin) GetSchemas ¶
GetSchemas returns all schemas for all supported dialects
func (*Plugin) GetUserByPhone ¶
GetUserByPhone retrieves a user by phone number
func (*Plugin) MountRoutes ¶
MountRoutes registers HTTP routes for the SMS plugin
func (*Plugin) ProvidesAuthMethods ¶
ProvidesAuthMethods returns authentication methods provided
func (*Plugin) RequiresTables ¶
RequiresTables returns core tables this plugin depends on
type Provider ¶
type Provider interface {
// SendOTP sends a one-time password to the specified phone number.
//
// Parameters:
// - to: Recipient phone number in E.164 format (e.g., "+14155551234")
// - code: OTP code to send (e.g., "123456")
//
// Returns:
// - error: If SMS sending fails
SendOTP(to, code string) error
// VerifyOTP verifies an OTP code for a phone number.
//
// Note: Most implementations delegate verification to the plugin's OTP storage.
// This method is available for provider-specific validation if needed.
//
// Parameters:
// - to: Phone number to verify
// - code: OTP code to verify
//
// Returns:
// - bool: true if OTP is valid
// - error: If verification fails
VerifyOTP(to, code string) (bool, error)
}
Provider is the interface that SMS service providers must implement.
Users should create their own implementation based on their SMS service (Twilio, AWS SNS, Vonage, MessageBird, Plivo, etc.).
Abstraction Benefits:
- Swap SMS providers without changing plugin code
- Test with mock providers
- Use different providers for different regions
Example Implementation (Twilio):
type TwilioProvider struct {
accountSID string
authToken string
from string // Twilio phone number
}
func (p *TwilioProvider) SendOTP(to, code string) error {
msgData := url.Values{}
msgData.Set("To", to)
msgData.Set("From", p.from)
msgData.Set("Body", fmt.Sprintf("Your verification code is: %s", code))
urlStr := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", p.accountSID)
req, _ := http.NewRequest("POST", urlStr, strings.NewReader(msgData.Encode()))
req.SetBasicAuth(p.accountSID, p.authToken)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{}
resp, err := client.Do(req)
return err
}
func (p *TwilioProvider) VerifyOTP(to, code string) (bool, error) {
return true, nil // Verification handled by plugin
}
Example Implementation (AWS SNS):
type SNSProvider struct {
client *sns.Client
}
func (p *SNSProvider) SendOTP(to, code string) error {
_, err := p.client.Publish(context.TODO(), &sns.PublishInput{
PhoneNumber: aws.String(to),
Message: aws.String(fmt.Sprintf("Your OTP: %s", code)),
})
return err
}
type RegisterWithPhoneRequest ¶
type RegisterWithPhoneRequest struct {
Avatar *string `json:"avatar"` // Optional avatar URL
Name *string `json:"name"` // User display name (required)
PhoneNumber string `json:"phoneNumber"` // Phone number in E.164 format (required)
Password string `json:"password"` // User password (required)
}
RegisterWithPhoneRequest represents phone+password registration request.
Example:
{
"name": "John Doe",
"phoneNumber": "+14155551234",
"password": "SecurePassword123!",
"avatar": "https://example.com/avatar.jpg"
}
type SendOTPRequest ¶
type SendOTPRequest struct {
PhoneNumber string `json:"phoneNumber"` // Phone number to send OTP to
UserID string `json:"userId"` // User ID requesting OTP
Purpose string `json:"purpose"` // OTP purpose ("phone_verification", "password_reset", "login_mfa")
}
SendOTPRequest represents the request to send an OTP code.
Example:
{
"phoneNumber": "+14155551234",
"userId": "user_123",
"purpose": "phone_verification"
}
type Store ¶
type Store interface {
// CreateUser creates a new user with a phone number.
//
// The phone is initially unverified (phoneVerified: false).
//
// Parameters:
// - ctx: Request context
// - user: User with phone field populated in E.164 format
//
// Returns:
// - *User: Created user
// - error: If user creation fails (e.g., duplicate phone)
CreateUser(ctx context.Context, user User) (*User, error)
// GetUserByPhone retrieves a user by phone number.
//
// Used for:
// - Phone+password login
// - Checking if phone already exists during registration
// - Phone verification lookup
//
// Parameters:
// - ctx: Request context
// - phone: Phone number to lookup (E.164 format)
//
// Returns:
// - *User: User with matching phone
// - error: If user not found or database error
GetUserByPhone(ctx context.Context, phone string) (*User, error)
// UpdateUserPhone updates a user's phone number and verification status.
//
// Used after successful OTP verification to mark phone as verified.
//
// Parameters:
// - ctx: Request context
// - userID: User ID
// - phone: New phone number (E.164 format)
// - verified: Phone verification status
//
// Returns:
// - error: If update fails
UpdateUserPhone(ctx context.Context, userID, phone string, verified bool) error
}
Store defines the interface for SMS OTP verification storage operations.
This interface provides phone-specific user management operations:
- User creation with phone numbers
- Phone lookup for authentication
- Phone verification status updates
Thread Safety: Implementations must be safe for concurrent use.
type User ¶
type User struct {
auth.User
Phone *string `json:"phone,omitempty"` // User phone number in E.164 format
PhoneVerified bool `json:"phoneVerified"` // Phone verification status
}
User extends the core User model with phone-specific fields.
This model adds phone verification status for display in API responses.
Use this when:
- Returning user data after phone registration
- Displaying phone verification status in user profiles
- Checking if user has verified their phone
Example:
user := sms.User{
User: auth.User{ID: "user_123", Name: "John Doe"},
Phone: ptr("+14155551234"),
PhoneVerified: true,
}
type VerifyOTPRequest ¶
type VerifyOTPRequest struct {
PhoneNumber string `json:"phoneNumber"` // Phone number to verify
Code string `json:"code"` // OTP code to verify
Purpose string `json:"purpose"` // OTP purpose
}
VerifyOTPRequest represents the request to verify an OTP code.
Example:
{
"phoneNumber": "+14155551234",
"code": "123456",
"purpose": "phone_verification"
}