sms

package
v1.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 5, 2026 License: MIT Imports: 19 Imported by: 0

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:

  1. User requests OTP via SendOTP endpoint (requires authentication)
  2. Plugin generates 6-digit code (configurable length)
  3. Code sent via configured SMS provider (Twilio, AWS SNS, etc.)
  4. Code stored in database with expiry (default: 10 minutes)
  5. User submits code via VerifyOTP endpoint
  6. Plugin validates code and marks phone as verified

Phone+Password Flow:

  1. User registers with phone+password via /register endpoint
  2. Password hashed with bcrypt and stored
  3. User can login with phone+password via /login endpoint
  4. 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

View Source
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

func GetMigrations(dialect plugins.Dialect) ([]plugins.Migration, error)

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

func GetSchema(dialect plugins.Dialect) (*plugins.Schema, error)

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

func ValidatePhoneNumber(phone string) error

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

func (s *DefaultSMSStore) CreateUser(ctx context.Context, user User) (*User, error)

CreateUser creates a new user with phone number.

func (*DefaultSMSStore) GetUserByID added in v1.2.1

func (s *DefaultSMSStore) GetUserByID(ctx context.Context, id string) (*User, error)

GetUserByID retrieves a user by their ID

func (*DefaultSMSStore) GetUserByPhone

func (s *DefaultSMSStore) GetUserByPhone(ctx context.Context, phone string) (*User, error)

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

func NewHandlers(plugin *Plugin) *Handlers

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:

  1. Validate phone number format (E.164)
  2. Retrieve user by phone number
  3. Verify password with bcrypt
  4. Create session
  5. 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:

  1. Validate phone number format (E.164)
  2. Check if phone already exists
  3. Create user with hashed password
  4. Create session (auto-login)
  5. 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

func New(cfg *Config, store Store, dialect ...plugins.Dialect) *Plugin

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

func (p *Plugin) Description() string

Description returns a human-readable description for logging.

func (*Plugin) EnrichUser added in v1.2.1

func (p *Plugin) EnrichUser(ctx context.Context, user *core.EnrichedUser) error

EnrichUser implements plugins.UserEnricher to add phone verification status.

This method is called automatically by the authentication system after user lookup. It adds the user's phone verification status to the EnrichedUser, making it available in API responses without requiring separate queries.

Fields Added:

  • "phoneVerified" (bool): Whether the user's phone has been verified

Parameters:

  • ctx: Request context
  • user: EnrichedUser to populate with phone verification data

Returns:

  • error: If lookup fails

func (*Plugin) GetMigrations

func (p *Plugin) GetMigrations() []plugins.Migration

GetMigrations returns the plugin migrations

func (*Plugin) GetSchemas

func (p *Plugin) GetSchemas() []plugins.Schema

GetSchemas returns all schemas for all supported dialects

func (*Plugin) GetUserByPhone

func (p *Plugin) GetUserByPhone(ctx context.Context, phone string) (*auth.User, error)

GetUserByPhone retrieves a user by phone number

func (*Plugin) Init

func (p *Plugin) Init(_ context.Context, a plugins.Aegis) error

Init initializes the plugin.

func (*Plugin) MountRoutes

func (p *Plugin) MountRoutes(router router.Router, prefix string)

MountRoutes registers HTTP routes for the SMS plugin

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns the plugin identifier.

func (*Plugin) ProvidesAuthMethods

func (p *Plugin) ProvidesAuthMethods() []string

ProvidesAuthMethods returns authentication methods provided

func (*Plugin) RequiresTables

func (p *Plugin) RequiresTables() []string

RequiresTables returns core tables this plugin depends on

func (*Plugin) SendOTP

func (p *Plugin) SendOTP(ctx context.Context, phoneNumber, purpose string) error

SendOTP generates and sends an OTP via SMS

func (*Plugin) VerifyOTP

func (p *Plugin) VerifyOTP(ctx context.Context, phoneNumber, code string) (bool, error)

VerifyOTP verifies an OTP code

func (*Plugin) Version

func (p *Plugin) Version() string

Version returns the plugin version for compatibility tracking.

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)

	// GetUserByID retrieves a user by their ID.
	//
	// Used for:
	//   - Enriching user data with phone verification status
	//   - Looking up phone data by user ID
	//
	// Parameters:
	//   - ctx: Request context
	//   - id: User ID to lookup
	//
	// Returns:
	//   - *User: User with matching ID
	//   - error: If user not found or database error
	GetUserByID(ctx context.Context, id string) (*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"
}

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL