emailotp

package
v1.0.0-beta.2 Latest Latest
Warning

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

Go to latest
Published: Jan 6, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package emailotp provides email-based OTP (One-Time Password) verification and authentication.

This plugin enables:

  • Email address verification via OTP codes
  • Email+password registration and login
  • Multi-factor authentication (MFA) via email
  • Password reset via email 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 email provider (SMTP, SendGrid, etc.)
  4. Code stored in database with expiry (default: 10 minutes)
  5. User submits code via VerifyOTP endpoint
  6. Plugin validates code and marks email as verified

Email+Password Flow:

  1. User registers with email+password via /register endpoint
  2. Password hashed with bcrypt and stored
  3. User can login with email+password via /login endpoint
  4. Session created on successful authentication

Route Structure:

  • POST /emailotp/send - Send OTP code (protected)
  • POST /emailotp/verify - Verify OTP code (public)
  • POST /emailotp/login - Login with email+password (public)
  • POST /emailotp/register - Register with email+password (public)

Provider Integration: Implement the Provider interface to use your email service:

type MySMTPProvider struct { host, user, pass string }
func (p *MySMTPProvider) SendOTP(to, code string) error {
  // Send email with OTP code
}

Index

Constants

View Source
const (
	// Request schemas
	SchemaLoginWithEmailRequest    = "LoginWithEmailRequest"
	SchemaRegisterWithEmailRequest = "RegisterWithEmailRequest"
	SchemaSendOTPRequest           = "SendOTPRequest"
	SchemaVerifyOTPRequest         = "VerifyOTPRequest"
)

Schema names for OpenAPI specification generation.

These constants define the OpenAPI schema names for emailotp 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 emailotp 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_email_verification.up.sql
  • Down migration: 002_add_email_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 emailotp plugin.

The schema extends the 'user' table with email-specific columns:

  • email (VARCHAR, unique): User email address
  • email_verified (BOOLEAN): Email verification status (default: false)

These extensions enable email+password authentication and email verification.

Parameters:

  • dialect: Database dialect (postgres, mysql)

Returns:

  • *plugins.Schema: Schema definition with SQL DDL
  • error: If dialect is not supported

func ValidateEmail

func ValidateEmail(email string) error

ValidateEmail validates an email address format using RFC 5322 regex.

Parameters:

  • email: Email address to validate

Returns:

  • error: If email is empty or has invalid format

Types

type Config

type Config struct {
	Provider  Provider      // Email sending provider (required for production)
	OTPExpiry time.Duration // OTP expiry duration (default: 10 minutes)
	OTPLength int           // OTP code length (default: 6)
}

Config holds Email OTP plugin configuration.

Example:

cfg := &emailotp.Config{
  Provider:  myEmailProvider,
  OTPExpiry: 15 * time.Minute,
  OTPLength: 8,
}

type DefaultEmailOTPStore

type DefaultEmailOTPStore struct {
	// contains filtered or unexported fields
}

DefaultEmailOTPStore implements EmailOTPStore using a SQL database.

This implementation uses sqlc-generated type-safe queries to manage users with email addresses in PostgreSQL, MySQL, or SQLite.

Database Schema: Extends the 'user' table with:

  • email (VARCHAR, unique): User email address
  • email_verified (BOOLEAN): Email verification status

Thread Safety: Safe for concurrent use through database transactions.

func NewDefaultEmailOTPStore

func NewDefaultEmailOTPStore(db *sql.DB) *DefaultEmailOTPStore

NewDefaultEmailOTPStore creates a new DefaultEmailOTPStore backed by SQL.

The provided database connection must have the emailotp schema applied.

Parameters:

  • db: Active SQL database connection

Returns:

  • *DefaultEmailOTPStore: Configured store ready for use

func (*DefaultEmailOTPStore) CreateUser

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

CreateUser creates a new user with email address.

func (*DefaultEmailOTPStore) GetUserByEmail

func (s *DefaultEmailOTPStore) GetUserByEmail(ctx context.Context, email string) (*User, error)

GetUserByEmail retrieves a user by email address

func (*DefaultEmailOTPStore) UpdateUserEmail

func (s *DefaultEmailOTPStore) UpdateUserEmail(ctx context.Context, userID, email string, verified bool) error

UpdateUserEmail updates a user's email address and verification status

type EncryptedOTPStorage

type EncryptedOTPStorage struct {
	// contains filtered or unexported fields
}

EncryptedOTPStorage stores OTPs using AES-256-GCM encryption.

Security Characteristics:

  • OTPs are encrypted with AES-256 (industry standard)
  • GCM mode provides authenticated encryption
  • Requires secure 32-byte encryption key

Use Cases:

  • When OTPs need to be retrieved in plain text
  • Audit logging requirements
  • Multi-system verification

Security Warning: Key management is critical. Store encryption keys securely (environment variables, secrets management systems).

Example:

// From 32-byte key
key := []byte("12345678901234567890123456789012")  // Must be exactly 32 bytes
storage, _ := emailotp.NewEncryptedOTPStorage(key)

// From string (hashed to 32 bytes)
storage := emailotp.NewEncryptedOTPStorageFromString("my-secret-key")

func NewEncryptedOTPStorage

func NewEncryptedOTPStorage(key []byte) (*EncryptedOTPStorage, error)

NewEncryptedOTPStorage creates a new encrypted OTP storage key must be exactly 32 bytes for AES-256

func NewEncryptedOTPStorageFromString

func NewEncryptedOTPStorageFromString(keyString string) *EncryptedOTPStorage

NewEncryptedOTPStorageFromString creates encrypted storage from a string key (hashed to 32 bytes)

func (*EncryptedOTPStorage) Compare

func (e *EncryptedOTPStorage) Compare(stored, input string) (bool, error)

Compare decrypts the stored OTP and compares it with the input

func (*EncryptedOTPStorage) Store

func (e *EncryptedOTPStorage) Store(otp string) (string, error)

Store encrypts the OTP using AES-256-GCM

type Handlers

type Handlers struct {
	// contains filtered or unexported fields
}

Handlers encapsulates Email OTP plugin HTTP handlers.

func NewHandlers

func NewHandlers(plugin *Plugin) *Handlers

NewHandlers creates Email OTP plugin handlers.

Parameters:

  • plugin: Initialized Email OTP plugin

Returns:

  • *Handlers: Handler instance ready for route registration

func (*Handlers) LoginWithEmailHandler

func (h *Handlers) LoginWithEmailHandler(w http.ResponseWriter, r *http.Request)

LoginWithEmailHandler handles email+password login.

Authentication Flow:

  1. Validate email format
  2. Retrieve user by email
  3. Verify password with bcrypt
  4. Create session
  5. Set session cookie

Endpoint:

  • Method: POST
  • Path: /emailotp/login
  • Auth: Public

Request Body:

{
  "email": "user@example.com",
  "password": "SecurePassword123!"
}

Response (200 OK):

{
  "success": true,
  "message": "Login successful",
  "data": {
    "user": {"id": "user_123", "email": "user@example.com", ...}
  }
}

func (*Handlers) RegisterWithEmailHandler

func (h *Handlers) RegisterWithEmailHandler(w http.ResponseWriter, r *http.Request)

RegisterWithEmailHandler handles email+password registration.

Registration Flow:

  1. Validate email format
  2. Check if email already exists
  3. Create user with hashed password
  4. Create session (auto-login)
  5. Set session cookie

Endpoint:

  • Method: POST
  • Path: /emailotp/register
  • Auth: Public

Request Body:

{
  "name": "John Doe",
  "email": "john@example.com",
  "password": "SecurePassword123!",
  "avatar": "https://example.com/avatar.jpg"  // Optional
}

Response (201 Created):

{
  "success": true,
  "message": "Registration successful",
  "data": {
    "user": {"id": "user_123", "email": "john@example.com", "emailVerified": false}
  }
}

func (*Handlers) SendOTPHandler

func (h *Handlers) SendOTPHandler(w http.ResponseWriter, r *http.Request)

SendOTPHandler handles sending OTP via email.

This endpoint is protected to prevent spam/abuse. Only authenticated users can request OTP codes.

Endpoint:

  • Method: POST
  • Path: /emailotp/send
  • Auth: Required (session)

Request Body:

{
  "email": "user@example.com",
  "userId": "user_123",
  "purpose": "email_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 OTP codes.

This endpoint is public to allow users to verify their email addresses without requiring prior authentication.

Endpoint:

  • Method: POST
  • Path: /emailotp/verify
  • Auth: Public

Request Body:

{
  "email": "user@example.com",
  "code": "123456",
  "purpose": "email_verification"
}

Response (200 OK):

{
  "success": true,
  "message": "OTP verified successfully"
}

Response (400 Bad Request):

{
  "success": false,
  "error": "Invalid or expired OTP"
}

type HashedOTPStorage

type HashedOTPStorage struct {
	// contains filtered or unexported fields
}

HashedOTPStorage stores OTPs using bcrypt hashing.

Security Benefits:

  • OTPs cannot be reversed from database
  • Resistant to rainbow table attacks
  • Configurable work factor (cost)

Performance:

  • Slower than plain text due to bcrypt cost
  • Default cost: 10 (recommended balance)

Use Cases:

  • Production environments
  • High-security applications
  • Compliance requirements (GDPR, PCI-DSS)

Example:

storage := emailotp.NewHashedOTPStorage(10)  // Cost factor 10
stored, _ := storage.Store("123456")        // Returns bcrypt hash
valid, _ := storage.Compare(stored, "123456") // true

func NewHashedOTPStorage

func NewHashedOTPStorage(cost int) *HashedOTPStorage

NewHashedOTPStorage creates a new hashed OTP storage

func (*HashedOTPStorage) Compare

func (h *HashedOTPStorage) Compare(stored, input string) (bool, error)

Compare verifies the OTP against the bcrypt hash

func (*HashedOTPStorage) Store

func (h *HashedOTPStorage) Store(otp string) (string, error)

Store hashes the OTP using bcrypt

type LoginWithEmailRequest

type LoginWithEmailRequest struct {
	Email    string `json:"email"`    // User email address
	Password string `json:"password"` // User password
}

LoginWithEmailRequest represents email+password login request.

Example:

{
  "email": "user@example.com",
  "password": "SecurePassword123!"
}

type OTPStorageMethod

type OTPStorageMethod interface {
	// Store encodes an OTP for database storage.
	//
	// Parameters:
	//   - otp: Plain text OTP code
	//
	// Returns:
	//   - string: Encoded OTP for storage
	//   - error: If encoding fails
	Store(otp string) (string, error)

	// Compare verifies an input OTP against the stored value.
	//
	// Parameters:
	//   - stored: Encoded OTP from database
	//   - input: Plain text OTP from user
	//
	// Returns:
	//   - bool: true if OTPs match
	//   - error: If comparison fails
	Compare(stored, input string) (bool, error)
}

OTPStorageMethod defines how OTPs are stored in the database.

This interface allows different storage strategies:

  • PlainOTPStorage: Store OTPs in plain text (not recommended for production)
  • HashedOTPStorage: Store bcrypt hashes (recommended, irreversible)
  • EncryptedOTPStorage: Store AES-256-GCM encrypted OTPs (reversible)

Security Considerations:

  • Plain text: Fast but insecure, suitable only for development
  • Hashed: Secure but slower, best for production
  • Encrypted: Reversible but requires secure key management

func NewOTPStorage

func NewOTPStorage(method string, encryptionKey string) (OTPStorageMethod, error)

NewOTPStorage creates the appropriate OTP storage method based on config

type PlainOTPStorage

type PlainOTPStorage struct{}

PlainOTPStorage stores OTPs in plain text.

Security Warning: This is NOT recommended for production. OTPs are stored without encryption, making them vulnerable to database breaches.

Use Cases:

  • Development and testing
  • Low-security environments

Example:

storage := emailotp.NewPlainOTPStorage()
stored, _ := storage.Store("123456")  // Returns "123456"

func NewPlainOTPStorage

func NewPlainOTPStorage() *PlainOTPStorage

NewPlainOTPStorage creates a new plain text OTP storage

func (*PlainOTPStorage) Compare

func (p *PlainOTPStorage) Compare(stored, input string) (bool, error)

Compare performs direct string comparison

func (*PlainOTPStorage) Store

func (p *PlainOTPStorage) Store(otp string) (string, error)

Store returns the OTP as-is

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin provides email-based OTP verification and authentication.

This plugin integrates with email service providers to send OTP codes and supports email+password authentication as an alternative auth method.

Features:

  • Configurable OTP length and expiry
  • Pluggable email providers (SMTP, SendGrid, SES, etc.)
  • Email address validation
  • Password hashing with bcrypt
  • Session management integration

func New

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

New creates a new Email OTP plugin instance.

Parameters:

  • cfg: Plugin configuration (can be nil for defaults)
  • store: Custom Store implementation (can be nil, will use DefaultEmailOTPStore)
  • dialect: Database dialect (optional, defaults to PostgreSQL)

Returns:

  • *Plugin: Configured plugin ready for initialization

Example:

plugin := emailotp.New(&emailotp.Config{
  Provider: mySMTPProvider,
  OTPExpiry: 15 * time.Minute,
  OTPLength: 8,
}, nil, plugins.DialectPostgres)

func (*Plugin) CreateUserWithEmailAndPassword

func (p *Plugin) CreateUserWithEmailAndPassword(ctx context.Context, name, email, password string) (*User, error)

CreateUserWithEmailAndPassword creates a new user with email+password authentication.

Registration Process:

  1. Create user record with email (unverified)
  2. Hash password with bcrypt
  3. Create password account in auth.accounts table
  4. Return user for session creation

Parameters:

  • ctx: Request context
  • name: User display name
  • email: User email address (becomes primary identifier)
  • password: Plain text password (will be hashed)

Returns:

  • *User: Created user with email field
  • error: If user creation or password hashing fails

Example:

user, err := plugin.CreateUserWithEmailAndPassword(ctx, "John Doe", "john@example.com", "SecurePass123!")

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) 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) GetUserByEmail

func (p *Plugin) GetUserByEmail(ctx context.Context, email string) (*auth.User, error)

GetUserByEmail retrieves a user by email address

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 Email OTP 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, emailAddress, purpose string) error

SendOTP generates and sends an OTP code via email.

OTP Generation and Delivery:

  1. Generate random N-digit code (configurable length)
  2. Send code via email provider
  3. Store code in verification service with expiry
  4. Invalidate any previous OTPs for same email+purpose

Parameters:

  • ctx: Request context
  • emailAddress: Recipient email address
  • userID: User ID requesting OTP
  • purpose: OTP purpose ("email_verification", "password_reset", "login_mfa")

Returns:

  • error: If OTP generation or sending fails

Example:

err := plugin.SendOTP(ctx, "user@example.com", "email_verification")

func (*Plugin) VerifyOTP

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

VerifyOTP verifies an OTP code for an email address.

Verification Process:

  1. Check if provider supports OTP verification (rare)
  2. Fall back to core verification service (standard)
  3. Validate code and check expiry
  4. Return success/failure

Parameters:

  • ctx: Request context
  • emailAddress: Email address to verify
  • code: OTP code to verify (e.g., "123456")
  • purpose: OTP purpose (must match SendOTP purpose)

Returns:

  • bool: true if OTP is valid and not expired
  • error: If verification fails

VerifyOTP verifies an OTP code for the given email address.

Example:

valid, err := plugin.VerifyOTP(ctx, "user@example.com", "123456")
if valid {
  // Mark email as verified
}

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 email address.
	//
	// Parameters:
	//   - to: Recipient email address
	//   - code: OTP code to send (e.g., "123456")
	//
	// Returns:
	//   - error: If email sending fails
	SendOTP(to, code string) error

	// VerifyOTP verifies an OTP code for an email address.
	//
	// Note: Most implementations delegate verification to the plugin's OTP storage.
	// This method is available for provider-specific validation if needed.
	//
	// Parameters:
	//   - to: Email address 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 email service providers must implement.

Users should create their own implementation based on their email service (SMTP, SendGrid, Resend, AWS SES, Postmark, Mailgun, etc.).

Abstraction Benefits:

  • Swap email providers without changing plugin code
  • Test with mock providers
  • Use different providers for different environments

Example Implementation (SMTP):

type SMTPProvider struct {
    host     string
    port     int
    username string
    password string
    from     string
}

func (p *SMTPProvider) SendOTP(to, code string) error {
    subject := "Your Verification Code"
    body := fmt.Sprintf("Your verification code is: %s\n\nThis code will expire in 10 minutes.", code)
    auth := smtp.PlainAuth("", p.username, p.password, p.host)
    msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s", p.from, to, subject, body)
    return smtp.SendMail(p.host+":"+strconv.Itoa(p.port), auth, p.from, []string{to}, []byte(msg))
}

func (p *SMTPProvider) VerifyOTP(to, code string) (bool, error) {
    // Verification logic handled by plugin's OTP storage
    // This method can be used for provider-specific validation if needed
    return true, nil
}

Example Implementation (SendGrid):

type SendGridProvider struct {
    apiKey string
    from   string
}

func (p *SendGridProvider) SendOTP(to, code string) error {
    message := mail.NewV3Mail()
    message.SetFrom(mail.NewEmail("", p.from))
    message.AddContent(mail.NewContent("text/plain", fmt.Sprintf("Your OTP: %s", code)))
    personalization := mail.NewPersonalization()
    personalization.AddTos(mail.NewEmail("", to))
    message.AddPersonalizations(personalization)

    client := sendgrid.NewSendClient(p.apiKey)
    _, err := client.Send(message)
    return err
}

type RegisterWithEmailRequest

type RegisterWithEmailRequest struct {
	Avatar   *string `json:"avatar"`   // Optional avatar URL
	Name     *string `json:"name"`     // User display name (required)
	Email    string  `json:"email"`    // User email address (required)
	Password string  `json:"password"` // User password (required)
}

RegisterWithEmailRequest represents email+password registration request.

Example:

{
  "name": "John Doe",
  "email": "john@example.com",
  "password": "SecurePassword123!",
  "avatar": "https://example.com/avatar.jpg"
}

type SendOTPRequest

type SendOTPRequest struct {
	Email   string `json:"email"`   // Email address to send OTP to
	UserID  string `json:"userId"`  // User ID requesting OTP
	Purpose string `json:"purpose"` // OTP purpose ("email_verification", "password_reset", "login_mfa")
}

SendOTPRequest represents the request to send an OTP code.

Example:

{
  "email": "user@example.com",
  "userId": "user_123",
  "purpose": "email_verification"
}

type Store

type Store interface {

	// CreateUser creates a new user with an email address.
	//
	// The email is initially unverified (emailVerified: false).
	//
	// Parameters:
	//   - ctx: Request context
	//   - user: User with email field populated
	//
	// Returns:
	//   - *User: Created user
	//   - error: If user creation fails (e.g., duplicate email)
	CreateUser(ctx context.Context, user User) (*User, error)

	// GetUserByEmail retrieves a user by email address.
	//
	// Used for:
	//   - Email+password login
	//   - Checking if email already exists during registration
	//   - Email verification lookup
	//
	// Parameters:
	//   - ctx: Request context
	//   - email: Email address to lookup
	//
	// Returns:
	//   - *User: User with matching email
	//   - error: If user not found or database error
	GetUserByEmail(ctx context.Context, email string) (*User, error)

	// UpdateUserEmail updates a user's email address and verification status.
	//
	// Used after successful OTP verification to mark email as verified.
	//
	// Parameters:
	//   - ctx: Request context
	//   - userID: User ID
	//   - email: New email address
	//   - verified: Email verification status
	//
	// Returns:
	//   - error: If update fails
	UpdateUserEmail(ctx context.Context, userID, email string, verified bool) error
}

Store defines the interface for Email OTP verification storage operations.

This interface provides email-specific user management operations:

  • User creation with email addresses
  • Email lookup for authentication
  • Email verification status updates

Thread Safety: Implementations must be safe for concurrent use.

type User

type User struct {
	auth.User
	Email         *string `json:"email,omitempty"` // User email address
	EmailVerified bool    `json:"emailVerified"`   // Email verification status
}

User extends the core User model with email-specific fields.

This model adds email verification status for display in API responses.

Use this when:

  • Returning user data after email registration
  • Displaying email verification status in user profiles
  • Checking if user has verified their email

Example:

user := emailotp.User{
  User: auth.User{ID: "user_123", Name: "John Doe"},
  Email: ptr("john@example.com"),
  EmailVerified: true,
}

type VerifyOTPRequest

type VerifyOTPRequest struct {
	Email   string `json:"email"`   // Email address 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:

{
  "email": "user@example.com",
  "code": "123456",
  "purpose": "email_verification"
}

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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