# Event Driven Architecture A transport-agnostic, resilient event bus for Authula built on Watermill. Supports 7 message brokers with automatic reconnection, concurrent handler execution, and graceful shutdown. ## Architecture [#architecture] Services publish events to the EventBus, which uses Watermill PubSub under the hood. The EventBus provides features like automatic ID assignment, timestamping, multiplexing, concurrent handler execution, retry logic, and panic recovery. Supported transports include GoChannel, Redis, Kafka, RabbitMQ, NATS, PostgreSQL, and SQLite. Handlers process events concurrently, and the EventBus manages reconnection, graceful shutdown, and message acknowledgment. **Event Flow Overview:** 1. **Services (A, B, C, ...)** - Each service can publish events to the EventBus. - Services can also subscribe to specific event types and handle them. 2. **EventBus (built on Watermill)** - Receives published events from services. - Assigns automatic IDs and timestamps. - Supports multiplexing and concurrent handler execution. - Handles retries, panic recovery, reconnection, graceful shutdown, and message acknowledgment. 3. **Pub/Sub Transport Layer** - The EventBus communicates with the underlying transport (GoChannel, Redis, Kafka, RabbitMQ, NATS, PostgreSQL, or SQLite). - The transport layer is responsible for delivering events between services. **Summary:** Services interact with the EventBus to publish and subscribe to events. The EventBus manages event delivery, reliability, and concurrency, while the transport layer handles the actual message passing between services. ## Quick Start [#quick-start] ```go // Initialize provider (Redis example) config := authulaconfig.WithEventBus(authulamodels.EventBusConfig{ Provider: authulaevents.ProviderRedis.String(), Redis: &authulamodels.RedisConfig{ URL: os.Getenv(authulaenv.EnvRedisURL), ConsumerGroup: os.Getenv(authulaenv.EnvEventBusConsumerGroup), }, }) // Then inside of plugins, you can use it like so: func (p *SomePlugin) Init(ctx *models.PluginContext) error { _, err := p.ctx.EventBus.Subscribe("event_name", func(ctx context.Context, event models.Event) error { return nil }) if err != nil { return err } return nil } ``` ## Supported Providers [#supported-providers] | Provider | Required Env Var | Optional Config | Best For | | ---------- | ---------------- | ----------------------------- | -------------------------- | | GoChannel | None | `BufferSize` | Testing, single-node apps | | Redis | `REDIS_URL` | `URL`, `ConsumerGroup` | Production, caching layers | | Kafka | `KAFKA_BROKERS` | `Brokers`, `ConsumerGroup` | High throughput, streaming | | RabbitMQ | `RABBITMQ_URL` | `URL` | Complex routing, AMQP | | NATS | `NATS_URL` | `URL` | Cloud-native, simplicity | | PostgreSQL | `POSTGRES_URL` | `URL` | Existing DB infrastructure | | SQLite | None | `DBPath` (default: events.db) | Embedded, edge deployments | ## Configuration [#configuration] Environment variables take precedence over config values: ```txt title=".env" REDIS_URL="redis://localhost:6379" EVENT_BUS_CONSUMER_GROUP="consumer_group" ``` ```go // With explicit config authulaconfig.WithEventBus(authulamodels.EventBusConfig{ Provider: authulaevents.ProviderRedis.String(), Redis: &authulamodels.RedisConfig{ URL: os.Getenv(authulaenv.EnvRedisURL), ConsumerGroup: os.Getenv(authulaenv.EnvEventBusConsumerGroup), }, }) ``` ## Best Practices [#best-practices] - **Handler Idempotency**: Events may be redelivered; design handlers to handle duplicates safely - **Topic Naming**: Use dot-notation (e.g., `user.created`, `session.expired`) - **Error Handling**: Return errors for retry, nil for success; panics are caught and logged - **Concurrency**: Default limit is 100 concurrent handlers; tune via `MaxConcurrentHandlers` - **Resource Cleanup**: Always call `bus.Close()` for graceful shutdown ## Resilience Features [#resilience-features] The EventBus automatically handles: - **Reconnection**: Exponential backoff (500ms to 30s) with jitter on transport failures - **Panics**: Recovered per handler; other handlers continue processing - **Graceful Shutdown**: Waits for active handlers to complete before closing - **Message Acknowledgment**: Automatic ack/nack based on handler success # Routes # Route Mappings [#route-mappings] Route mappings let you declare which plugins should run for specific routes without hard-coding that wiring into your application. Use route mappings when you want Authula to decide, from configuration, which capabilities apply to a route. This is the main way to connect plugin behaviour to HTTP endpoints in standalone deployments, and it also works in embedded applications through the Go configuration API. ## Why use route mappings [#why-use-route-mappings] Route mappings make plugin routing explicit and easy to audit. Instead of spreading route-specific auth logic across handlers, you define the route once and assign the relevant plugin capabilities to it. That is useful when you want to: - protect a route with session authentication - apply optional auth to public endpoints - attach permissions to specific admin or account routes - keep route-to-plugin wiring in config instead of code ## Configuration format [#configuration-format] In `config.toml`, route mappings are defined with repeated `[[route_mappings]]` tables. Each mapping can contain: - `paths` - one or more route patterns - `plugins` - plugin capabilities to run for those routes - `permissions` - optional permission names to associate with the route Example: ```toml [[route_mappings]] paths = ["GET:/me", "POST:/sign-out"] plugins = ["session.auth"] [[route_mappings]] paths = ["POST:/email-password/sign-in"] plugins = ["session.auth.optional"] ``` ## Supported path styles [#supported-path-styles] Route mapping paths support two forms: - `METHOD:/path` for a method-specific route - `/path` for a route that applies to all supported methods Examples: ```toml [[route_mappings]] paths = ["GET:/access-control/roles/{role_id}"] plugins = ["session.auth"] [[route_mappings]] paths = ["/organizations/*"] plugins = ["session.auth"] ``` The second example applies to all HTTP methods for any route under `/organizations`. ## Dynamic and wildcard routes [#dynamic-and-wildcard-routes] Route mappings support parameterized patterns such as `{role_id}` and wildcard suffixes such as `/*`. Use parameterized patterns when the route has a stable shape but dynamic values: ```toml [[route_mappings]] paths = [ "GET:/access-control/users/{user_id}/roles", "DELETE:/access-control/users/{user_id}/roles/{role_id}", ] plugins = ["session.auth"] ``` Use wildcard patterns when you want to cover a whole route subtree: ```toml [[route_mappings]] paths = ["/organizations/*"] plugins = ["session.auth"] ``` ## Multiple mappings for the same route [#multiple-mappings-for-the-same-route] You can define more than one mapping for the same route. Authula merges the plugin and permission lists for matching route keys, so you can build up route behaviour from multiple config entries. That makes it practical to separate concerns, for example one mapping for session handling and another for access-control permissions. ## Base path behaviour [#base-path-behaviour] If your app uses a base path such as `/api/auth`, Authula applies that base path when it builds the internal route metadata used by the router. That means a mapping like: ```toml [[route_mappings]] paths = ["GET:/me"] plugins = ["session.auth"] ``` is treated as the base-path-aware route for your deployment, such as `GET:/api/auth/me` when the base path is `/api/auth`. --- # Disabled Paths [#disabled-paths] Disabled paths let you tell Authula to skip specific API routes entirely. Use this when a path should not go through Authula’s normal handling pipeline. This is different from route mappings: route mappings attach metadata such as plugin capabilities or permissions (via Access Control plugin) etc to a path, while disabled paths prevent the path from being processed by Authula at all. ## Why use disabled paths [#why-use-disabled-paths] Disabled paths are helpful when you want to: - Prevent exposing API routes of a plugin if you are building your own embedded application using Authula as a library - Exclude public or externally managed endpoints - Turn off specific route groups during development or testing ## Configuration format [#configuration-format-1] Disabled paths are configured with the `disabled_paths` array in `config.toml`. Make sure to declare it at the top level of the config file, not inside the plugins section or under any other sections which are object types e.g. struct types/maps. Example: ```toml disabled_paths = [ "GET:/email-password/request-email-change", "/organizations", "/admin/*", ] ``` ## Supported path styles [#supported-path-styles-1] Disabled paths support two forms: - `METHOD:/path` for a specific method and route - `/path` for all supported methods for a specific route - `/path/*` for all routes under that path since it's a wildcard subtree ## Matching behavior [#matching-behavior] Authula normalizes route patterns before checking them, so a disabled route can match: - a method-qualified route path such as `GET:/email-password/request-email-change` - a bare route path such as `/organizations` - a wildcard subtree such as `/organizations/*` If the request matches a disabled path, Authula skips the route during registration and does not process it through the usual handler path. ## Base path behavior [#base-path-behavior] If your application uses a base path such as `/api/auth`, disabled-route matching is applied with that base path in mind. That means a disabled entry like: ```toml disabled_paths = ["GET:/health"] ``` can still match the route as it exists under the configured base path. ## Method-specific examples [#method-specific-examples] Use method-specific entries when only one HTTP method should be disabled: ```toml disabled_paths = [ "GET:/admin", "POST:/organizations", ] ``` Use wildcard paths when all routes under a specific path should be skipped for a route subtree: ```toml disabled_paths = ["/organizations/*"] ``` ## Route mappings vs disabled paths [#route-mappings-vs-disabled-paths] These two features solve different problems: - route mappings declare what metadata should run for a route such as plugin capabilities or permissions, and the route is still processed by Authula regardless whether it has a mapping or not. - disabled paths declare that the route should not be processed by Authula at all, so it is skipped during route registration and is not handled by the HTTP handler. So if a route should remain available but require auth or permissions, use route mappings. If a route should be excluded from Authula processing, use disabled paths. # Security # Security Architecture [#security-architecture] Authula is built with a **security-first philosophy**, treating protection as a foundational requirement rather than an add-on. Our architecture employs defense-in-depth, modern cryptographic primitives, and automated security lifecycles to protect user data. --- ## 1. Cryptographic Standards [#1-cryptographic-standards] We utilize industry-leading algorithms selected for their resistance to modern attack vectors (GPU/ASIC cracking and side-channel attacks). | Feature | Algorithm | Implementation Details | | ---------------------- | ----------------------- | ------------------------------------------------------------ | | **Password Hashing** | **Argon2id** | 64MB memory, 4 parallel threads, 16-byte unique salts. | | **Digital Signatures** | **Ed25519** | Used for JWTs; faster and more secure than RSA-3072. | | **Data Encryption** | **ChaCha20-Poly1305-X** | Superior performance on mobile; resistant to timing attacks. | --- ## 2. Authentication & Session Management [#2-authentication--session-management] Authula uses a multi-layered approach to verify identity and maintain state. ### Intelligent Sessions [#intelligent-sessions] - **Sliding Window:** 7-day duration with active validation every 24 hours. - **Fingerprinting:** Tracks IP addresses and User Agents to detect session hijacking. ### JWT Implementation & Rotation [#jwt-implementation--rotation] We use a dual-token system (Access/Refresh) with automated **Zero-Downtime Key Rotation**. - **Access Tokens:** Short-lived (15 min) to limit exposure. - **Refresh Tokens:** Long-lived (7 days) for user convenience. - **Key Rotation:** Keys rotate every 30 days. A 1-hour grace period ensures that old keys remain valid for tokens currently in transit, preventing service interruption. --- ## 3. Threat Mitigation [#3-threat-mitigation] ### CSRF Protection [#csrf-protection] We employ a **dual-layer defense**: 1. **Double-Submit Cookie Pattern:** Validates cryptographically secure 24-byte tokens via request headers. 2. **Native Browser Integration:** Leverages Go 1.25’s `CrossOriginProtection` to validate `Sec-Fetch-Site` and `Origin` headers at the browser level. ### Intelligent Rate Limiting [#intelligent-rate-limiting] Protects against brute-force and DoS attacks with granular control: - **Backends:** Supports In-memory, Redis (distributed), or Database storage with automatic failover. - **Headers:** Provides transparent `X-RateLimit` headers for client-side retry logic. - **Proxy Awareness:** Sophisticated detection ensures limits apply to the actual client IP, not the load balancer. --- ## 4. Token Invalidation & Telemetry [#4-token-invalidation--telemetry] Security doesn't end after a token is issued. We monitor and manage the token lifecycle actively. - **Blacklisting:** Redis-backed blacklist with automatic TTL management for logged-out or revoked tokens. - **Event Telemetry:** - `TokenReuseRecoveredEvent`: Triggered on the first reuse (likely a network glitch). - `TokenReuseMaliciousEvent`: Triggered on multiple attempts, flagging a potential replay attack. --- ## 5. Network & Infrastructure Security [#5-network--infrastructure-security] ### IP Anti-Spoofing [#ip-anti-spoofing] We operate on a **Zero-Trust IP policy**. Unless a specific CIDR range is configured as a "Trusted Proxy," the system ignores `X-Forwarded-For` headers to prevent IP masquerading. ### Security Headers & CORS [#security-headers--cors] - **CORS:** Strict origin validation; prevents dangerous wildcard (`*`) configurations when credentials are involved. - **Hardened Headers:** Automatically injects `X-Content-Type-Options`, `X-Frame-Options`, and `Referrer-Policy`. ### Configuration Vaulting [#configuration-vaulting] To prevent credential leaks, the system automatically detects and encrypts sensitive configuration keys (e.g., `APP_SECRET`, `DB_URL`) using ChaCha20-Poly1305-X before they are processed in-memory. # Basic Usage # Email & Password + Sessions [#email--password--sessions] ## Standalone Mode [#standalone-mode] ```toml title="config.toml" app_name = "Authula" base_url = "http://localhost:8080" base_path = "/api/auth" # ----------------------------------- # - Database Configuration # ----------------------------------- [database] provider = "sqlite" url = "auth.db" # ----------------------------------- # - Logger Configuration # ----------------------------------- [logger] level = "debug" # ----------------------------------- # - Session Configuration # ----------------------------------- [session] cookie_name = "authula.session_token" expires_in = "24h" update_age = "5m" cookie_max_age = "24h" secure = false http_only = true same_site = "lax" auto_cleanup = true cleanup_interval = "1m" max_sessions_per_user = 5 # ----------------------------------- # - Verification Configuration # ----------------------------------- [verification] auto_cleanup = true cleanup_interval = "1m" # ----------------------------------- # - Security Configuration # ----------------------------------- [security] trusted_origins = ["http://localhost:3000"] trusted_headers = [] trusted_proxies = [] [security.cors] allow_credentials = true allowed_origins = ["http://localhost:3000"] allowed_methods = ["OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"] allowed_headers = ["Authorization", "Content-Type", "Set-Cookie", "Cookie"] exposed_headers = [] max_age = "24h" # ----------------------------------- # - Event Bus Configuration # ----------------------------------- [event_bus] prefix = "" max_concurrent_handlers = 100 context_timeout = "5s" provider = "gochannel" [event_bus.go_channel] buffer_size = 100 # ----------------------------------- # - Plugins Configuration # ----------------------------------- [plugins] # --- Email Plugin --- [plugins.email] enabled = true provider = "smtp" from_address = "noreply@example.com" # --- Email/Password Plugin --- [plugins.email_password] enabled = true min_password_length = 8 max_password_length = 128 disable_sign_up = false require_email_verification = true auto_sign_in = true send_email_on_sign_up = true send_email_on_sign_in = false email_verification_expires_in = "24h" password_reset_expires_in = "1h" request_email_change_expires_in = "1h" # --- Session Plugin --- [plugins.session] enabled = true # ----------------------------------- # - Route Mappings # ----------------------------------- # Core routes [[route_mappings]] paths = ["GET:/me", "POST:/sign-out"] plugins = ["session.auth"] # Email/Password routes [[route_mappings]] paths = [ "POST:/email-password/sign-in", "POST:/email-password/sign-up", "GET:/email-password/verify-email" ] plugins = ["session.auth.optional"] [[route_mappings]] paths = [ "POST:/email-password/send-email-verification", "POST:/email-password/request-password-reset", "POST:/email-password/change-password", "POST:/email-password/request-email-change", ] plugins = ["session.auth"] ``` ## Library Mode [#library-mode] ```go title="main.go" package main import ( "log" "net/http" "github.com/joho/godotenv" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" emailplugin "github.com/Authula/authula/plugins/email" emailpasswordplugin "github.com/Authula/authula/plugins/email-password" emailpasswordplugintypes "github.com/Authula/authula/plugins/email-password/types" emailplugintypes "github.com/Authula/authula/plugins/email/types" sessionplugin "github.com/Authula/authula/plugins/session" ) func main() { _ = godotenv.Load(".env") config := authulaconfig.NewConfig( authulaconfig.WithAppName("YourAppName"), authulaconfig.WithBasePath("/api/auth"), authulaconfig.WithDatabase(authulamodels.DatabaseConfig{ Provider: "sqlite", URL: "auth.db", }), authulaconfig.WithLogger(authulamodels.LoggerConfig{ Level: "debug", }), authulaconfig.WithSession(authulamodels.SessionConfig{ CookieName: "authula.session_token", ExpiresIn: 24 * time.Hour, UpdateAge: 5 * time.Minute, CookieMaxAge: 24 * time.Hour, Secure: false, HttpOnly: true, SameSite: "lax", MaxSessionsPerUser: 5, AutoCleanup: true, CleanupInterval: time.Minute, }), authulaconfig.WithVerification(authulamodels.VerificationConfig{ AutoCleanup: true, CleanupInterval: time.Minute, }), authulaconfig.WithSecurity(authulamodels.SecurityConfig{ TrustedOrigins: []string{"http://localhost:3000"}, CORS: authulamodels.CORSConfig{ AllowCredentials: true, AllowedOrigins: []string{"http://localhost:3000"}, AllowedMethods: []string{"OPTIONS", "GET", "POST", "PATCH", "PUT", "DELETE"}, AllowedHeaders: []string{"Authorization", "Content-Type", "Set-Cookie", "Cookie"}, ExposedHeaders: []string{}, MaxAge: 24 * time.Hour, }, }), authulaconfig.WithEventBus(authulamodels.EventBusConfig{ Provider: authulaevents.ProviderGoChannel, }), authulaconfig.WithRouteMappings( []authulamodels.RouteMapping{ // Core routes { Paths: []string{"GET:/me", "POST:/sign-out"}, Plugins: []string{"session.auth"}, }, // Email/Password routes { Paths: []string{ "POST:/email-password/sign-in", "POST:/email-password/sign-up", "GET:/email-password/verify-email", }, Plugins: []string{"session.auth.optional"}, }, { Paths: []string{ "POST:/email-password/send-email-verification", "POST:/email-password/request-password-reset", "POST:/email-password/change-password", "POST:/email-password/request-email-change", }, Plugins: []string{"session.auth"}, }, }, ), ) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{ emailplugin.New(emailplugintypes.EmailPluginConfig{ Enabled: true, Provider: emailplugintypes.ProviderSMTP, FromAddress: "email@domain.com", }), emailpasswordplugin.New(emailpasswordplugintypes.EmailPasswordPluginConfig{ Enabled: true, MinPasswordLength: 8, MaxPasswordLength: 32, DisableSignUp: false, RequireEmailVerification: true, AutoSignIn: true, SendEmailOnSignUp: true, SendEmailOnSignIn: false, EmailVerificationExpiresIn: 24 * time.Hour, PasswordResetExpiresIn: time.Hour, RequestEmailChangeExpiresIn: time.Hour, }), sessionplugin.New(sessionplugin.SessionPluginConfig{ Enabled: true, }), }, }) // Start the server and mount the handler log.Fatal(http.ListenAndServe(":8080", auth.Handler())) } ``` --- ## Environment Variables [#environment-variables] ```bash title=".env" SMTP_HOST="domain.com" SMTP_PORT="465" SMTP_USER="username" SMTP_PASS="password" ``` --- ## Testing the Setup [#testing-the-setup] ### Sign Up [#sign-up] ```bash curl -X POST http://localhost:8080/api/auth/email-password/sign-up \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john.doe@example.com", "password": "password123"}' ``` ### Sign In [#sign-in] ```bash curl -X POST http://localhost:8080/api/auth/email-password/sign-in \ -H "Content-Type: application/json" \ -d '{"email": "john.doe@example.com", "password": "password123"}' ``` ### Get Authenticated User Info [#get-authenticated-user-info] ```bash curl -X GET http://localhost:8080/api/auth/me \ -H "Cookie: authula.session_token=your-session-token" ``` # Installation There are two ways to configure Authula in your project: as a standalone server or as a library integrated into your existing API server. The following sections will guide you through both methods. ## Standalone Mode [#standalone-mode] ### Setup configuration file [#setup-configuration-file] - Make a copy of the following `config.toml` file which provides you with some default configuration to get started: ```toml title="config.toml" # Application Name app_name = "Authula" # Base URL for the application (used as the base url of the API and for redirects, emails, etc.) base_url = "http://localhost:8080" # Base Path for API endpoints (e.g., "/auth") base_path = "/auth" # Used for encryption. Generate using `openssl rand -hex 32`. # NOTE: It is recommended to set this via the AUTHULA_SECRET environment variable instead of hardcoding it here for better security. # secret = "" # Optional list of API routes to exclude from Authula's router. disabled_paths = [] # ----------------------------------- # - Database Configuration # ----------------------------------- [database] provider = "postgres" # NOTE: It is recommended to set the database URL via the AUTHULA_DATABASE_URL environment variable instead of hardcoding it here for better security. # url = "postgresql://username:password@localhost:5432/authula?sslmode=disable" max_open_conns = 25 max_idle_conns = 5 conn_max_lifetime = "10m" # ----------------------------------- # - Logger Configuration # ----------------------------------- [logger] level = "debug" # Options: "debug", "info", "warn", "error" # ----------------------------------- # - Session Configuration # ----------------------------------- [session] cookie_name = "authula.session_token" expires_in = "24h" update_age = "5m" cookie_max_age = "24h" secure = false http_only = true same_site = "lax" # Options: "lax", "strict", "none" auto_cleanup = true # Flag to automatically clean up expired sessions and enforce max sessions per user cleanup_interval = "1m" # Interval for automatic cleanup of expired sessions max_sessions_per_user = 5 # Maximum number of concurrent sessions per user (0 = unlimited) # ----------------------------------- # - Verification Configuration # ----------------------------------- [verification] auto_cleanup = true # Flag to automatically clean up expired verifications cleanup_interval = "1m" # Interval for automatic cleanup of expired verifications # ----------------------------------- # - Security Configuration # ----------------------------------- [security] trusted_origins = ["http://localhost:3000"] trusted_headers = [] # e.g., ["X-Real-IP", "CF-Connecting-IP" (cloudflare), ...] trusted_proxies = [] # IMPORTANT CORS NOTE: If using the session plugin, the CORS config below must specify exact allowed_origins. # Setting allowed_origins = ["*"] with allow_credentials = true is NOT allowed by browsers and will fail. # Example CORS configuration when using sessions: # allow_credentials = true # allowed_origins = ["http://localhost:3000"] [security.cors] allow_credentials = true allowed_origins = ["http://localhost:3000"] # If using session plugin, specify exact origins instead of "*" allowed_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] # If using the CSRF plugin, ensure the CSRF token header is included here allowed_headers = ["Authorization", "Content-Type", "Set-Cookie", "Cookie"] exposed_headers = [] max_age = "24h" # ----------------------------------- # - Event Bus Configuration # ----------------------------------- # The event bus handles internal authentication events and allows plugins to communicate # Supports multiple providers via Watermill. # Provider options: "gochannel", "sqlite", "postgres", "redis", "kafka", "nats", "rabbitmq" [event_bus] # Optional prefix for all event topics (default: "") prefix = "" # Maximum concurrent handlers for event processing (default: 100) max_concurrent_handlers = 100 # Timeout for async published events (default: 5s) context_timeout = "5s" # Options: "gochannel", "sqlite", "postgres", "redis", "kafka", "nats", "rabbitmq" provider = "sqlite" # GoChannel Configuration (Default - In-Memory) # Best for development and testing - events lost on restart # Other providers enable distributed event handling across multiple instances [event_bus.go_channel] buffer_size = 100 # SQLite Configuration # Event storage in SQLite file (good for single-instance deployments with persistence) # [event_bus.sqlite] # db_path = "events.db" # File path for the SQLite database (relative to current working directory) # Redis Configuration # Enables distributed event handling across multiple instances # [event_bus.redis] # consumer_group = "authula_consumer_group" # Kafka Configuration # For high-throughput, distributed event streaming # [event_bus.kafka] # brokers = "localhost:9092" # consumer_group = "authula_consumer_group" # NATS Configuration # Lightweight, cloud-native messaging # [event_bus.nats] # url = "nats://localhost:4222" # AMQP Configuration (RabbitMQ) # Traditional message broker with advanced routing # [event_bus.rabbitmq] # Prefer to use EVENT_BUS_RABBITMQ_URL environment variable to set the URL # url = "amqp://guest:guest@localhost:5672/" # ----------------------------------- # - Plugins Configuration # ----------------------------------- [plugins] # Check the plugins section of the documentation for details on configuring each plugin. ``` - Feel free to modify the configuration fields with your desired settings. Most of these settings allow you to configure the server according to your needs. - It is recommended that you do not set secret values within the `config.toml` file directly. Instead, use environment variables to manage sensitive values securely. ### Set environment variables via .env file [#set-environment-variables-via-env-file] - Make a copy of the following environment variables into a `.env` file: ```bash title=".env" # Third party ENV variables # Required for OAuth2 plugin DISCORD_CLIENT_ID= DISCORD_CLIENT_SECRET= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= # Required depending on the plugin being used and their providers POSTGRES_URL= REDIS_URL= KAFKA_BROKERS= NATS_URL= RABBITMQ_URL= # Optional - Required only if using event bus consumers e.g. Redis/Kafka EVENT_BUS_CONSUMER_GROUP= # Authula ENV variables # Path to your configuration file (e.g., ./config.toml) (optional as it defaults to looking for config.toml in the current working directory) AUTHULA_CONFIG_PATH= # The base URL of your API (e.g., http://localhost:8080) (optional as it can be set in config.toml) AUTHULA_BASE_URL= # Used for encryption. Generate using `openssl rand -hex 32` (required if not set in config.toml) AUTHULA_SECRET= # sqlite: "auth.db" # postgresql: "postgresql://username:password@localhost:5432/authula?sslmode=disable" # mysql: "username:password@tcp(localhost:3306)/authula" # (required if not set in config.toml) AUTHULA_DATABASE_URL= GO_ENV=development PORT=8080 ``` - Feel free to remove any environment variables you are not using or do not need for your setup. ### Run the server via Docker [#run-the-server-via-docker] - You can run the server using Docker by mounting your configuration file and passing the necessary environment variables. Here's an example: ```bash $ docker run -itd \ -p 8080:8080 \ -v ./config.toml:/home/appuser/config.toml \ --env-file ./.env \ ghcr.io/authula/authula:latest ``` - Make sure to have the `config.toml` and `.env` files in the same directory when you run the Docker command above. --- ## Library Mode [#library-mode] ### Set environment variables [#set-environment-variables] Follow the environment variables step from standalone mode above. ### Install the Package [#install-the-package] ```bash $ go get github.com/Authula/authula ``` ### Create auth config [#create-auth-config] ```go import ( authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" ) config := authulaconfig.NewConfig( /* * the same options as config.toml can be set here via functional options. * See the "config/options.go" file in the repository for all available options. * e.g.: authulaconfig.WithAppName("YourAppName"), authulaconfig.WithBasePath("/api/auth"), // ...other configuration options */ ) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: [/*...*/], }) ``` ### Configure database [#configure-database] Authula supports multiple database providers. You can select a database provider by supplying it in the config. SQLite Postgres MySQL ```go import ( authulaconfig "github.com/Authula/authula/config" ) authulaconfig.NewConfig( authulaconfig.WithDatabase(authulamodels.DatabaseConfig{ Provider: "sqlite", }), ) ``` ```go import ( authulaconfig "github.com/Authula/authula/config" ) authulaconfig.NewConfig( authulaconfig.WithDatabase(authulamodels.DatabaseConfig{ Provider: "postgres", }), ) ``` ```go import ( authulaconfig "github.com/Authula/authula/config" ) authulaconfig.NewConfig( authulaconfig.WithDatabase(authulamodels.DatabaseConfig{ Provider: "mysql", }), ) ``` ### Database migrations [#database-migrations] Authula runs all migrations (core & plugins) automatically when you create a new instance of the library so all the relevant database tables are created for you without any additional steps. This is done via a custom migrator which utilises Bun ORM and raw SQL. See the [Database Schema](/docs/reference/database-schema) for the core tables created by these migrations. ### Authentication Methods [#authentication-methods] Configure the authentication methods you want to use by choosing from the available plugins. Authula supports various authentication methods, including the Email/Password authentication method as a plugin and also the OAuth2 plugin with providers such as Discord, GitHub and Google with more coming soon. ```go import ( authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" emailpasswordplugin "github.com/Authula/authula/plugins/email-password" emailpasswordplugintypes "github.com/Authula/authula/plugins/email-password/types" ) config := authulaconfig.NewConfig(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: [ emailpasswordplugin.New(emailpasswordplugintypes.EmailPasswordPluginConfig{ Enabled: true, // ...other configuration options }), ], }) ``` ### Mount Handlers [#mount-handlers] To handle API requests, you need to integrate the Authula handlers in your API. This is an example of how it can be done with the standard library server: ```go import ( "log" "net/http" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" ) func main() { // Initialise auth config := authulaconfig.NewConfig(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: [/*...*/], }) // Start the server and mount the handler log.Fatal(http.ListenAndServe(":8080", auth.Handler())) } ``` Take a look at the [Integrations](/docs/integrations/adapters) section to see how to integrate it with other libraries. # Client-Side Plugins ## Overview [#overview] Authula provides a flexible plugin architecture that allows developers to extend the functionality of the TypeScript SDK with custom authentication flows, middleware, and utilities. The plugin system enables you to enhance the client-side authentication experience without modifying the core SDK. ## How the Plugin System Works [#how-the-plugin-system-works] The plugin architecture is built around the concept of intercepting and enhancing HTTP requests made by the SDK. Each plugin can: - Register before-fetch hooks to modify requests before they're sent - Register after-fetch hooks to handle responses and potentially retry requests - Add custom methods to the client instance - Access and manipulate authentication tokens and cookies ### Core Architecture [#core-architecture] The plugin system operates through three main components: 1. **Plugin Interface**: Defines the contract that all plugins must implement 2. **Hook Registration**: Allows plugins to intercept requests and responses 3. **Method Extension**: Enables plugins to add functionality to the client ## Creating Custom Plugins [#creating-custom-plugins] ### The Plugin Interface [#the-plugin-interface] Every plugin must implement the `Plugin` interface: ```typescript export interface Plugin { readonly id: string; init(client: AuthulaClient): any; } ``` - `id`: A unique identifier for the plugin that becomes the property name on the client - `init`: A method that receives the client instance and returns the plugin's public API ### Basic Plugin Structure [#basic-plugin-structure] Here's the basic structure of a custom plugin: ```typescript import type { AuthulaClient, Plugin } from "authula"; class MyCustomPlugin implements Plugin { public readonly id = "myCustom"; // This becomes client.myCustom public init(client: AuthulaClient) { // Register hooks and return public API return { // Public methods that will be available on client.myCustom doSomething: async () => { // Implementation here }, }; } } ``` ### Using Hooks [#using-hooks] Plugins can register two types of hooks: #### Before-Fetch Hooks [#before-fetch-hooks] Before-fetch hooks allow you to modify requests before they're sent to the server: ```typescript client.registerBeforeFetch(async (ctx: FetchContext) => { // Modify the request context ctx.init.headers = { ...ctx.init.headers, "X-Custom-Header": "custom-value", }; }); ``` #### After-Fetch Hooks [#after-fetch-hooks] After-fetch hooks allow you to handle responses and potentially retry requests: ```typescript client.registerAfterFetch(async (ctx: FetchContext, res: Response) => { if (res.status === 401) { // Handle unauthorized responses // Return "retry" to retry the request return "retry"; } }); ``` ## Available Plugins [#available-plugins] ### Email/Password [#emailpassword] The EmailPassword plugin provides convenience methods to make requests to the API endpoints on the Authula server. View the [Source Code](https://github.com/Authula/authula-node-sdk/blob/main/src/plugins/email-password/). ### OAuth2 [#oauth2] The OAuth2 plugin provides convenience methods to make requests to the API endpoints on the Authula server. View the [Source Code](https://github.com/Authula/authula-node-sdk/blob/main/src/plugins/oauth2/). ### CSRF [#csrf] The CSRF plugin automatically handles CSRF tokens for mutating requests. View the [Source Code](https://github.com/Authula/authula-node-sdk/blob/main/src/plugins/csrf/). ### JWT [#jwt] The JWT plugin provides convenience methods to make requests to the API endpoints on the Authula server. View the [Source Code](https://github.com/Authula/authula-node-sdk/blob/main/src/plugins/jwt/). ### Bearer [#bearer] The Bearer plugin automatically adds authorization headers and handles token refresh. View the [Source Code](https://github.com/Authula/authula-node-sdk/blob/main/src/plugins/bearer/). ### Magic Link [#magic-link] The Magic Link plugin provides convenience methods to handle magic link authentication flows. View the [Source Code](https://github.com/Authula/authula-node-sdk/blob/main/src/plugins/magic-link/). ## Available Capabilities [#available-capabilities] ### Hook System [#hook-system] Plugins have access to the following hook capabilities: - **Before-Fetch Hooks**: Modify request headers, parameters, or body before sending - **After-Fetch Hooks**: Handle responses, implement retry logic, or manage token refresh - **Retry Mechanism**: Return "retry" from after-fetch hooks to automatically retry failed requests ### Client Integration [#client-integration] Plugins can: - Access the client configuration (server URL, cookies, fetch options) - Register multiple hooks of the same type - Add custom methods to the client instance - Interact with other registered plugins ### Cookie Management [#cookie-management] For SSR environments, plugins can work with cookie stores: ```typescript // Access cookies in SSR contexts if (client.config.cookies) { const cookieStore = await client.config.cookies(); // Manipulate cookies as needed } ``` ## Using Plugins [#using-plugins] ### Installing Plugins [#installing-plugins] To use plugins, pass them when creating a client instance: ```typescript import { createClient } from "authula"; import { EmailPasswordPlugin, BearerPlugin, CSRFPlugin } from "authula/plugins"; const client = createClient({ url: "http://localhost:8080/auth", plugins: [ new EmailPasswordPlugin(), new BearerPlugin(), new CSRFPlugin({ cookieName: "csrf-token", headerName: "X-CSRF-Token", }), new MyCustomPlugin(), ], }); // Now you can use plugin methods await client.emailPassword.signIn({ email, password }); await client.myCustom.doSomething(); ``` ### Plugin Dependencies [#plugin-dependencies] Some plugins may depend on others. For example, the Bearer plugin requires the JWT plugin for token refresh functionality: ```typescript // Type-safe plugin dependencies public init(client: ClientWithPlugins<[JWTPlugin]>) { // The client is guaranteed to have the JWT plugin available if (!client.jwt) { console.warn("JWT Plugin is required for Bearer token refresh."); return; } // Use client.jwt methods here } ``` ## Best Practices [#best-practices] ### 1. Unique Plugin IDs [#1-unique-plugin-ids] Always use unique, descriptive IDs for your plugins to avoid conflicts: ```typescript // Good public readonly id = "myFeature"; // Avoid generic names public readonly id = "plugin"; // Too generic ``` ### 2. Proper Error Handling [#2-proper-error-handling] Handle errors gracefully, especially in hooks that affect all requests: ```typescript client.registerAfterFetch(async (ctx: FetchContext, res: Response) => { try { if (res.status === 401) { // Handle unauthorized } } catch (error) { console.error("Plugin error:", error); // Don't break the request chain } }); ``` ### 3. Environment Awareness [#3-environment-awareness] Be aware of client vs. server environments when accessing browser-specific APIs: ```typescript client.registerBeforeFetch(async (ctx: FetchContext) => { if (typeof document === "undefined") { // Server-side code return; } // Client-side code const token = localStorage.getItem("token"); }); ``` ### 4. Single-Flight Requests [#4-single-flight-requests] For operations that should not run concurrently (like token refresh), implement single-flight patterns: ```typescript private refreshPromise: Promise | null = null; // In your method: if (!this.refreshPromise) { this.refreshPromise = refreshTokenLogic(); } const result = await this.refreshPromise; this.refreshPromise = null; ``` ### 5. Clean Up Resources [#5-clean-up-resources] If your plugin creates resources that need cleanup, consider providing a destroy method or ensuring proper cleanup in your plugin lifecycle. ## Advanced Patterns [#advanced-patterns] ### Conditional Hook Registration [#conditional-hook-registration] Register hooks conditionally based on configuration: ```typescript public init(client: AuthulaClient) { if (this.options.enableLogging) { client.registerBeforeFetch(logRequest); client.registerAfterFetch(logResponse); } return { // Plugin methods }; } ``` ### Plugin Composition [#plugin-composition] Combine multiple plugins or create plugin factories: ```typescript function createAuthPlugin(options: AuthPluginOptions) { return [ new CSRFPlugin(options.csrf), new BearerPlugin(options.bearer), new EmailPasswordPlugin(), ]; } const client = createClient({ url: "...", plugins: createAuthPlugin(config), }); ``` ## Conclusion [#conclusion] The Authula plugin system provides a powerful and flexible way to extend the client-side authentication SDK. By implementing the Plugin interface and leveraging the hook system, you can customize authentication flows, add security features, implement caching strategies, and much more. The architecture is designed to be modular, type-safe, and compatible with both client-side and server-side rendering environments, making it suitable for a wide range of applications and use cases. # Library Mode # Overview [#overview] In library mode, you can embed Authula directly into your Go application. This gives you direct access to Authula's functionality for various different use cases. **This section covers**: - [Programmatic APIs](./programmatic-apis.mdx): Learn how to use Authula's programmatic APIs in library mode for direct access to Authula functionality. # Programmatic APIs # Overview [#overview] Authula can be embedded directly into a Go application. In library mode, you keep a handle to the `Auth` instance and call its exported APIs or the APIs exposed by enabled plugins. This allows you to access Authula's functionality directly without needing to send HTTP requests to the server or the need to go through the HTTP Request/Response cycle. --- ## Core API [#core-api] The top-level `Auth` type exposes the main programmatic entry point through its exported `Api` field. ```go import ( "context" "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" ) config := authulaconfig.New(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{}, }) // Fetch a user's profile and current session meResult, err := auth.Api.GetMe(context.Background(), "user ID") // Sign out a user from a session (defaults to most recent session if sessionID is nil and signOutAll is false) signOutResult, err := auth.Api.SignOut(context.Background(), "user ID", nil, new(false)) ``` --- ## Plugin APIs [#plugin-apis] Enabled plugins also expose programmatic APIs. The common pattern is: 1. Create the plugin instance. 2. Pass it into `authula.New(...)`. 3. After initialization, call the plugin's exported `Api` field. `Method 1`: ```go import ( "context" "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" emailpasswordplugin "github.com/Authula/authula/plugins/email-password" emailpasswordplugintypes "github.com/Authula/authula/plugins/email-password" ) // Create plugin instance before initialization so you can access it directly after initialization without looking it up from the plugin registry emailPasswordPlugin := emailpasswordplugin.New(emailpasswordplugintypes.EmailPasswordPluginConfig{}) config := authulaconfig.New(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{ emailPasswordPlugin, }, }) // You can now easily access the plugin here user, err := emailPasswordPlugin.Api.SignIn(context.Background(), email, password, nil, nil, nil) ``` `Method 2:` ```go import ( "context" "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" organizationsplugin "github.com/Authula/authula/plugins/organizations" organizationsplugintypes "github.com/Authula/authula/plugins/organizations" ) config := authulaconfig.New(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{ emailPasswordPlugin, }, }) // Look up the plugin from the registry after initialization organizationsPlugin, ok := auth.PluginRegistry.GetPlugin(authulamodels.PluginOrganizations.String()).(*organizations.OrganizationsPlugin) if !ok { // You can return early here in your own code or handle it differently however you wish... return } // Now you can use the plugin's API to perform actions programmatically organizations, err := organizationsPlugin.Api.GetAllOrganizations(context.Background(), "user ID") if err != nil { // Handle error } ``` --- ## When To Use Which? [#when-to-use-which] - Use `auth.Api` for access to Authula's core APIs and functionality. - Use a plugin `Api` when you need access to functionality provided by a plugin. - Use the lower-level helpers only when you need custom routing or integration behavior e.g. composing Authula into a larger application. --- ## Notes [#notes] - Core and plugin APIs are available after Authula instance initialization. - The exact API surface depends on which plugins you enable in `AuthConfig`. # Server-Side Plugins ## Overview [#overview] Authula's plugin system lets you extend the authentication framework with custom functionality. Each plugin implements a base interface and can optionally add routes, migrations, middleware, hooks, or auth methods. --- ## Core Architecture [#core-architecture] ### Plugin Interface [#plugin-interface] Every plugin must implement this: ```go type Plugin interface { Metadata() PluginMetadata Config() any Init(ctx *PluginContext) error Close() error } ``` | Method | Purpose | | ------------ | ----------------------------------------------------------------- | | `Metadata()` | Returns plugin ID, version, description | | `Config()` | Returns the typed config struct | | `Init(ctx)` | Initialize with access to DB, logger, event bus, service registry | | `Close()` | Clean up resources on shutdown | ### Plugin Context [#plugin-context] ```go type PluginContext struct { DB bun.IDB Logger Logger EventBus EventBus ServiceRegistry ServiceRegistry GetConfig func() *Config } ``` ### Service Registry [#service-registry] Plugins communicate by registering and retrieving services: ```go type ServiceRegistry interface { Register(name string, service any) Get(name string) any } ``` --- ## Optional Capabilities [#optional-capabilities] Plugins implement these optional interfaces to add functionality: ### `PluginWithMigrations` — Database Migrations [#pluginwithmigrations--database-migrations] ```go type PluginWithMigrations interface { Migrations(provider string) []migrations.Migration DependsOn() []string } ``` ```go func (p *MyPlugin) Migrations(provider string) []migrations.Migration { return migrations.ForProvider(provider, migrations.ProviderVariants{ "sqlite": func() []migrations.Migration { return []migrations.Migration{mySQLite()} }, "postgres": func() []migrations.Migration { return []migrations.Migration{myPostgres()} }, }) } func (p *MyPlugin) DependsOn() []string { return nil } func mySQLite() migrations.Migration { return migrations.Migration{ Version: "20250101000000_create_my_table", Up: func(ctx context.Context, tx bun.Tx) error { return migrations.ExecStatements(ctx, tx, `CREATE TABLE IF NOT EXISTS my_table ( id TEXT NOT NULL PRIMARY KEY );`, ) }, Down: func(ctx context.Context, tx bun.Tx) error { return migrations.ExecStatements( ctx, tx, `DROP TABLE IF EXISTS my_table;` ) }, } } ``` ### `PluginWithRoutes` — HTTP Routes [#pluginwithroutes--http-routes] ```go type PluginWithRoutes interface { Routes() []Route } ``` ```go func (p *MyPlugin) Routes() []models.Route { return []models.Route{ { Method: "GET", Path: "/api/my-plugin/status", Handler: http.HandlerFunc(p.handleStatus), }, } } ``` ### `PluginWithMiddleware` — Global Middleware [#pluginwithmiddleware--global-middleware] ```go type PluginWithMiddleware interface { Middleware() []func(http.Handler) http.Handler } ``` ### `AuthMethodProvider` — Authentication Middleware [#authmethodprovider--authentication-middleware] ```go type AuthMethodProvider interface { AuthMiddleware() func(http.Handler) http.Handler OptionalAuthMiddleware() func(http.Handler) http.Handler } ``` - `AuthMiddleware()` — rejects unauthenticated requests - `OptionalAuthMiddleware()` — authenticates if credentials present, allows anonymous ### `PluginWithHooks` — Request Lifecycle Hooks [#pluginwithhooks--request-lifecycle-hooks] ```go type PluginWithHooks interface { Hooks() []Hook } ``` Four hook stages: ```go HookOnRequest // Very start of every request HookBefore // Before route matching and handling HookAfter // After route handling, before response sent HookResponse // After response written ``` Hook struct: ```go type Hook struct { Stage HookStage PluginID string // Optional — filters by route metadata["plugins"] Matcher HookMatcher // Optional — filters by request context Handler HookHandler // The hook implementation Order int // Execution order (local to PluginID) Async bool // Side-effects only — never auth/security } ``` **Execution rules:** - Hooks without `PluginID` run for all routes; with `PluginID` only if listed in route metadata - Hooks sorted by `PluginID` (grouping), then by `Order` within each group - `Matcher` returning `false` skips the hook - `Handler` returning an error is logged but doesn't stop other hooks - Setting `reqCtx.Handled = true` stops subsequent hooks at that stage - **Async hooks** run in background goroutines for side-effects only (logging, analytics, events, webhooks). Never use for auth, CSRF, rate-limiting, or security. ```go func (p *MyPlugin) Hooks() []models.Hook { return []models.Hook{ { Stage: models.HookBefore, PluginID: "my_plugin", Matcher: func(rc *models.RequestContext) bool { return strings.HasPrefix(rc.Path, "/api/secure") }, Handler: p.validateToken, Order: 10, }, } } ``` ### RequestContext [#requestcontext] Available to all hooks and handlers: ```go reqCtx.Request // *http.Request reqCtx.ResponseWriter // http.ResponseWriter reqCtx.Path // string reqCtx.Method // string reqCtx.Headers // http.Header reqCtx.Actor // *Actor — resolved identity reqCtx.ClientIP // string reqCtx.Values // map[string]any — shared key-value store reqCtx.Route // *Route — matched route with metadata reqCtx.Handled // bool — short-circuit flag ``` Helper methods: ```go reqCtx.SetActorInContext(actor *Actor) // Sets actor in both RequestContext and Go context reqCtx.SetResponse(status int, headers http.Header, body []byte) // Override response reqCtx.SetJSONResponse(status int, payload any) // Override with JSON ``` --- ## Creating a Custom Plugin [#creating-a-custom-plugin] ### 1. Define the plugin [#1-define-the-plugin] ```go package myplugin import ( "net/http" "github.com/Authula/authula/internal/util" "github.com/Authula/authula/models" "github.com/Authula/authula/services" ) type MyPlugin struct { config MyPluginConfig logger models.Logger ctx *models.PluginContext userService services.UserService } type MyPluginConfig struct { Enabled bool `json:"enabled" toml:"enabled"` SomeField string `json:"some_field" toml:"some_field"` } ``` ### 2. Implement the base interface [#2-implement-the-base-interface] ```go func New(config MyPluginConfig) *MyPlugin { return &MyPlugin{config: config} } func (p *MyPlugin) Metadata() models.PluginMetadata { return models.PluginMetadata{ ID: "my_plugin", Version: "1.0.0", Description: "Custom functionality", } } func (p *MyPlugin) Config() any { return p.config } func (p *MyPlugin) Init(ctx *models.PluginContext) error { p.ctx = ctx p.logger = ctx.Logger if err := util.LoadPluginConfig(ctx.GetConfig(), p.Metadata().ID, &p.config); err != nil { return err } userSvc, ok := ctx.ServiceRegistry.Get(models.ServiceUser.String()).(services.UserService) if !ok { return fmt.Errorf("user service not available") } p.userService = userSvc return nil } func (p *MyPlugin) Close() error { return nil } ``` ### 3. Add routes (optional) [#3-add-routes-optional] ```go func (p *MyPlugin) Routes() []models.Route { return []models.Route{ { Method: "GET", Path: "/api/my-plugin/info", Handler: http.HandlerFunc(p.handleInfo), }, } } func (p *MyPlugin) handleInfo(w http.ResponseWriter, r *http.Request) { util.JSONResponse(w, http.StatusOK, map[string]string{ "plugin_id": p.Metadata().ID, }) } ``` ### 4. Register with Authula [#4-register-with-authula] ```go config := authulaconfig.NewConfig() auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []models.Plugin{ myplugin.New(myplugin.MyPluginConfig{ SomeField: "your-value", }), }, }) ``` Plugins can also be loaded automatically from a config file — see the configuration docs for details. --- ## Configuration [#configuration] Example TOML: ```toml [plugins.session] enabled = true [plugins.oauth2] enabled = true [plugins.ratelimit] enabled = true window = "1m" max = 100 [plugins.my_plugin] enabled = true some_field = "your-value" ``` --- ## Best Practices [#best-practices] 1. Validate config in `Init` — return an error if invalid 2. Clean up resources in `Close` 3. Use the provided logger, not external ones 4. Use `Async: true` only for side-effects (logging, analytics, events) 5. Never make auth validation, CSRF, or rate-limiting hooks async 6. Plugin IDs should be lowercase with underscores 7. Use `reqCtx.SetActorInContext()` to set the authenticated actor 8. Use `util.JSONResponse()` for consistent API responses --- ## Complete Example: Request Logger Plugin [#complete-example-request-logger-plugin] ```go package logger import ( "fmt" "time" "github.com/Authula/authula/models" ) type LoggerPlugin struct { config LoggerPluginConfig logger models.Logger ctx *models.PluginContext } type LoggerPluginConfig struct { Enabled bool `json:"enabled" toml:"enabled"` LogRequests bool `json:"log_requests" toml:"log_requests"` LogLevel string `json:"log_level" toml:"log_level"` } func New(config LoggerPluginConfig) *LoggerPlugin { return &LoggerPlugin{config: config} } func (p *LoggerPlugin) Metadata() models.PluginMetadata { return models.PluginMetadata{ ID: "logger", Version: "1.0.0", Description: "Logs incoming requests with timing", } } func (p *LoggerPlugin) Config() any { return p.config } func (p *LoggerPlugin) Init(ctx *models.PluginContext) error { p.ctx = ctx p.logger = ctx.Logger return nil } func (p *LoggerPlugin) Close() error { return nil } func (p *LoggerPlugin) Hooks() []models.Hook { return []models.Hook{ { Stage: models.HookOnRequest, Matcher: func(rc *models.RequestContext) bool { return p.config.LogRequests }, Handler: p.logStart, Order: 10, }, { Stage: models.HookAfter, Matcher: func(rc *models.RequestContext) bool { return p.config.LogRequests }, Handler: p.logComplete, Order: 20, Async: true, }, } } func (p *LoggerPlugin) logStart(rc *models.RequestContext) error { rc.Values["start_time"] = time.Now() if p.config.LogLevel == "debug" { p.logger.Debug(fmt.Sprintf("→ %s %s from %s", rc.Method, rc.Path, rc.ClientIP)) } return nil } func (p *LoggerPlugin) logComplete(rc *models.RequestContext) error { start, ok := rc.Values["start_time"].(time.Time) if !ok { return nil } userID := "" if rc.Actor != nil && rc.Actor.Type == models.ActorUser { userID = rc.Actor.ID } elapsed := time.Since(start) msg := fmt.Sprintf("← %s %s → %d (%v)", rc.Method, rc.Path, rc.ResponseStatus, elapsed) if userID != "" { msg += fmt.Sprintf(" user=%s", userID) } if rc.ResponseStatus >= 400 { p.logger.Error(msg) } else { p.logger.Info(msg) } return nil } ``` # Welcome **Authula** is an open-source authentication solution that scales with you. Embed it as a library in your Go app, or run it as a standalone auth server with any tech stack. All functionality is delivered through a powerful plugin system, allowing you to compose exactly the authentication stack you need — no more, no less, all built with clean architecture. **Authula** is flexible enough to integrate with any technology stack. It streamlines the implementation of essential security features through a clean, modular architecture, allowing developers to concentrate on building their applications without the overhead of managing authentication complexities. Check out the full README here on [GitHub](https://github.com/Authula/authula) for more details. # Adapters # Overview [#overview] Authula provides adapters for some popular Go HTTP routers and frameworks, allowing you to easily integrate authentication into your applications. These adapters are designed to work seamlessly with the Authula handler, making it easy to mount the authentication routes on your preferred router or framework. Some have adapters and others can be used directly with the handler without needing an adapter. > **Note:** When using adapters, any custom routes you create outside the Authula handler won't have access to the Authula hooks system. To make use of hooks on your custom routes, use the method `auth.RegisterCustomRoute()`. ## Standard Library (net/http) [#standard-library-nethttp] The following example shows how to use the Authula handler with the standard library's `net/http` package. This doesn't require an adapter and can be used directly with the handler provided by Authula. ```go package main import ( "log" "net/http" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" ) func main() { config := authulaconfig.NewConfig(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{}, }) // Mount handler http.Handle("/api/auth/", auth.Handler()) // Start the server and mount the handler log.Fatal(http.ListenAndServe(":8080", nil)) } ``` --- ## Chi [#chi] Authula already uses Chi under the hood. The following example shows how to add more routes to Authula. This doesn't require an adapter and can be used directly with the handler provided by Authula. ```go package main import ( "log" "net/http" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" ) func main() { config := authulaconfig.NewConfig(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{}, }) // Add a custom route. This will be added outside of auth routes such as /api/auth // allowing you to define your own custom routes. auth.RegisterCustomRoute(authulamodels.Route{ Method: "GET", Path: "/api/health", Metadata: map[string]any{}, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reqCtx, _ := authulamodels.GetRequestContext(r.Context()) reqCtx.SetJSONResponse(http.StatusOK, map[string]any{ "status": "ok", }) }), }) log.Fatal(http.ListenAndServe(":8080", auth.Handler())) } ``` If you instead want to mount the handler on a different router, you can do so like this: ```go r := chi.NewRouter() // Replace /auth with your base path r.Handle("/auth/*", auth.Handler()) log.Fatal(http.ListenAndServe(":8080", r)) ``` --- ## Echo [#echo] The following example shows how to use Echo to mount the Authula handler. This allows you to easily integrate Authula into your Echo-based applications and doesn't require an adapter since the handler can be used directly. ```go package main import ( "log" "net/http" "github.com/labstack/echo/v4" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" ) func main() { config := authulaconfig.NewConfig(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{}, }) echoInstance := echo.New() // Mount auth handler echoInstance.Any("/api/auth/*", echo.WrapHandler(auth.Handler())) log.Fatal(echoInstance.Start(":8080")) } ``` --- ## Fiber [#fiber] The following example shows how to use the official Fiber adapter to mount the Authula handler on a Fiber application. ```go package main import ( "log" "github.com/gofiber/fiber/v3" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" fiberadapter "github.com/Authula/authula/adapters/fiber" ) func main() { config := authulaconfig.NewConfig(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{}, }) app := fiber.New() // Mount Authula handler using the Fiber adapter app.Use("/api/auth", fiberadapter.New(fiberadapter.Config{ Handler: auth.Handler(), })) // Add your custom routes here using app.Get, app.Post, etc. log.Fatal(app.Listen(":8080")) } ``` --- # Custom Routes ## Adding Custom Routes [#adding-custom-routes] Authula allows you to add custom API endpoints to your application. This is useful for adding features or handling custom logic. ### How It Works [#how-it-works] You can register custom routes using the `RegisterCustomRoute` method. Each route is defined by a `Route` struct: ```go import ( "net/http" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" ) type Route struct { Method string Path string Handler http.Handler Middleware []func(http.Handler) http.Handler // Metadata holds route-specific metadata, including plugin IDs ("plugins"), // custom tags, and plugin-specific attributes for conditional hook execution. Metadata map[string]any } ``` - **Method**: The HTTP method (e.g., `GET`, `POST`). - **Path**: The endpoint path. - **Handler**: An `http.Handler` for the route. - **Middleware**: A list of middleware functions to apply to the route. - **Metadata**: Additional metadata for the route. ### Example: Adding Custom Endpoints [#example-adding-custom-endpoints] #### 1. GET `/health` [#1-get-health] Health check endpoint. ```go import ( "net/http" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" ) config := authulaconfig.NewConfig(/*...*/) auth := authula.New(config) auth.RegisterCustomRoute(authulamodels.Route{ Method: "GET", Path: "/health", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqCtx, _ := authulamodels.GetRequestContext(ctx) reqCtx.SetJSONResponse(http.StatusOK, map[string]any{ "status": "ok", }) }), // Optional // Middleware: []func(http.Handler) http.Handler{}, // Optional // Metadata: map[string]any{}, }) ``` #### 2. POST `/send-message` [#2-post-send-message] Accepts a JSON body and returns it in the response. ```go import ( "net/http" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" authulainternalutils "github.com/Authula/authula/internal/util" ) config := authulaconfig.NewConfig(/*...*/) auth := authula.New(config) type SendMessageRequest struct { Message string `json:"message"` } auth.RegisterCustomRoute(authulamodels.Route{ Method: "POST", Path: "/send-message", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqCtx, _ := authulamodels.GetRequestContext(ctx) var request SendMessageRequest if err := authulainternalutils.ParseJSON(r, &request); err != nil { reqCtx.SetJSONResponse(http.StatusUnprocessableEntity, map[string]any{ "message": "invalid request body", }) reqCtx.Handled = true return } reqCtx.SetJSONResponse(http.StatusOK, map[string]any{ "message": "Data received", "data": request, }) }), // Middleware: []func(http.Handler) http.Handler{}, Metadata: map[string]any{ "plugins": []string{ "csrf.protect", }, }, }) ``` #### Request Context [#request-context] When handling requests in custom routes, you can access the custom `RequestContext` to pass structured request data through the request lifecycle. It encapsulates request-related information, provides hook control mechanisms, and is available inside route handlers. Using `RequestContext` is recommended because it includes useful utility methods and is the main request object used throughout the HTTP request/response lifecycle. Changes made to it persist for the duration of the request, so if a hook has already modified data in `RequestContext`, your custom route can access the updated values. ```go import ( "net/http" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" ) auth.RegisterCustomRoute(authulamodels.Route{ Method: "GET", Path: "/api/custom", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // You can access it like so: ctx := r.Context() reqCtx, _ := authulamodels.GetRequestContext(ctx) reqCtx.SetJSONResponse(http.StatusOK, map[string]any{ "status": "ok", }) }), }) ``` ### Step-by-Step Guide [#step-by-step-guide] 1. **Define your custom route** using the `Route` struct. 2. **Implement the handler** function as an `http.Handler`. 3. **Register the route** with `auth.RegisterCustomRoute(...)`. 4. Your endpoint will be available at the specified path. ### Notes [#notes] - Custom routes are not protected by authentication by default; implement authentication via middleware or metadata by adding the plugins you want with their capabilities. - You can add as many custom routes as needed. # Hooks ## Overview [#overview] The hooks system in Authula provides a flexible and extensible mechanism for plugins to intercept and modify the request lifecycle. Hooks allow you to execute custom logic at specific points during request processing without tightly coupling your code to the router. The system enables powerful capabilities such as authentication, authorization, rate limiting, logging, and custom business logic integration. Each hook operates within the request context and can modify request state, share data with other hooks, or short-circuit the request flow. ## Hook Stages [#hook-stages] Hooks execute at four distinct stages in the request lifecycle: ### 1. `HookOnRequest` [#1-hookonrequest] - Executes for every incoming request at the very start - Ideal for global logging, metrics collection, or request preprocessing - Runs before route matching occurs ### 2. `HookBefore` [#2-hookbefore] - Executes after route matching but before the main route handler - Perfect for authentication, authorization, and request validation - Common use: checking session tokens, validating permissions ### 3. `HookAfter` [#3-hookafter] - Executes after the route handler but before the response is sent - Useful for response modification, post-processing, or cleanup - Good for adding response headers or transforming output ### 4. `HookOnResponse` [#4-hookonresponse] - Executes after the response has been written to the client - Suitable for analytics, audit logging, or fire-and-forget operations - Does not affect the response sent to the client ## Configuration Options [#configuration-options] ### Synchronization Types [#synchronization-types] Hooks can be either synchronous or asynchronous: ```go // Synchronous hook (default) Hook{ Async: false, // Blocks the request flow until completion Handler: myHandler, } // Asynchronous hook Hook{ Async: true, // Runs in a background goroutine Handler: myAsyncHandler, } ``` - **Synchronous**: Blocks the request flow and waits for completion. Use for authentication, validation, and critical security checks. - **Asynchronous**: Runs in a background goroutine without blocking the response. Ideal for logging, analytics, and side effects. ### Error Handling Modes [#error-handling-modes] Configure how the router handles hook errors: ```go opts := &RouterOptions{ HookErrorMode: HookErrorModeContinue, // Default: log errors, continue execution } ``` - **`HookErrorModeContinue`** (default): Logs errors but continues to the next hook - **`HookErrorModeFailFast`**: Logs error and stops remaining hooks in the current stage - **`HookErrorModeSilent`**: Silently ignores errors without logging ### Async Hook Timeout [#async-hook-timeout] Control how long async hooks can run before being cancelled: ```go opts := &RouterOptions{ AsyncHookTimeout: 30 * time.Second, // Default timeout } ``` ## Hook Structure [#hook-structure] Each hook is defined with the following properties: ```go type Hook struct { Stage HookStage // When to execute (HookOnRequest, HookBefore, etc.) PluginID string // Optional: limits execution to specific routes Matcher HookMatcher // Optional: conditional execution based on request Handler HookHandler // The function that executes the hook Order int // Execution order within the same stage Async bool // Whether to run asynchronously } ``` ### Properties Explained [#properties-explained] - **Stage**: Determines when the hook executes in the request lifecycle - **PluginID**: If set, hook only executes if the route's metadata includes this plugin ID - **Matcher**: A function that returns true/false to conditionally execute the hook - **Handler**: The actual function that performs the hook's logic - **Order**: Controls execution order among hooks in the same stage (lower numbers first) - **Async**: Whether the hook runs synchronously or asynchronously ## Practical Examples [#practical-examples] ### Basic Authentication Hook [#basic-authentication-hook] ```go // Simple authentication hook that checks for a valid session authHook := models.Hook{ Stage: models.HookBefore, Order: 0, Handler: func(ctx *models.RequestContext) error { // Check if user is authenticated sessionToken := ctx.Headers.Get("Authorization") if sessionToken == "" { ctx.SetResponse(401, nil, []byte(`{"error": "Unauthorized"}`)) ctx.Handled = true return nil } // Validate session and set user info userID, err := validateSession(sessionToken) if err != nil { ctx.SetResponse(401, nil, []byte(`{"error": "Invalid session"}`)) ctx.Handled = true return nil } // Store user ID in context for later use ctx.SetUserIDInContext(userID) return nil }, } ``` ### Conditional Hook with Matcher [#conditional-hook-with-matcher] ```go // Only execute hook for admin routes adminOnlyHook := models.Hook{ Stage: models.HookBefore, Matcher: func(reqCtx *models.RequestContext) bool { // Only run for paths starting with /admin return strings.HasPrefix(reqCtx.Path, "/admin") }, Handler: func(reqCtx *models.RequestContext) error { if reqCtx.Actor == nil || reqCtx.Actor.Type != models.ActorUser { reqCtx.SetJSONResponse(http.StatusUnauthorized, map[string]any{"message": "Authentication required"}) reqCtx.Handled = true return nil } userID := reqCtx.Actor.ID // Check if user has admin role isAdmin, err := hasRole(userID, "admin") if err != nil || !isAdmin { reqCtx.SetJSONResponse(http.StatusForbidden, map[string]any{"message": "Admin access required"}) reqCtx.Handled = true return nil } return nil }, } ``` ### Data Sharing Between Hooks [#data-sharing-between-hooks] ```go // First hook sets data in the Values map setUserDataHook := models.Hook{ Stage: models.HookBefore, Order: 0, Handler: func(reqCtx *models.RequestContext) error { if reqCtx.Actor == nil || reqCtx.Actor.Type != models.ActorUser { return nil // No user, skip } userID := reqCtx.Actor.ID if userID != "" { // Fetch user profile and store in Values for other hooks userProfile, err := getUserProfile(userID) if err != nil { return err } reqCtx.Values["user_profile"] = userProfile } return nil }, } // Second hook reads the shared data auditLogHook := models.Hook{ Stage: models.HookAfter, Order: 1, // Must run after setUserDataHook Handler: func(reqCtx *models.RequestContext) error { if profile, exists := reqCtx.Values["user_profile"]; exists { // Log the action with user information logAuditEvent(profile.(UserProfile), reqCtx.Path, reqCtx.Method) } return nil }, } ``` ### Asynchronous Analytics Hook [#asynchronous-analytics-hook] ```go // Fire-and-forget analytics hook analyticsHook := models.Hook{ Stage: models.HookOnResponse, Async: true, Handler: func(reqCtx *models.RequestContext) error { if reqCtx.Actor == nil || reqCtx.Actor.Type != models.ActorUser { return nil // No user, skip analytics } // Track page views, user actions, etc. trackEvent("page_view", map[string]interface{}{ "path": reqCtx.Path, "method": reqCtx.Method, "clientIP": reqCtx.ClientIP, "userID": reqCtx.Actor.ID, }) return nil }, } ``` ### Plugin-Specific Hook [#plugin-specific-hook] ```go // Hook that only runs for routes that include this plugin rateLimitHook := models.Hook{ Stage: models.HookBefore, PluginID: "ratelimit", Order: 0, Handler: func(reqCtx *models.RequestContext) error { // Apply rate limiting only to routes that have "ratelimit" in their metadata if err := checkRateLimit(reqCtx.ClientIP); err != nil { reqCtx.SetJSONResponse(http.StatusTooManyRequests, map[string]any{"message": "Rate limit exceeded"}) reqCtx.Handled = true return nil } return nil }, } ``` ## Best Practices [#best-practices] ### 1. Order Matters [#1-order-matters] - Use lower `Order` values for security-critical hooks (authentication, authorization) - Higher `Order` values for less critical operations - Remember that order is local to each plugin, so compare within the same `PluginID` ### 2. Handle Errors Gracefully [#2-handle-errors-gracefully] - Always return appropriate errors from your handlers - Use `ctx.Handled = true` to short-circuit when returning an error response - Consider the error handling mode when designing your hooks ### 3. Use Asynchronous Hooks for Side Effects [#3-use-asynchronous-hooks-for-side-effects] - Use `Async: true` for logging, analytics, notifications - Keep synchronous hooks for authentication, validation, and response modification - Be mindful of the timeout for async hooks ### 4. Leverage the Values Map [#4-leverage-the-values-map] - Use `reqCtx.Values` to share data between hooks - Store computed values that other hooks might need - Avoid storing sensitive information in Values ### 5. Use Matchers for Conditional Logic [#5-use-matchers-for-conditional-logic] - Implement `Matcher` functions to conditionally execute hooks - This is more efficient than checking conditions inside the handler - Keep matchers lightweight to avoid performance overhead ### 6. Plugin Integration [#6-plugin-integration] - Use `PluginID` to ensure hooks only run on relevant routes - Coordinate with route metadata to enable/disable hooks per endpoint - Follow the plugin architecture patterns for consistency ## Use Cases [#use-cases] ### Authentication & Authorization [#authentication--authorization] - Session validation - JWT verification - Role-based access control - Permission checking ### Security [#security] - Rate limiting - CSRF protection - Input validation - IP blacklisting ### Monitoring & Analytics [#monitoring--analytics] - Request logging - Performance metrics - User activity tracking - Error monitoring ### Business Logic [#business-logic] - Custom validation rules - Data enrichment - Audit trails - Notification triggers The hooks system provides a powerful and flexible foundation for extending Authula's functionality while maintaining clean separation of concerns between different aspects of your authentication and authorization logic. NOTE: this documentation explains a general overview of the hooks system but bear in mind that Authula may already contain a lot of this functionality already as plugins so you may not need to implement the things that are mentioned under "use cases" in your own project etc. So take a look at the [Plugins](/docs/guides/plugins) documentation next to see what is already available! # Middleware Authula provides middleware via plugins. Examples of plugins with middleware and what it does: - **Session Plugin Middleware**: checks if the user is authenticated by validating the session cookie. - **Bearer Plugin Middleware**: checks if the user is authenticated by validating bearer tokens in a header, typically `Authorization` header. This is dependent on the JWT Plugin being in use. - **CSRF**: protects state-changing (POST, PUT, PATCH, DELETE) endpoints with CSRF tokens. # Service Hooks Authula provides powerful hooks to let you run custom logic before and after service operations for users, accounts, sessions, verifications and more. This allows you to integrate with your own systems, perform validations, trigger side effects, or log activity. ## Config [#config] ```go type ServiceHook[T any] func(*T) error type CoreServiceHooksConfig struct { Users *ServiceHooks[User] Accounts *ServiceHooks[Account] Sessions *ServiceHooks[Session] Verifications *ServiceHooks[Verification] } type ServiceHooks[T any] struct { beforeCreate []ServiceHook[T] afterCreate []ServiceHook[T] beforeUpdate []ServiceHook[T] afterUpdate []ServiceHook[T] } func (h *ServiceHooks[T]) RegisterBeforeCreate(fn ServiceHook[T]) { h.beforeCreate = append(h.beforeCreate, fn) } func (h *ServiceHooks[T]) RegisterAfterCreate(fn ServiceHook[T]) { h.afterCreate = append(h.afterCreate, fn) } func (h *ServiceHooks[T]) RegisterBeforeUpdate(fn ServiceHook[T]) { h.beforeUpdate = append(h.beforeUpdate, fn) } func (h *ServiceHooks[T]) RegisterAfterUpdate(fn ServiceHook[T]) { h.afterUpdate = append(h.afterUpdate, fn) } func (h *ServiceHooks[T]) BeforeCreateHooks() []ServiceHook[T] { return h.beforeCreate } func (h *ServiceHooks[T]) AfterCreateHooks() []ServiceHook[T] { return h.afterCreate } func (h *ServiceHooks[T]) BeforeUpdateHooks() []ServiceHook[T] { return h.beforeUpdate } func (h *ServiceHooks[T]) AfterUpdateHooks() []ServiceHook[T] { return h.afterUpdate } ``` ## Example: [#example] Suppose you want to log every time a user is created in your system. You can register an `AfterCreate` hook on the `ServiceHooks[User]`. ```go import ( "log" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" ) func logUserCreation(user *authulamodels.User) error { log.Printf("New user created - ID: %s, Email: %s", user.ID, user.Email) return nil } config := authulaconfig.NewConfig( authulaconfig.WithCoreServiceHooks(authulamodels.CoreServiceHooksConfig{ Users: &authulamodels.ServiceHooks[authulamodels.User]{}, }), ) config.CoreServiceHooks.Users.RegisterAfterCreate(logUserCreation) ``` ## Available Service Hooks [#available-service-hooks] You can hook into the following events: - **Users**: `RegisterBeforeCreate`, `RegisterAfterCreate`, `RegisterBeforeUpdate`, `RegisterAfterUpdate` - **Accounts**: `RegisterBeforeCreate`, `RegisterAfterCreate`, `RegisterBeforeUpdate`, `RegisterAfterUpdate` - **Sessions**: `RegisterBeforeCreate`, `RegisterAfterCreate`, `RegisterBeforeUpdate`, `RegisterAfterUpdate` - **Verifications**: `RegisterBeforeCreate`, `RegisterAfterCreate` Each hook receives a pointer to the entity being created/updated. For "before" hooks, you can make direct modifications to the entity before it is stored. For "after" hooks, you can read the entity to perform side effects or log activity. You can return an error from any hook to abort the operation. ## How to Integrate [#how-to-integrate] 1. **Implement your hook functions** matching the `ServiceHook[T]` signature, where `T` is the entity type (e.g. `User`). 2. **Register them** on the appropriate service hooks struct using its `Register*` method. 3. **Pass the hooks struct** to the matching field in the `CoreServiceHooksConfig` when creating your Authula config. 4. **Return an error** from your hook to abort the operation, or `nil` to continue. See the [Config Reference](/docs/reference/config) for the full structure and all available hooks. # Architecture Authula follows **Clean Architecture** principles with built-in HTTP handlers: ``` ┌─────────────────────────────────────────────────────────────┐ │ Authula Library │ ├─────────────────────────────────────────────────────────────┤ │ Standalone HTTP Handlers │ │ • Built-in http.Handler implementation │ │ • auth.Handler() returns ready-to-use handler │ │ • Mount on any standard Mux HTTP server │ │ • Uses Chi router under the hood │ │─────────────────────────────────────────────────────────────│ │ Database Adapters │ ├─────────────────────────────────────────────────────────────┤ │ • SQLite │ │ • PostgreSQL │ │ • MySQL │ └─────────────────────────────────────────────────────────────┘ ``` ## Key Design Principles [#key-design-principles] 1. **Framework-Agnostic Core**: Clean separation between business logic and HTTP concerns 2. **Standard Library First**: Handlers implement `net/http.Handler` interface ## Component Layers [#component-layers] ### Models Layer [#models-layer] - **Entities**: User, Account, Session, Verification and more ### Handler Layer [#handler-layer] - Entrypoint for API endpoints, delegates logic to either use cases in some scenarios which call one or multiple services. Or if the handler only has a dependency on one service then it delegates to that single service without the need for a use case. Services handle business logic and delegate data access to repositories providing clear separation of concerns. - Integrated with hooks for extensibility ### Use Cases Layer [#use-cases-layer] - Use cases (interfaces), services (interfaces with concrete implementations for business logic), repositories (handle database access). ### Database Layer [#database-layer] - [Bun ORM](https://bun.uptrace.dev/) for database interactions with support for multiple databases ### Extensibility [#extensibility] - Various hooks into the authentication flow for customisation - Plugin system for custom logic and extensions - Event Bus for event-driven architecture # Comparison Authula is a batteries-included authentication framework for Go. It unifies common auth needs in the form of powerful and flexible plugins that can practically handle anything related to auth. Some of which come built-in such as: email/password with various flows, OAuth providers, session and token handling, hooks, plugins, and adapters for databases/Redis so teams avoid stitching many small libraries together. It focuses solely on the authentication layer — a crucial part of any app — and is designed to be stable, secure, and production-ready. You can adopt Authula without replacing other parts of your stack; it integrates cleanly with existing services and frameworks. It emphasises performance, low operational overhead, and an explicit extension model (hooks, event bus, plugins). ## Key advantages [#key-advantages] - **Integrated:** Single API for registration, sessions, social logins, webhooks and lifecycle hooks — less glue code. - **Extensible:** First-class plugin and event APIs for safe customisation without forking core code. - **Secure defaults:** Modern practices and plugins giving you core authentication primitives out of the box so you don't need to reinvent the wheel. - **Low ops & performance:** Go binaries are small, fast, and easy to deploy — ideal where latency and resource use matter. ## Benefits in the Go Ecosystem [#benefits-in-the-go-ecosystem] The Go community has long struggled with fragmented auth tools, often requiring integrations with external services or custom code that increases technical debt. A library like Authula is invaluable because: - **Rapid Development:** Its configurability accelerates prototyping and production deployment, allowing teams to focus on business logic rather than auth boilerplate. - **Scalability and Performance:** Built with Go's concurrency in mind, it handles high-load scenarios efficiently, unlike some heavier frameworks that introduce unnecessary overhead. - **Community and Extensibility:** With support for plugins and hooks, it fosters a vibrant ecosystem where developers can extend functionality (e.g., adding custom plugins and more) without forking the library. In summary, Authula isn't just another auth library — it's a game-changer for Go developers, offering the flexibility and robustness the ecosystem needs to build secure, user-friendly applications. ## How it compares [#how-it-compares] - **Single-purpose Go libraries (goth, oauth2, jwt, session middleware):** Those are focused and composable, but building a full, secure system requires substantial glue (user model, migrations, verification emails, rate limits). Authula provides the glue and conventions so teams ship faster and maintain less glue code. - **Framework-bound or monolithic OSS solutions:** Framework-tied tools may lock you to a runtime or server model. Authula supports both in-process embedding and a standalone service model, so you can start embedded and migrate to a central service with the same semantics. - **Managed providers (Auth0, Firebase, hosted offerings):** Hosted services reduce ops and give enterprise features but add vendor lock-in, cost at scale, and limited deep customization. Self-hosting Authula gives full control over data, customization, and costs. ## Go vs Others (Python/Node) [#go-vs-others-pythonnode] - **Performance & footprint:** Go apps compile to static binaries with low memory use and predictable latency — typically lower runtime overhead than others for equivalent workloads. - **Deployment & ops:** Go produces single binaries that are easy to deploy and observe; others require a runtime and typically more complex dependency management and container sizing. - **Type safety & reliability:** Go’s static typing and compilation catch many errors early. Others provide type safety too, but transpilation and runtime ecosystem differences can lead to more variability across deployments. - **Ecosystem breadth:** Other ecosystems may offer a larger number of third-party packages and faster churn, but many of those packages vary in maintenance and quality. Go provides a mature standard library and a stable, well-maintained ecosystem that reduces churn and lowers maintenance risk while still enabling extensive integrations. - **Smaller dependency surface & security:** Go’s rich standard library and tendency toward fewer third‑party packages mean fewer external dependencies to audit and smaller transitive dependency trees — reducing the overall attack surface compared with ecosystems (Node/Python) that commonly pull in many packages. - **Extensibility & speed of iteration:** Others may allow faster prototyping due to ecosystem tooling; Authula focuses on safe, explicit extension points for production-grade customisation. When to pick which: - Choose Authula (Go) when you prioritise low-latency, low-resource deployments, tight control over auth logic/data residency, and a production-ready extensibility model. Note: choosing Authula doesn't require you to replace other parts of your stack — it is intentionally scoped to the auth layer and is designed to integrate with other tech stacks e.g. outsourcing Auth to Authula. - Choose other stacks when you need the sheer quantity of community packages (npm, pypi etc.) and prefer an ecosystem tailored for rapid prototyping; note that package maintenance and long-term stability can vary across those ecosystems. ## Pragmatic guidance [#pragmatic-guidance] - Use Authula if you want fewer moving parts in production, deterministic performance, and strong, explicit extension points. It can be adopted as the authentication layer while you keep other services and frameworks in place. - Use smaller libraries when your needs are extremely narrow and you prefer tiny dependencies. - Use a hosted provider when you need SLA-backed features, compliance, and minimal engineering overhead, and vendor lock-in is acceptable. ## Bottom line [#bottom-line] Authula reduces glue code and operational complexity while offering a flexible, production-focused extension model. Go is mature and increasingly adopted for performance-sensitive back-end and infrastructure work (including parts of AI stacks), giving projects stability, predictable operations, and a smaller runtime surface. Compared to other solutions, Authula leverages Go's mature standard library and stable ecosystem to deliver low runtime overhead and simple deployments while providing reliable integrations and long-term stability — choose based on your team's priorities. # Access Control Plugin ## Overview [#overview] The **Access Control plugin** provides a comprehensive **Role-Based Access Control (RBAC)** system for managing authorization in your application. It enables you to define roles, assign permissions to those roles, and grant roles to users, creating a flexible and scalable authorization model. ### What It Does [#what-it-does] - **Role Management** — Create and manage roles. Supports system-managed roles that cannot be modified or deleted once created. Roles also support adding a weight to determine their hierarchy and precedence. This is reminicent of the concept of "role hierarchy" in traditional RBAC systems, but implemented with a flexible weight system that allows for more granular control. - **Permission Management** — Define fine-grained permissions with unique keys - **Role-Permission Mapping** — Associate permissions with roles and manage bulk updates - **User Authorization** — Assign roles to users and query their effective permissions - **Audit Tracking** — Track who granted permissions and assigned roles The plugin uses a hierarchical model: Users → Roles → Permissions, allowing you to manage authorization at any level and simplifying policy changes across multiple users. --- ## Configuration [#configuration] ### Standalone Mode [#standalone-mode] ```toml [plugins.access_control] enabled = true ``` ### Library Mode [#library-mode] ```go accesscontrolplugin "github.com/Authula/authula/plugins/access-control" accesscontrolplugintypes "github.com/Authula/authula/plugins/access-control/types" accesscontrolplugin.New(accesscontrolplugintypes.AccessControlPluginConfig{ Enabled: true, }) ``` --- ## API Reference [#api-reference] Each endpoint in this plugin requires the caller to have a specific hardcoded permission assigned to their role(s). ### Roles Management [#roles-management] | HTTP Method | Route Path | Description | Required Permission | | ----------- | ------------------------------------------- | ------------------------ | ----------------------------- | | `POST` | `/access-control/roles` | Create a new role | `access-control:roles:create` | | `GET` | `/access-control/roles` | List all available roles | `access-control:roles:list` | | `GET` | `/access-control/roles/by-name/{role_name}` | Get a role by name | `access-control:roles:read` | | `GET` | `/access-control/roles/{role_id}` | Get a role by ID | `access-control:roles:read` | | `PATCH` | `/access-control/roles/{role_id}` | Update a role | `access-control:roles:update` | | `DELETE` | `/access-control/roles/{role_id}` | Delete a role | `access-control:roles:delete` | ### Permissions Management [#permissions-management] | HTTP Method | Route Path | Description | Required Permission | | ----------- | --------------------------------------------- | ----------------------- | ----------------------------------- | | `POST` | `/access-control/permissions` | Create a new permission | `access-control:permissions:create` | | `GET` | `/access-control/permissions` | List all permissions | `access-control:permissions:list` | | `GET` | `/access-control/permissions/{permission_id}` | Get a permission by ID | `access-control:permissions:read` | | `PATCH` | `/access-control/permissions/{permission_id}` | Update a permission | `access-control:permissions:update` | | `DELETE` | `/access-control/permissions/{permission_id}` | Delete a permission | `access-control:permissions:delete` | ### Role-Permission Mapping [#role-permission-mapping] | HTTP Method | Route Path | Description | Required Permission | | ----------- | ------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------- | | `POST` | `/access-control/roles/{role_id}/permissions` | Add a single permission to a role | `access-control:role-permissions:assign` | | `GET` | `/access-control/roles/{role_id}/permissions` | Get all permissions assigned to a role | `access-control:role-permissions:read` | | `PUT` | `/access-control/roles/{role_id}/permissions` | Replace all permissions of a role (bulk update) | `access-control:role-permissions:assign` | | `DELETE` | `/access-control/roles/{role_id}/permissions/{permission_id}` | Remove a permission from a role | `access-control:role-permissions:remove` | ### User Access & Permissions [#user-access--permissions] | HTTP Method | Route Path | Description | Required Permission | | ----------- | --------------------------------------------------- | ----------------------------------------- | --------------------------------------- | | `GET` | `/access-control/users/{user_id}/roles` | Get all roles assigned to a user | `access-control:user-roles:read` | | `POST` | `/access-control/users/{user_id}/roles` | Assign a role to a user | `access-control:user-roles:assign` | | `PUT` | `/access-control/users/{user_id}/roles` | Replace all roles of a user (bulk update) | `access-control:user-roles:assign` | | `DELETE` | `/access-control/users/{user_id}/roles/{role_id}` | Remove a role from a user | `access-control:user-roles:remove` | | `GET` | `/access-control/users/{user_id}/permissions` | Get all permissions for a user | `access-control:user-permissions:read` | | `POST` | `/access-control/users/{user_id}/permissions/check` | Check whether a user has permissions | `access-control:user-permissions:check` | --- ## Database Schema [#database-schema] This plugin creates the following database tables: Table: `access_control_roles` | Field | Type | Key | Description | | ------------- | --------- | --- | ------------------------------ | | `id` | string | PK | Unique identifier for the role | | `name` | string | - | Name of the role | | `description` | string? | - | Role description | | `weight` | int | - | Role weight for hierarchy | | `is_system` | boolean | - | Whether role is system-managed | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | Table: `access_control_permissions` | Field | Type | Key | Description | | ----------- | --------- | --- | ------------------------------------ | | id | string | PK | Unique identifier for permission | | key | string | - | Permission key identifier | | description | string? | - | Permission description | | is_system | boolean | - | Whether permission is system-managed | | created_at | timestamp | - | Record creation time | | updated_at | timestamp | - | Record last update time | Table: `access_control_role_permissions` | Field | Type | Key | Description | | ------------------ | --------- | --- | -------------------------------- | | role_id | string | PK | Role identifier | | permission_id | string | PK | Permission identifier | | granted_by_user_id | string? | FK | User who granted the permission | | granted_at | timestamp | - | Time when permission was granted | Table: `access_control_user_roles` | Field | Type | Key | Description | | ------------------- | ---------- | --- | --------------------------------- | | user_id | string | PK | User identifier | | role_id | string | PK | Role identifier | | assigned_by_user_id | string? | FK | User who assigned the role | | assigned_at | timestamp | - | Time when role was assigned | | expires_at | timestamp? | - | Time when role assignment expires | Migrations are automatically handled when the plugin is initialized. --- ## Plugin Capabilities [#plugin-capabilities] #### Example Usage [#example-usage] `Standalone mode`: ```toml [[route_mappings]] path = "/some/path" method = "GET" # Make sure to include an auth capability plugin (e.g. session.auth) before the access control plugin # to ensure the user is authenticated before enforcing permissions. plugins = ["session.auth"] permissions = ["domain:resource:action"] # Specify the required custom permission to access this route ``` `Library mode`: ```go import ( authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" sessionplugin "github.com/Authula/authula/plugins/session" accesscontrolplugin "github.com/Authula/authula/plugins/access-control" ) config := authulaconfig.NewConfig( // Other config options... authulaconfig.WithRouteMappings([]authulamodels.RouteMapping{ { Method: "GET", Path: "/some/path", Plugins: []string{ sessionplugin.HookIDSessionAuth.String(), }, Permissions: []string{ "domain:resource:action", }, }, }), ) ``` --- ## Notes [#notes] - System-managed roles and permissions are protected from modification or deletion to ensure critical access controls remain intact. - Role weights allow you to establish a hierarchy, where higher-weighted roles take precedence over lower-weighted ones. This is very useful for scenarios where users may have multiple roles, and you want to determine which role is of higher priority. Also this is important to take into consideration as a user can only grant roles with equal or lower weight than their highest role, preventing privilege escalation. - An example of this is an `Admin` role with weight `100`, `Editor` role with weight `50`, and `Viewer` role with weight `25`. An `Admin` can assign any of these roles, while an `Editor` can only assign the `Viewer` role. --- ## Client Plugin [#client-plugin] If you're using the [Authula SDK](https://github.com/Authula/authula-node-sdk), add the plugin to the SDK like so: ```typescript import { createClient } from "authula"; import { AccessControlPlugin } from "authula/plugins"; export const authulaClient = createClient({ url: "http://localhost:8080/auth", plugins: [ // other plugins... new AccessControlPlugin(), ], }); ``` # Admin Plugin ## Overview [#overview] The Admin plugin provides administrative operations for managing users, accounts, sessions, and user impersonation. It enables system administrators to perform CRUD operations on users and accounts, manage user and session states (banning, revoking sessions), control impersonation activities, and maintain comprehensive audit trails. ### Features [#features] - **User Management** — Create, read, update, and delete users with full lifecycle support - **Account Management** — Manage linked provider accounts for users - **User State Management** — Ban and unban users with optional expiration and reason tracking - **Session State Management** — Revoke sessions with audit trail and reason tracking - **User Impersonation** — Admin impersonation of users with time-limited sessions and audit logging - **Audit Trail** — Comprehensive tracking of all administrative actions --- ## Configuration [#configuration] ### Standalone Mode [#standalone-mode] ```toml [plugins.admin] enabled = true impersonation_max_expires_in = "15m" ``` ### Library Mode [#library-mode] ```go import ( adminplugin "github.com/Authula/authula/plugins/admin" adminplugintypes "github.com/Authula/authula/plugins/admin/types" ) adminplugin.New(adminplugintypes.AdminPluginConfig{ Enabled: true, ImpersonationMaxExpiresIn: 15 * time.Minute, }), ``` --- ## API Reference [#api-reference] Each endpoint in this plugin requires the caller to have a specific hardcoded permission assigned to their role(s). ### User Management [#user-management] | Method | Endpoint | Description | Required Permission | | -------- | ------------------------ | ------------------------------------------- | ------------------------ | | `POST` | `/admin/users` | Create a new user | `admin:users:create` | | `GET` | `/admin/users` | List all users with cursor-based pagination | `admin:users:list` | | `GET` | `/admin/users/{user_id}` | Get a specific user by ID | `admin:users:read` | | `PATCH` | `/admin/users/{user_id}` | Update user details | `admin:users:update` | | `DELETE` | `/admin/users/{user_id}` | Delete a user | `admin:users:delete` | ### Account Management [#account-management] | Method | Endpoint | Description | Required Permission | | -------- | --------------------------------- | ------------------------------------------------- | ---------------------------- | | `POST` | `/admin/users/{user_id}/accounts` | Create a linked provider account for a user | `admin:accounts:create` | | `GET` | `/admin/users/{user_id}/accounts` | List all accounts associated with a user | `admin:accounts:list` | | `GET` | `/admin/accounts/{id}` | Get a specific account by ID | `admin:accounts:read` | | `PATCH` | `/admin/accounts/{id}` | Update account details | `admin:accounts:update` | | `DELETE` | `/admin/accounts/{id}` | Delete a user account and remove provider linkage | `admin:accounts:delete` | ### User State Management [#user-state-management] | Method | Endpoint | Description | Required Permission | | -------- | ------------------------------ | ------------------------------------------------------ | ----------------------------------- | | `GET` | `/admin/users/{user_id}/state` | Get user state including banned status and ban details | `admin:user-state:read` | | `POST` | `/admin/users/{user_id}/state` | Create user state record | `admin:user-state:create` | | `PATCH` | `/admin/users/{user_id}/state` | Update user state | `admin:user-state:update` | | `DELETE` | `/admin/users/{user_id}/state` | Delete user state record | `admin:user-state:delete` | | `GET` | `/admin/users/states/banned` | List all banned users | `admin:user-state:list-banned` | | `POST` | `/admin/users/{user_id}/ban` | Ban a user with optional expiration and reason | `admin:user-state:ban` | | `POST` | `/admin/users/{user_id}/unban` | Remove ban from a user | `admin:user-state:unban` | ### Session State Management [#session-state-management] | Method | Endpoint | Description | Required Permission | | -------- | ------------------------------------- | ----------------------------------------------- | -------------------------------------- | | `GET` | `/admin/sessions/{session_id}/state` | Get session state including revocation status | `admin:session-state:read` | | `POST` | `/admin/sessions/{session_id}/state` | Create session state record | `admin:session-state:create` | | `PATCH` | `/admin/sessions/{session_id}/state` | Update session state | `admin:session-state:update` | | `DELETE` | `/admin/sessions/{session_id}/state` | Delete session state record | `admin:session-state:delete` | | `POST` | `/admin/sessions/{session_id}/revoke` | Revoke a session with optional reason | `admin:session-state:revoke` | | `GET` | `/admin/sessions/states/revoked` | List all revoked sessions with session state | `admin:session-state:list-revoked` | | `GET` | `/admin/users/{user_id}/sessions` | List all sessions with session state for a user | `admin:user-state:list-sessions` | ### Impersonation Management [#impersonation-management] | Method | Endpoint | Description | Required Permission | | ------ | ----------------------------------------------- | ---------------------------------------------------- | -------------------------------- | | `GET` | `/admin/impersonations` | List all active impersonations | `admin:impersonations:list` | | `GET` | `/admin/impersonations/{impersonation_id}` | Get a specific impersonation by ID | `admin:impersonations:read` | | `POST` | `/admin/impersonations` | Start impersonating a user with audit trail | `admin:impersonations:start` | | `POST` | `/admin/impersonations/{impersonation_id}/stop` | End impersonation and restore original admin session | `admin:impersonations:stop` | --- ## Database Schema [#database-schema] ### Table: `admin_impersonations` [#table-admin_impersonations] | Field | Type | Key | Description | | -------------------------- | ---------- | --- | ---------------------------------------------------- | | `id` | string | PK | Unique identifier for the impersonation record | | `actor_user_id` | string | FK | Reference to the admin user initiating impersonation | | `target_user_id` | string | FK | Reference to the user being impersonated | | `actor_session_id` | string? | FK | Reference to the admin's original session | | `impersonation_session_id` | string? | FK | Reference to the impersonation session | | `reason` | string | - | Reason for impersonation | | `started_at` | timestamp | - | When impersonation began | | `expires_at` | timestamp | - | When impersonation expires | | `ended_at` | timestamp? | - | When impersonation ended | | `ended_by_user_id` | string? | FK | Reference to the user who ended impersonation | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | ### Table: `admin_user_states` [#table-admin_user_states] | Field | Type | Key | Description | | ------------------- | ---------- | --- | ----------------------------------------- | | `user_id` | string | PK | Reference to the user | | `banned` | boolean | - | Whether the user is currently banned | | `banned_at` | timestamp? | - | When the ban was issued | | `banned_until` | timestamp? | - | When the ban expires (if temporary) | | `banned_reason` | string? | - | Reason for the ban | | `banned_by_user_id` | string? | FK | Reference to the admin who issued the ban | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | ### Table: `admin_session_states` [#table-admin_session_states] | Field | Type | Key | Description | | -------------------------- | ---------- | --- | ---------------------------------------------------- | | `session_id` | string | PK | Reference to the session | | `revoked_at` | timestamp? | - | When the session was revoked | | `revoked_reason` | string? | - | Reason for revocation | | `revoked_by_user_id` | string? | FK | Reference to the admin who revoked the session | | `impersonator_user_id` | string? | FK | Reference to the impersonating admin (if applicable) | | `impersonation_reason` | string? | - | Reason for impersonation (if applicable) | | `impersonation_expires_at` | timestamp? | - | When impersonation expires (if applicable) | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | > Migrations are automatically handled when the plugin is initialized. --- ## Plugin Capabilities [#plugin-capabilities] The Admin plugin provides the following capabilities: - **User CRUD Operations** — Full lifecycle management for user accounts - **Account Linking** — Manage external provider account associations - **User Banning** — Temporary or permanent user suspension with reason tracking - **Session Revocation** — Invalidate sessions with audit trail - **User Impersonation** — Admin impersonation with time-limited sessions and comprehensive logging - **Audit Logging** — All administrative actions are tracked for compliance and debugging ### Impersonation Lifecycle [#impersonation-lifecycle] ``` ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Start │────▶│ Impersonation│────▶│ Active │ │ Impersonation│ │ Created │ │ Session │ └─────────────┘ └──────────────┘ └──────┬──────┘ │ ┌─────────────────────────┼──────────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐ │ Expiration │ │ Manual │ │ Target │ │ (Timeout) │ │ Stop │ │ Deleted │ └──────┬───────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐ │ Session │ │ Session │ │ Session │ │ Ended │ │ Ended │ │ Ended │ └──────────────┘ └─────────────┘ └─────────────┘ ``` --- ## Client Plugin [#client-plugin] If you're using the [Authula SDK](https://github.com/Authula/authula-node-sdk), add the plugin to the SDK like so: ```typescript import { createClient } from "authula"; import { AdminPlugin } from "authula/plugins"; export const authulaClient = createClient({ url: "http://localhost:8080/auth", plugins: [ // other plugins... new AdminPlugin(), ], }); ``` --- ## Security Recommendations [#security-recommendations] - **Access Control** — All admin endpoints should be protected by appropriate authentication and authorization ideally using the Access Control plugin alongside this plugin to restrict access to authorized administrators only. - **Impersonation Limits** — Set reasonable `impersonation_max_expires_in` values to limit exposure window - **Audit Review** — Regularly review impersonation and ban logs for compliance and security auditing - **Least Privilege** — Only grant access to routes for users who absolutely require it - **Reason Tracking** — Always document reasons for bans and session revocations for audit purposes - **Session Security** — Impersonation sessions should be treated with the same security considerations as regular sessions # API Key Plugin ## Overview [#overview] The API Key plugin provides programmable key management for machine-to-machine or personal access. It supports two ownership models — user-owned and organization-owned keys and enforces a permission subset rule: the creator of a key can only assign permissions they themselves hold. API keys can be configured with expiration, per-key rate limits, custom metadata, and a configurable header for automatic authentication via a pre-request hook. ### Features [#features] - **Two ownership models**: Keys can be owned by a user or an organization - **Scoped permissions**: Assign granular permission strings to each key - **Permission subset enforcement**: A key's permissions are bounded by the creator's own scopes - **Automatic authentication**: Validates keys via a configurable request header and sets the actor on the request context - **Per-key rate limiting**: Enforce request quotas per key using the shared rate limiter service - **Key expiration**: Keys can be set to expire at a specific time - **Key prefix & preview**: Configurable prefix and start/last snippets for identifying keys without exposing the full secret - **Metadata**: Arbitrary key-value metadata attached to each key - **Programmatic CRUD**: Full Create, Read, Update, Delete API via authenticated HTTP endpoints --- ## Configuration [#configuration] ### Standalone Mode [#standalone-mode] ```toml [plugins.api_key] enabled = true # Whether to allow organization-owned API keys. # Requires the organizations plugin to be enabled. allow_org_keys = false # Optional prefix prepended to generated API keys (e.g. "authula_sk_"). default_prefix = "" # HTTP header used for API key authentication. # Default: "X-AUTHULA-API-KEY" header = "X-AUTHULA-API-KEY" # Whether to automatically cleanup expired API keys. auto_cleanup = false # Interval at which the cleanup job runs. # Default: "1m" cleanup_interval = "1m" ``` ### Library Mode [#library-mode] ```go import ( apikeyplugin "github.com/Authula/authula/plugins/api-key" apikeyplugintypes "github.com/Authula/authula/plugins/api-key/types" ) plugin := apikeyplugin.New(apikeyplugintypes.ApiKeyPluginConfig{ AllowOrgKeys: false, DefaultPrefix: "", Header: "X-AUTHULA-API-KEY", AutoCleanup: false, CleanupInterval: time.Minute, }) ``` ### Cleanup System [#cleanup-system] When `auto_cleanup` is enabled, the plugin starts a background goroutine that periodically deletes expired API keys. The `cleanup_interval` field controls how often this job runs (defaults to 1 minute if not set or set to a non-positive value). --- ## API Reference [#api-reference] All endpoints require authentication via a bearer token or session. The authenticated user's scopes determine what operations they can perform. | HTTP Method | Route Path | Description | | ----------- | ------------------ | ---------------------------- | | `POST` | `/api-keys` | Create an API key | | `GET` | `/api-keys` | List API keys | | `GET` | `/api-keys/{id}` | Get an API key by ID | | `PATCH` | `/api-keys/{id}` | Update an API key | | `DELETE` | `/api-keys/{id}` | Delete an API key | | `POST` | `/api-keys/verify` | Verify an API key's validity | ### Create API Key [#create-api-key] **`POST /api-keys`** Request body: ```json { "name": "My API Key", "owner_type": "user", "owner_id": "user-123", "prefix": "sk_", "enabled": true, "expires_at": "2027-12-31T23:59:59Z", "rate_limit_enabled": true, "rate_limit_time_window": 60, "rate_limit_max_requests": 100, "permissions": ["admin:users:list", "organizations:read"], "metadata": { "key": "value" } } ``` **Constraints:** - `owner_type` must be `"user"` or `"organization"` — organization keys only work when `allow_org_keys` is enabled and the organizations plugin is registered. - For user-owned keys, `owner_id` must match the authenticated user's ID. - For organization-owned keys, the actor (human user) must hold the `org:api-key:create` scope. - The actor can only assign permissions they themselves hold in their own scopes. Response (`201 Created`): ```json { "raw_api_key": "sk_6d75b3232bebb5b684e6c2183b074313580c7e31de5f4781c5b32c5d70acf1e1", "api_key": { "id": "uuid-...", "name": "My API Key", "owner_type": "user", "owner_id": "user-123", "prefix": "sk_", "start": "6d75", "last": "f1e1", "enabled": true, "rate_limit_enabled": true, "expires_at": "2027-12-31T23:59:59Z", "permissions": ["admin:users:list", "organizations:read"], "metadata": { "key": "value" }, "created_at": "2026-07-01T12:00:00Z", "updated_at": "2026-07-01T12:00:00Z" } } ``` > **⚠️ Important:** The `raw_api_key` value is only returned at creation time and cannot be retrieved again. Store it securely. ### List API Keys [#list-api-keys] **`GET /api-keys?page=1&limit=20&owner_type=organization&owner_id=org-456`** - User-owned keys: automatically scoped to the authenticated user (no special scope required). - Organization-owned keys: the actor must hold the `org:api-key:list` scope. Response (`200 OK`): ```json { "items": [ ... ], "total": 1, "page": 1, "limit": 20 } ``` ### Get API Key [#get-api-key] **`GET /api-keys/{id}`** - User-owned keys: only the owner can view. - Organization-owned keys: the actor must hold the `org:api-key:read` scope. Response (`200 OK`): ```json { "api_key": { ... } } ``` ### Update API Key [#update-api-key] **`PATCH /api-keys/{id}`** Request body — all fields optional: ```json { "name": "Updated Name", "enabled": false, "rate_limit_enabled": true, "rate_limit_time_window": 30, "rate_limit_max_requests": 50, "expires_at": "2028-01-01T00:00:00Z", "permissions": ["read"], "metadata": { "env": "staging" } } ``` - User-owned keys: only the owner can update. - Organization-owned keys: the actor must hold the `org:api-key:update` scope. - Permission subset enforcement applies: new permissions must be within the actor's scopes. Response (`200 OK`): ```json { "api_key": { ... } } ``` ### Delete API Key [#delete-api-key] **`DELETE /api-keys/{id}`** - User-owned keys: only the owner can delete. - Organization-owned keys: the actor must hold the `org:api-key:delete` scope. Response (`200 OK`): ```json { "message": "API key deleted successfully" } ``` ### Verify API Key [#verify-api-key] **`POST /api-keys/verify`** Request body: ```json { "key": "sk_6d75b3232bebb5b684e6c2183b074313580c7e31de5f4781c5b32c5d70acf1e1" } ``` The raw key is hashed using the token service and looked up. Returns whether the key is valid (exists, enabled, and not expired). Response (`200 OK`): ```json { "api_key": { ... } } ``` Response (`401 Unauthorized`): ```json { "message": "invalid or expired API key" } ``` --- ## Database Schema [#database-schema] ### Table: `api_keys` [#table-api_keys] | Field | Type | Key | Description | | -------------------- | ---------- | --- | ----------------------------------------------------------------- | | `id` | UUID | PK | Unique identifier for the API key | | `key_hash` | string | - | Hashed value of the full API key (used for lookup) | | `name` | string | - | Human-readable name for the key | | `owner_type` | string | - | Owner type: `"user"` or `"organization"` | | `owner_id` | UUID | FK | Reference to the owner (user or organization) | | `prefix` | string? | - | Optional prefix prepended to the raw key value | | `start` | string | - | First 4 characters of the raw key (for display without revealing) | | `last` | string | - | Last 4 characters of the raw key (for display without revealing) | | `enabled` | boolean | - | Whether the key is active | | `rate_limit_enabled` | boolean | - | Whether per-key rate limiting is enabled | | `last_requested_at` | timestamp? | - | Timestamp of the last request using this key | | `expires_at` | timestamp? | - | Optional expiration timestamp (keys are rejected after this time) | | `permissions` | string[] | - | List of permission strings scoped to this key | | `metadata` | JSON | - | Arbitrary key-value metadata | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | **Indexes:** `key_hash` (unique), `owner_id`, `(owner_type, owner_id)`, `expires_at`, `enabled`. Migrations are automatically handled when the plugin is initialized — no manual migration steps required. --- ## Plugin Capabilities [#plugin-capabilities] This plugin doesn't have any hooks and capabilities. --- ## Permission Registration [#permission-registration] The plugin registers the following permissions with the access control system at initialization: | Permission Key | Description | | -------------------- | -------------------------------------- | | `org:api-key:create` | Create API keys for the organization | | `org:api-key:list` | List API keys for the organization | | `org:api-key:read` | Read an API key for the organization | | `org:api-key:update` | Update an API key for the organization | | `org:api-key:delete` | Delete an API key for the organization | --- ## Security [#security] ### Permission Subset Enforcement [#permission-subset-enforcement] The API Key plugin implements a critical security constraint: **a user can only assign permissions to an API key that they themselves possess**. When creating or updating a key, the service checks that every permission being assigned to the key exists within the actor's own `Scopes` list. This prevents privilege escalation where a user with limited permissions could create a key with broader permissions than their own. - For user-owned keys: the actor's scopes come from their own roles and user permissions. - For organization-owned keys: the actor's scopes include their organizational roles and permissions, and they must also hold the relevant `org:api-key:*` scope. ### Wildcard Support [#wildcard-support] The permission system supports wildcard matching (`*` and `organizations:*`), so an actor with a wildcard scope can assign any matching permission to their keys. ### Key Storage [#key-storage] Only the hashed key value is stored in the database. The raw key is returned once at creation and cannot be recovered. Store it securely at creation time. ### Plugin Dependencies [#plugin-dependencies] The API Key plugin depends on the **Access Control** plugin and will not initialize without it. Organization-owned keys additionally require the **Organizations** plugin. --- ## Troubleshooting [#troubleshooting] | Symptom | Likely Cause | Solution | | -------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- | | `organization service is not available` | `allow_org_keys` is `true` but the organizations plugin is missing | Enable the organizations plugin or set `allow_org_keys = false` | | `permission "..." is not within your scopes` | Actor tried to assign a permission they don't hold | Assign the permission to the actor first, or use a subset | | `failed to ensure api key permissions` | Access Control plugin is not enabled | Enable the `access-control` plugin | | Key accepted but 403 on downstream routes | The key's permissions don't include the required scopes | Update the key's permissions or the route's required scope | | API key not recognized | Wrong header name or key format | Verify `header` config matches the client's header | # Bearer Plugin ## Overview [#overview] The Bearer plugin provides Bearer token authentication by validating JWT access tokens from the configured request header. It extracts tokens in the `Bearer ` format, validates them using the JWT plugin service, and sets `UserID` in the request context for downstream authorization. ### Features [#features] - **Authorization Header Validation** — Extracts and validates `Authorization: Bearer ` headers or a custom header. - **JWT Integration** — Validates token signature, expiration, and blacklist state through the JWT plugin service. - **Required and Optional Modes** — Supports `bearer.auth` for required auth and `bearer.auth.optional` for optional auth. - **Cooperative Authentication** — Skips validation when another auth plugin already populated `UserID`. - **Configurable Header** — Custom header name support with `header_name`. - **Stateless Validation** — No session storage or persistent state is required. --- ## Configuration [#configuration] ### Standalone Mode [#standalone-mode] ```toml [plugins.bearer] enabled = true header_name = "Authorization" ``` ### Library Mode [#library-mode] ```go import ( bearerplugin "github.com/Authula/authula/plugins/bearer" ) bearerplugin.New(bearerplugin.BearerPluginConfig{ Enabled: true, HeaderName: "Authorization", }) ``` --- ## API Reference [#api-reference] This plugin doesn't expose any HTTP endpoints. --- ## Database Schema [#database-schema] This plugin doesn't create any database tables. --- ## Plugin Capabilities [#plugin-capabilities] The Bearer plugin provides the following hooks: - `bearer.auth` — Required authentication hook. Validates the Bearer token and returns `401 Unauthorized` if the token is missing or invalid. - `bearer.auth.optional` — Optional authentication hook. Validates the Bearer token if present and silently continues if missing or invalid. ### Standalone Mode [#standalone-mode-1] ```toml [[route_mappings]] paths = ["GET:/protected"] plugins = ["bearer.auth"] [[route_mappings]] paths = ["GET:/public"] plugins = ["bearer.auth.optional"] ``` ### Library Mode [#library-mode-1] ```go import ( authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" bearerplugin "github.com/Authula/authula/plugins/bearer" ) config := authulaconfig.NewConfig( authulaconfig.WithRouteMappings([]authulamodels.RouteMapping{ { Paths: []string{"GET:/protected"}, Plugins: []string{bearerplugin.HookIDBearerAuth.String()}, }, { Paths: []string{"GET:/public"}, Plugins: []string{bearerplugin.HookIDBearerAuthOptional.String()}, }, }), ) ``` --- ## Security Recommendations [#security-recommendations] - Use HTTPS for all requests to protect the `Authorization` header and bearer token. - Ensure the JWT plugin is enabled and registered before using the Bearer plugin. - Use `bearer.auth` for routes that require authenticated access and `bearer.auth.optional` for routes that may accept both authenticated and anonymous requests. - Keep access tokens short-lived and validate them on every request. - If you use token blacklisting, pair the Bearer plugin with the JWT plugin and Secondary Storage plugin. --- ## Troubleshooting [#troubleshooting] - `401 Unauthorized` on a protected route usually means the token is missing, malformed, expired, or the header name does not match `header_name`. - If valid tokens are not recognized, confirm the request header uses the Bearer scheme exactly: `Authorization: Bearer `. - If another auth plugin already sets `UserID`, the Bearer plugin will skip validation, which may affect route behavior if route mappings are misconfigured. --- ## Notes [#notes] - The Bearer plugin is stateless and does not persist authentication state. - It is intended to be used with the JWT plugin because token validation depends on the JWT service. - Cooperative authentication allows Bearer to operate alongside session-based or other auth plugins without conflicting. # CSRF Plugin ## Overview [#overview] The CSRF plugin provides server-side protection against Cross-Site Request Forgery (CSRF) attacks using the **Double-Submit Cookie pattern** integrated into Authula's lifecycle hook system. It automatically generates cryptographically secure tokens for safe HTTP methods and validates them on state-changing requests. ### Features [#features] - **Double-Submit Cookie Pattern** — Validates CSRF tokens in both cookies and request headers - **Automatic Token Generation** — Generates secure tokens on safe HTTP methods (GET, HEAD, OPTIONS) - **Selective Enforcement** — Only validates on routes that explicitly request protection - **Token Cleanup** — Automatically clears CSRF cookies on sign-out - **Optional Header Protection** — Supports Go 1.25's CrossOriginProtection for additional security --- ## Configuration [#configuration] ### Standalone Mode [#standalone-mode] ```toml [plugins.csrf] enabled = true cookie_name = "authula_csrf_token" header_name = "X-AUTHULA-CSRF-TOKEN" max_age = "24h" secure = true same_site = "lax" enable_header_protection = false ``` ### Library Mode [#library-mode] ```go import ( csrfplugin "github.com/Authula/authula/plugins/csrf" ) csrfplugin.New(csrfplugin.CSRFPluginConfig{ Enabled: true, CookieName: "authula_csrf_token", HeaderName: "X-AUTHULA-CSRF-TOKEN", MaxAge: 24 * time.Hour, Secure: false, SameSite: "lax", EnableHeaderProtection: false, }), ``` ### Configuration Options [#configuration-options] | Option | Type | Default | Description | | -------------------------- | -------- | ------------------------ | ---------------------------------------------- | | `enabled` | boolean | `true` | Enable or disable the plugin | | `cookie_name` | string | `"authula_csrf_token"` | Name of the CSRF cookie | | `header_name` | string | `"X-AUTHULA-CSRF-TOKEN"` | Name of the CSRF header | | `max_age` | duration | `"24h"` | Token lifetime before expiration | | `secure` | boolean | `false` | Force Secure flag on cookies | | `same_site` | string | `"lax"` | SameSite attribute: `lax`, `strict`, or `none` | | `enable_header_protection` | boolean | `false` | Enable Go's CrossOriginProtection | --- ## API Reference [#api-reference] The CSRF plugin doesn't expose any HTTP endpoints. --- ## Database Schema [#database-schema] The CSRF plugin doesn't create any database tables. --- ## Plugin Capabilities [#plugin-capabilities] The CSRF plugin integrates with Authula through the **lifecycle hook system**, registering hooks at specific stages: - **Before Hook (Order 5)** — Token generation for safe methods (GET, HEAD, OPTIONS) - **Before Hook (Order 5)** — Token validation for unsafe methods (POST, PUT, PATCH, DELETE) — requires explicit opt-in via route metadata - **After Hook (Order 5)** — Cookie cleanup on sign-out It also has the following capabilities: - `csrf.protect` — Validates the presence and correctness of CSRF tokens on protected routes ### Token Generation Flow [#token-generation-flow] On safe HTTP methods, the plugin automatically generates a cryptographically secure token and sets it as a cookie. The cookie is intentionally readable by JavaScript (`HttpOnly=false`) to enable the Double-Submit pattern. ### Token Validation Flow [#token-validation-flow] On state-changing methods, the plugin validates that the CSRF token exists in both: 1. A cookie (readable by JavaScript) 2. A request header Both values must match exactly. Requests with missing or mismatched tokens are rejected with a `403 Forbidden` response. ### Optional Layered Protection [#optional-layered-protection] Beyond the Double-Submit pattern, the plugin optionally supports **header-based origin validation** using Go 1.25's `CrossOriginProtection`. When enabled via `enable_header_protection`, this validates `Sec-Fetch-Site`, `Origin`, and `Host` headers against configured trusted origins before token validation occurs. ### How to Use [#how-to-use] Routes must explicitly request CSRF protection by including the plugin identifier in their metadata. This allows selective protection—authentication endpoints like sign-in can skip validation while authenticated endpoints enforce it. ```toml [[route_mappings]] method = "POST" path = "/some/path" plugins = ["csrf.protect"] ``` --- ## Client Plugin [#client-plugin] If you're using the [Authula SDK](https://github.com/Authula/authula-node-sdk), add the plugin to the SDK like so: ```typescript import { createClient } from "authula"; import { CSRFPlugin } from "authula/plugins"; export const authulaClient = createClient({ url: "http://localhost:8080/auth", plugins: [ // other plugins... new CSRFPlugin({ cookieName: "authula_csrf_token", // optional, defaults to "authula_csrf_token" headerName: "X-AUTHULA-CSRF-TOKEN", // optional, defaults to "X-AUTHULA-CSRF-TOKEN" }), ], }); ``` --- ## Key Design Decisions [#key-design-decisions] - **HttpOnly=false** — Required for Double-Submit pattern where JavaScript must read the cookie value - **Selective enforcement** — Only validates on routes that explicitly request protection - **Defense in depth** — Optional header protection provides additional layer beyond tokens - **Hot reloadable** — Configuration changes apply without server restart --- ## Security Recommendations [#security-recommendations] - **Enable Secure Flag in Production** — Set `secure = true` to ensure cookies are only transmitted over HTTPS - **Strict SameSite** — Set `same_site = "strict"` for maximum CSRF protection if you need it (may impact UX). But you may want to keep it at `lax` to still allow some cross-site requests especially for top-level navigations. - **Enable Header Protection** — Consider enabling `enable_header_protection = true` for additional origin validation - **Trusted Origins** — When using header protection, configure trusted origins appropriately for your deployment - **Token Rotation** — The 24-hour default `max_age` provides a balance between security and UX; adjust based on your security requirements - **HttpOnly Trade-off** — The Double-Submit pattern requires `HttpOnly=false`, making tokens accessible to JavaScript; ensure your application is protected against XSS attacks # Email & Password Plugin ## Overview [#overview] The Email & Password plugin provides email/password authentication with various email-based flows. It integrates with the Email plugin to send verification and password reset emails, and supports configurable password policies and sign-up controls. ### Features [#features] - **User Registration** — Sign-up with email and password with optional email verification - **User Authentication** — Sign-in with email and password credentials - **Email Verification** — Optional email verification flow with expiring verification links - **Password Reset** — Secure password reset flow with expiring reset links - **Email Change** — Email change requests with verification to both old and new addresses - **Password Policy** — Configurable minimum and maximum password length requirements - **Sign-Up Control** — Option to disable new user registration - **Auto Sign-In** — Optional automatic sign-in after successful registration --- ## Configuration [#configuration] ### Standalone Mode [#standalone-mode] ```toml [plugins.email_password] enabled = true # Minimum password length requirement (8 recommended) min_password_length = 8 # Maximum password length requirement max_password_length = 128 # Disable new user registration disable_sign_up = false # Require email verification before sign-in require_email_verification = true # Whether to automatically sign in the user after sign-up auto_sign_in = true # Send verification email on sign-up send_email_on_sign_up = true # Send email on sign-in (optional) send_email_on_sign_in = false # Expiration time for email verification links email_verification_expires_in = "24h" # Expiration time for password reset links password_reset_expires_in = "1h" # Expiration time for email change requests request_email_change_expires_in = "1h" ``` ### Library Mode [#library-mode] ```go import ( authulamodels "github.com/Authula/authula/models" emailpasswordplugin "github.com/Authula/authula/plugins/email-password" emailpasswordplugintypes "github.com/Authula/authula/plugins/email-password/types" ) emailpasswordplugin.New(emailpasswordplugintypes.EmailPasswordPluginConfig{ Enabled: true, MinPasswordLength: 8, MaxPasswordLength: 128, DisableSignUp: false, RequireEmailVerification: true, AutoSignIn: true, SendEmailOnSignUp: true, SendEmailOnSignIn: false, EmailVerificationExpiresIn: 24 * time.Hour, PasswordResetExpiresIn: time.Hour, RequestEmailChangeExpiresIn: time.Hour, // Optional: custom email sending logic SendEmailVerification: func() { /* ... */}, SendPasswordResetEmail: func() { /* ... */}, SendChangedPasswordEmail: func() { /* ... */}, SendRequestEmailChangeEmail: func() { /* ... */}, SendChangedEmailToOldEmail: func() { /* ... */}, SendChangedEmailToNewEmail: func() { /* ... */}, }), ``` --- ## API Reference [#api-reference] | Method | Endpoint | Description | | ------ | ----------------------------------------- | --------------------------------------- | | `POST` | `/email-password/sign-up` | Register a new user with email/password | | `POST` | `/email-password/sign-in` | Sign in with email/password | | `GET` | `/email-password/verify-email` | Verify email address | | `POST` | `/email-password/send-email-verification` | Send email verification link | | `POST` | `/email-password/request-password-reset` | Request password reset email | | `POST` | `/email-password/change-password` | Change user password | | `POST` | `/email-password/request-email-change` | Request email change verification | --- ## Database Schema [#database-schema] This plugin does not create any database tables. --- ## Plugin Capabilities [#plugin-capabilities] This plugin doesn't have any plugin hooks and capabilities. --- ## Security Recommendations [#security-recommendations] - **Password Length Requirements** — Set appropriate `min_password_length` (at least 8) and `max_password_length` values - **Email Verification** — Enable `require_email_verification = true` in production to prevent fake accounts - **Token Expiration** — Use short expiration times for password reset tokens (`1h` recommended) - **Disable Sign-Up** — Set `disable_sign_up = true` for closed user bases or invite-only applications - **HTTPS Required** — Always use HTTPS in production to protect credentials in transit - **Rate Limiting** — Consider rate limiting sign-in and password reset endpoints to prevent brute-force attacks using the rate limit plugin. - **Password Hashing** — Passwords are automatically hashed using argon2 so plain text passwords are never stored in the database. --- ## Client Plugin [#client-plugin] If you're using the [Authula SDK](https://github.com/Authula/authula-node-sdk), add the plugin to the client instance as follows: ```typescript import { createClient } from "authula"; import { EmailPasswordPlugin } from "authula/plugins"; export const authulaClient = createClient({ url: "http://localhost:8080/auth", plugins: [ // other plugins... new EmailPasswordPlugin(), ], }); ``` # Email Plugin ## Overview [#overview] The Email plugin provides transactional email capabilities with support for multiple providers and automatic failover. It enables Authula and other plugins to send verification emails, password reset links, and other transactional messages with configurable provider support. ### Features [#features] - **Multi-Provider Support** — Use SMTP or Resend as your email provider - **Automatic Fallback** — Configure a backup provider that activates when the primary fails - **HTML & Text Support** — Send both plain text and HTML email formats - **Template Ready** — Built-in support for Go template rendering (HTML/text) - **Environment Variable Support** — Securely configure credentials via environment variables --- ## Configuration [#configuration] ### Standalone Mode [#standalone-mode] ```toml [plugins.email] enabled = true provider = "smtp" # Options: "smtp", "resend" # You can set/override `from_address` using the `FROM_ADDRESS` environment variable. from_address = "noreply@example.com" tls_mode = "starttls" # Options: "off", "starttls", "tls" # Optional: Fallback provider if primary fails # fallback_provider = "resend" # Optional backup: "smtp" or "resend" # SMTP Configuration (when provider = "smtp") [plugins.email.smtp] host = "smtp.gmail.com" port = 587 user = "your-email@gmail.com" pass = "your-app-password" # Resend Configuration (when provider = "resend") # [plugins.email.resend] # NOTE: it is recommended to set the Resend API key via the RESEND_API_KEY environment variable rather than hardcoding it here. # api_key = "" ``` ### Library Mode [#library-mode] ```go import ( emailplugin "github.com/Authula/authula/plugins/email" emailplugintypes "github.com/Authula/authula/plugins/email/types" ) emailplugin.New(emailplugintypes.EmailPluginConfig{ Enabled: true, Provider: emailplugintypes.ProviderResend, FromAddress: "from@example.com", TLSMode: emailplugintypes.SMTPTLSModeStartTLS, FallbackProvider: emailplugintypes.ProviderSMTP, }), ``` ### Configuration Options [#configuration-options] | Option | Type | Default | Description | | ------------------- | ------- | ------------ | ------------------------------------------------ | | `enabled` | boolean | `true` | Enable or disable the plugin | | `provider` | string | `"smtp"` | Email provider: `"smtp"` or `"resend"` | | `from_address` | string | - | Default sender email address | | `tls_mode` | string | `"starttls"` | TLS mode: `"off"`, `"starttls"`, or `"tls"` | | `fallback_provider` | string | - | Optional backup provider: `"smtp"` or `"resend"` | ### SMTP Options [#smtp-options] | Option | Type | Default | Description | | ------ | ------ | ------- | ------------------------------------- | | `host` | string | - | SMTP server hostname | | `port` | int | `587` | SMTP port: `25`, `465`, `587`, `2525` | | `user` | string | - | SMTP username (optional for local) | | `pass` | string | - | SMTP password (optional for local) | ### Provider Details [#provider-details] **SMTP Provider** Connects to any SMTP-compliant server (Gmail, SendGrid, AWS SES, etc.). Uses opportunistic TLS for secure connections and supports SMTP authentication. **Resend Provider** Integrates with [Resend](https://resend.com) email API for simple API-based sending with built-in deliverability optimization. --- ## API Reference [#api-reference] The Email plugin does not expose any HTTP endpoints. It operates as an internal service used by other plugins (e.g., Email & Password, Magic Link) to send transactional emails. --- ## Database Schema [#database-schema] The Email plugin doesn't create any database tables. --- ## Plugin Capabilities [#plugin-capabilities] The Email plugin doesn't have any hooks and capabilities. --- ### Environment Variables [#environment-variables] **Core:** ```txt title=".env" EMAIL_PROVIDER="smtp" # Options: "smtp", "resend" FROM_ADDRESS="noreply@example.com" ``` **SMTP Provider:** ```txt title=".env" SMTP_HOST="smtp.gmail.com" SMTP_PORT="587" SMTP_USER="your-email@gmail.com" SMTP_PASS="your-app-password" ``` **Resend Provider:** ```txt title=".env" RESEND_API_KEY="re_xxxxxxxxxxxx" ``` ### Fallback Behavior [#fallback-behavior] When a fallback provider is configured, emails are first attempted via the primary provider. If that fails, the plugin automatically tries the fallback provider. This ensures email delivery even if the primary provider experiences issues. --- ## Security Recommendations [#security-recommendations] - **Use Environment Variables** — Store SMTP credentials and API keys in environment variables, not in configuration files - **Enable TLS** — Always use `tls_mode = "tls"` or `tls_mode = "starttls"` in production for encrypted connections - **App Passwords** — When using Gmail or similar providers, use app-specific passwords instead of your main account password - **Rate Limiting** — Be aware of provider rate limits; consider utilizing the rate limit plugin to prevent hitting limits and ensure reliable delivery - **From Address Verification** — Ensure your `from_address` is verified with your email provider to prevent delivery issues - **SPF/DKIM/DMARC** — Configure DNS records for your sending domain to improve deliverability and prevent spoofing # JWT Plugin ## Overview [#overview] The JWT plugin provides JWKS-based JWT authentication with automatic key rotation and refresh token support. It is designed for SPAs and mobile applications, offering short-lived access tokens with long-lived refresh tokens, multiple signing algorithms, and public key distribution via a JWKS endpoint. ### Features [#features] - **Multiple Signing Algorithms** — EdDSA (default), RS256, PS256, ES256, ES512, ECDH-ES - **Automatic Key Rotation** — Rotates signing keys on configurable intervals with grace periods - **Token Pairs** — Short-lived access tokens (15m default) with long-lived refresh tokens (7d default) - **JWKS Endpoint** — Public key distribution via `/.well-known/jwks.json` for token verification - **Token Blacklisting** — Revoke tokens immediately when needed (requires Secondary Storage plugin) - **Refresh Token Rotation** — New refresh token issued on each refresh for security - **Grace Periods** — Smooth transitions during key rotation and refresh token reuse detection --- ## Configuration [#configuration] ### Standalone Mode [#standalone-mode] ```toml [plugins.jwt] enabled = true algorithm = "eddsa" key_rotation_interval = "720h" key_rotation_grace_period = "1h" expires_in = "15m" refresh_expires_in = "168h" jwks_cache_ttl = "24h" refresh_grace_period = "10s" ``` ### Library Mode [#library-mode] ```go import ( jwtplugin "github.com/Authula/authula/plugins/jwt" jwtplugintypes "github.com/Authula/authula/plugins/jwt/types" ) jwtplugin.New(jwtplugintypes.JWTPluginConfig{ Enabled: true, Algorithm: jwtplugintypes.JWTAlgEdDSA, ExpiresIn: 15 * time.Minute, KeyRotationInterval: 720 * time.Hour, KeyRotationGracePeriod: time.Hour, RefreshExpiresIn: 168 * time.Hour, JWKSCacheTTL: 24 * time.Hour, RefreshGracePeriod: 10 * time.Second, }), ``` ### Configuration Options [#configuration-options] | Option | Type | Default | Description | | --------------------------- | -------- | --------- | -------------------------------------------------------------- | | `enabled` | boolean | `true` | Enable or disable the plugin | | `algorithm` | string | `"eddsa"` | Signing algorithm: `eddsa`, `rs256`, `ps256`, `es256`, `es512` | | `key_rotation_interval` | duration | `"720h"` | How often to rotate signing keys (30 days) | | `key_rotation_grace_period` | duration | `"1h"` | Grace period for old key validity after rotation | | `expires_in` | duration | `"15m"` | Access token TTL | | `refresh_expires_in` | duration | `"168h"` | Refresh token TTL (7 days) | | `jwks_cache_ttl` | duration | `"24h"` | JWKS cache TTL | | `refresh_grace_period` | duration | `"10s"` | Grace period for refresh token reuse detection | --- ## API Reference [#api-reference] | Method | Path | Description | | ------ | ------------------------ | ------------------------------------------- | | `GET` | `/.well-known/jwks.json` | Public JWKS endpoint for token verification | | `POST` | `/token/refresh` | Exchange refresh token for new token pair | --- ## Database Schema [#database-schema] ### Table: `jwt_keys` [#table-jwt_keys] | Field | Type | Key | Description | | ------------- | --------- | --- | ------------------------------------------------ | | `id` | string | PK | Unique identifier for the key | | `key_id` | string | - | JWKS key ID (kid) | | `public_key` | string | - | PEM-encoded public key | | `private_key` | string | - | PEM-encoded private key | | `algorithm` | string | - | Signing algorithm used | | `created_at` | timestamp | - | Key creation time | | `expires_at` | timestamp | - | Key expiration time (after grace period) | | `is_active` | boolean | - | Whether this key is currently active for signing | ### Table: `jwt_refresh_tokens` [#table-jwt_refresh_tokens] | Field | Type | Key | Description | | ------------ | --------- | --- | ------------------------------------------ | | `id` | string | PK | Unique identifier for the refresh token | | `user_id` | string | FK | Reference to the user | | `session_id` | string | FK | Reference to the session | | `token` | string | - | Hashed refresh token | | `expires_at` | timestamp | - | Token expiration time | | `revoked_at` | timestamp | - | When the token was revoked (if applicable) | | `created_at` | timestamp | - | Record creation time | > Migrations are automatically handled when the plugin is initialized. --- ## Plugin Capabilities [#plugin-capabilities] The JWT plugin provides the following capabilities: - **jwt.respond_json** — Sends JWT tokens in JSON response after successful authentication ### Token Lifecycle [#token-lifecycle] ``` ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ Sign In │────▶│ Access │────▶│ Refresh │ │ Success │ │ Token │ │ Token │ └─────────────┘ └──────┬───────┘ └──────┬───────┘ │ │ ▼ │ ┌──────────────┐ │ │ API Calls │──────────────┘ │ (15 min) │ When expired └──────┬───────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Valid │ │ Expired │ │ Sign Out │ │ Request │ │ Token │ │ (Manual)│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Continue │ │ Refresh │ │ Revoke │ │ │ │ Endpoint │ │ Tokens │ └──────────┘ └────┬─────┘ └──────────┘ │ ▼ ┌──────────────┐ │ New Token │ │ Pair │ └──────────────┘ ``` ### Key Rotation Lifecycle [#key-rotation-lifecycle] ``` ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ Key Gen │────▶│ Active │────▶│ Rotation │ │ (Init) │ │ Signing │ │ (Interval) │ └─────────────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ ┌──────┴──────┐ │ ▼ ▼ │ ┌──────────┐ ┌──────────┐ │ │ New Key │ │ Old Key │ │ │ (Active) │ │ (Grace) │ │ └────┬─────┘ └────┬─────┘ │ │ │ └─────────────┴─────────────┘ │ ▼ ┌──────────────┐ │ Old Key │ │ (Expired) │ └──────────────┘ ``` ### How to Use [#how-to-use] Enable JWT responses on authentication routes: ```toml [[route_mappings]] method = "POST" path = "/auth/sign-in" plugins = ["bearer.auth.optional", "jwt.respond_json"] ``` After successful authentication, the response will include: ```json { "access_token": "eyJhbGciOiJFZERTQSJ9...", "refresh_token": "eyJhbGciOiJFZERTQSJ9..." } ``` ### Algorithm Support [#algorithm-support] | Algorithm | Type | Recommended Use | | --------- | ------------------------------- | ----------------------------------- | | EdDSA | Edwards-curve Digital Signature | **Default** — Fast, secure, modern | | RS256 | RSA with SHA-256 | Legacy compatibility | | PS256 | RSA-PSS with SHA-256 | Enhanced RSA security | | ES256 | ECDSA with P-256 | Mobile/IoT constrained environments | | ES512 | ECDSA with P-521 | Maximum security requirements | | ECDH-ES | Elliptic Curve Diffie-Hellman | Key exchange only, not for signing | **Note**: ECDH-ES is available for key exchange but cannot be used for JWT signing. May be removed in a future version. --- ## Dependencies [#dependencies] - **Secondary Storage** (optional) — Enables token blacklisting for immediate revocation --- ## Client Plugin [#client-plugin] If you're using the [Authula SDK](https://github.com/Authula/authula-node-sdk), add the plugin to the SDK like so: ```typescript import { createClient } from "authula"; import { JWTPlugin } from "authula/plugins"; export const authulaClient = createClient({ url: "http://localhost:8080/auth", plugins: [ // other plugins... new JWTPlugin(), ], }); ``` --- ## Security Recommendations [#security-recommendations] - **Use EdDSA by Default** — EdDSA provides the best balance of security and performance for most use cases - **Enable Key Rotation** — Set `key_rotation_interval` to 30 days or less for production environments - **Short Access Token TTL** — Keep `expires_in` short (15 minutes or less) to minimize exposure if tokens are compromised - **Refresh Token Rotation** — Each refresh issues a new refresh token, invalidating the old one to detect replay attacks - **Token Blacklisting** — Use the Secondary Storage plugin to enable immediate token revocation on sign-out - **Secure JWKS Caching** — Set appropriate `jwks_cache_ttl` to balance performance and key rotation responsiveness - **HTTPS Required** — Always use HTTPS in production to protect tokens in transit - **Grace Period Tuning** — Adjust `key_rotation_grace_period` and `refresh_grace_period` based on your token distribution needs # Magic Link Plugin ## Overview [#overview] The Magic Link plugin provides passwordless authentication using email-based magic links. Users receive a secure link via email that they can click to sign in without passwords, offering a seamless and secure authentication experience. ### Features [#features] - **Passwordless Authentication** — Sign in via email without passwords - **Secure Token-Based Verification** — Cryptographically secure tokens with configurable expiration - **Configurable Expiration** — Set custom token expiration times - **Optional Sign-Up Prevention** — Prevent sign-ups for magic links - **Custom Email Sending** — Support for custom email sending functions - **Callback URL Support** — Redirect users to custom URLs after verification --- ## Configuration [#configuration] ### Standalone Mode [#standalone-mode] ```toml [plugins.magic_link] enabled = true expires_in = "15m" disable_sign_up = false ``` ### Library Mode [#library-mode] ```go import ( "time" authulamodels "github.com/Authula/authula/models" magiclinkplugin "github.com/Authula/authula/plugins/magic-link" magiclinkplugintypes "github.com/Authula/authula/plugins/magic-link/types" ) magiclinkplugin.New(magiclinkplugintypes.MagicLinkPluginConfig{ Enabled: true, ExpiresIn: 15 * time.Minute, DisableSignUp: false, SendMagicLinkVerificationEmail: func( params magiclinkplugintypes.SendMagicLinkVerificationEmailParams, reqCtx *authulamodels.RequestContext, ) error { // Optionally handle sending email here... return nil }, }) ``` ### Configuration Options [#configuration-options] | Option | Type | Default | Description | | ----------------- | --------------- | ------- | -------------------------------- | | `expires_in` | `time.Duration` | `"15m"` | Magic link token expiration time | | `disable_sign_up` | `bool` | `false` | Prevent new user registration | --- ## API Reference [#api-reference] | Method | Endpoint | Description | | ------ | ---------------------- | ---------------------------------------- | | `POST` | `/magic-link/sign-in` | Send magic link email to user | | `GET` | `/magic-link/verify` | Verify token from email link | | `POST` | `/magic-link/exchange` | Exchange verified token for user session | ### Sign In [#sign-in] Sends a magic link email to the user. **POST** `/magic-link/sign-in` Request body: ```json { "email": "user@example.com", "name": "John Doe", "callback_url": "https://yourapp.com/callback" } ``` ### Verify [#verify] Verifies the token from the email link. If `callback_url` is provided, redirects with the token. Otherwise returns the token in JSON. **GET** `/magic-link/verify?token=&callback_url=` ### Exchange [#exchange] Exchanges the verified token for user session and authentication. **POST** `/magic-link/exchange` Request body: ```json { "token": "" } ``` --- ## Database Schema [#database-schema] The Magic Link plugin does not create any database tables. --- ## Plugin Capabilities [#plugin-capabilities] The Magic Link plugin provides no hooks or capabilities. --- ### Flow Steps [#flow-steps] **1. Request Sign-In** — Client calls `/magic-link/sign-in` with user's email address. **2. Magic Link Sent** — A secure token is generated and sent to the user's email address. **3. User Clicks Link** — User clicks the magic link in the email, which calls `/magic-link/verify`. **4. Token Verified** — The token is verified for validity and expiration. **5. Exchange for Session** — Client calls `/magic-link/exchange` with the verified token to establish a session. **6. Session Created** — User is authenticated with an active session. --- ## Client Plugin [#client-plugin] If you're using the [Authula SDK](https://github.com/Authula/authula-node-sdk), add the plugin to the SDK like so: ```typescript import { createClient } from "authula"; import { MagicLinkPlugin } from "authula/plugins"; export const authulaClient = createClient({ url: "http://localhost:8080/auth", plugins: [ // other plugins... new MagicLinkPlugin(), ], }); ``` --- ## Security Recommendations [#security-recommendations] - **Short Expiration Times** — Use short `expires_in` values (15 minutes or less) to minimize token exposure window - **Single-Use Tokens** — Tokens are automatically marked as used after verification to prevent replay attacks - **HTTPS Required** — Always use HTTPS in production to protect magic links in transit - **Rate Limiting** — Consider rate limiting the `/magic-link/sign-in` endpoint to prevent email flooding attacks - **Email Verification** — For new user registration, consider additional verification steps before allowing account creation - **Token Hashing** — Tokens are automatically hashed before storage; never log or expose raw tokens - **Disable Sign-Up** — Set `disable_sign_up = true` for applications that do not want to allow new user registration via magic links # Discord **Note:** This guide uses Discord OAuth2 for user sign-in, which is recommended by Discord for new integrations. ### Obtain Discord credentials [#obtain-discord-credentials] 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications). 1. Click on "New Application" and provide a name for your application. 1. Fill in all the general information required for your application. 1. Navigate to the "OAuth2" section and then you will see your "Client ID" and "Client Secret". Make a note of the "Client ID" and "Client Secret". Bear in mind you will only be shown the "Client Secret" once. 1. Under "OAuth2", go to the "Redirects" section and add the following redirect URI: ```bash # Assuming base path is /api/auth http://localhost:8080/api/auth/oauth2/callback/discord ``` For production, replace localhost with the base URL of your Authula server (e.g. `https://api.yourdomain.com`). ### Configure the provider [#configure-the-provider] #### Standalone Mode [#standalone-mode] Add the following to your `config.toml`: ```toml # SECURITY NOTE: It is recommended to set the 'client_secret' for each of these via their # respective environment variables as shown in the .env.example file rather than hardcoding it here. [plugins.oauth2.providers.discord] enabled = true client_id = "your-client-id" client_secret = "your-client-secret" redirect_url = "http://localhost:8080/auth/oauth2/callback/discord" scopes = [] ``` #### Library Mode [#library-mode] ```go import ( "os" "fmt" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" oauth2plugin "github.com/Authula/authula/plugins/oauth2" oauth2plugintypes "github.com/Authula/authula/plugins/oauth2/types" ) func main() { config := authulaconfig.NewConfig( authulaconfig.WithSecurity(authulamodels.SecurityConfig{ // Make sure to configure CORS and Trusted Origins appropriately TrustedOrigins: []string{"your frontend origin"}, CORS: authulamodels.CORSConfig{ AllowCredentials: true, AllowedOrigins: []string{"your frontend origin"}, }, }), ) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{ oauth2plugin.New(oauth2plugintypes.OAuth2PluginConfig{ Enabled: true, Providers: map[string]oauth2plugintypes.ProviderConfig{ "discord": { Enabled: true, ClientID: os.Getenv(authulaenv.EnvDiscordClientID), ClientSecret: os.Getenv(authulaenv.EnvDiscordClientSecret), RedirectURL: fmt.Sprintf("%s%s/oauth2/callback/discord", config.BaseURL, config.BasePath), }, }, }), }, }) } ``` ### Test the integration [#test-the-integration] 1. Start your Authula server. 1. In your webapp, create a button when clicked on, it navigates the user to: ```bash # (replace localhost with your Authula server URL in production). http://localhost:8080/auth/oauth2/authorize/discord?redirect_to= ``` 1. You should be redirected to Discord's authorization page. After authorizing, you will be redirected back to your application. # GitHub **Note:** This guide uses GitHub Apps for user sign-in, which is recommended by GitHub for new integrations. ### Obtain GitHub credentials [#obtain-github-credentials] 1. Go to [GitHub Apps](https://github.com/settings/apps). 1. Click on "New GitHub App". 1. Fill in the GitHub App name, Homepage URL, and Description. 1. On the app page, generate a Client secret under "Client secrets". 1. Note the Client ID and the generated Client Secret. 1. Under the "Identifying and authorizing users" section, set the Callback URL to: ```bash # Assuming base path is /api/auth http://localhost:8080/api/auth/oauth2/callback/github ``` For production, replace localhost with the base URL of your Authula server (e.g. `https://api.yourdomain.com`). 1. Make sure to check the following checkboxes: - Request user authorization (OAuth) during installation - Enable Device Flow 1. Then save the changes and click on the "Permissions & events" tab on the left, under the "Account permissions", set the "Email addresses" to "Read-only" to access the user's email. ### Configure the provider [#configure-the-provider] #### Standalone Mode [#standalone-mode] Add the following to your `config.toml`: ```toml # SECURITY NOTE: It is recommended to set the 'client_secret' for each of these via their # respective environment variables as shown in the .env.example file rather than hardcoding it here. [plugins.oauth2.providers.github] enabled = true client_id = "your-client-id" client_secret = "your-client-secret" redirect_url = "http://localhost:8080/auth/oauth2/callback/github" scopes = [] ``` #### Library Mode [#library-mode] ```go import ( "os" "fmt" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" oauth2plugin "github.com/Authula/authula/plugins/oauth2" oauth2plugintypes "github.com/Authula/authula/plugins/oauth2/types" ) func main() { config := authulaconfig.NewConfig( authulaconfig.WithSecurity(authulamodels.SecurityConfig{ // Make sure to configure CORS and Trusted Origins appropriately TrustedOrigins: []string{"your frontend origin"}, CORS: authulamodels.CORSConfig{ AllowCredentials: true, AllowedOrigins: []string{"your frontend origin"}, }, }), ) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{ oauth2plugin.New(oauth2plugintypes.OAuth2PluginConfig{ Enabled: true, Providers: map[string]oauth2plugintypes.ProviderConfig{ "github": { Enabled: true, ClientID: os.Getenv(authulaenv.EnvGitHubClientID), ClientSecret: os.Getenv(authulaenv.EnvGitHubClientSecret), RedirectURL: fmt.Sprintf("%s%s/oauth2/callback/github", config.BaseURL, config.BasePath), }, }, }), }, }) // Start the server and mount the handler log.Fatal(http.ListenAndServe(":8080", auth.Handler())) } ``` ### Test the integration [#test-the-integration] 1. Start your Authula server. 2. In your webapp, create a button when clicked on, it navigates the user to: ```bash # (replace localhost with your Authula server URL in production). http://localhost:8080/auth/oauth2/authorize/github?redirect_to= ``` 3. You should be redirected to GitHub's authorization page. After authorizing, you will be redirected back to your application. # Google ### Obtain Google credentials [#obtain-google-credentials] 1. Go to the [Google Cloud Console](https://console.cloud.google.com/). 1. Create a new project or select an existing one. 1. Search for "APIs & Services", then go to "Credentials" in the left menu, click "Create Credentials" > "OAuth client ID". 1. Choose "Web application", give it a name. 1. Add authorized redirect URIs: ```bash # Assuming base path is /api/auth http://localhost:8080/api/auth/oauth2/callback/google ``` For production, replace localhost with the base URL of your Authula server (e.g. `https://api.yourdomain.com`). 1. Note the Client ID and Client Secret. ### Configure the provider [#configure-the-provider] #### Standalone Mode [#standalone-mode] Add the following to your `config.toml`: ```toml # SECURITY NOTE: It is recommended to set the 'client_secret' for each of these via their # respective environment variables as shown in the .env.example file rather than hardcoding it here. [plugins.oauth2.providers.google] enabled = true client_id = "your-client-id" client_secret = "your-client-secret" redirect_url = "http://localhost:8080/auth/oauth2/callback/google" scopes = [] ``` #### Library Mode [#library-mode] ```go import ( "os" "fmt" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" authulaenv "github.com/Authula/authula/env" oauth2plugin "github.com/Authula/authula/plugins/oauth2" oauth2plugintypes "github.com/Authula/authula/plugins/oauth2/types" ) func main() { config := authulaconfig.NewConfig( authulaconfig.WithSecurity(authulamodels.SecurityConfig{ // Make sure to configure CORS and Trusted Origins appropriately TrustedOrigins: []string{"your frontend origin"}, CORS: authulamodels.CORSConfig{ AllowCredentials: true, AllowedOrigins: []string{"your frontend origin"}, }, }), ) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{ oauth2plugin.New(oauth2plugintypes.OAuth2PluginConfig{ Enabled: true, Providers: map[string]oauth2plugintypes.ProviderConfig{ "google": { Enabled: true, ClientID: os.Getenv(authulaenv.EnvGoogleClientID), ClientSecret: os.Getenv(authulaenv.EnvGoogleClientSecret), RedirectURL: fmt.Sprintf("%s%s/oauth2/callback/google", config.BaseURL, config.BasePath), }, }, }), }, }) } ``` ### Test the integration [#test-the-integration] 1. Start your Authula server. 1. In your webapp, create a button when clicked on, it navigates the user to: ```bash # (replace localhost with your Authula server URL in production). http://localhost:8080/auth/oauth2/authorize/google?redirect_to= ``` 1. You should be redirected to Google's authorization page. After authorizing, you will be redirected back to your application. # OAuth2 Plugin ## Overview [#overview] The OAuth2 plugin for Authula allows you to integrate third-party OAuth2 providers into your authentication flow, enabling users to sign in using their accounts from popular services like Google, GitHub, and Discord. ### Features [#features] - **Third-Party Authentication** — Allow users to authenticate using existing accounts from OAuth2-enabled providers - **Multiple Provider Support** — Configure and enable multiple OAuth2 providers simultaneously (Google, GitHub, Discord) - **Automatic Token Management** — Handles authorization requests, token exchanges, and user profile retrieval - **Security Best Practices** — Built-in support for secure credential management via environment variables ### Supported Providers [#supported-providers] - **[Discord](discord.mdx)** — Enable sign-in with Discord accounts - **[GitHub](github.mdx)** — Allow users to authenticate using their GitHub credentials - **[Google](google.mdx)** — Integrate Google OAuth2 for seamless user authentication Each provider guide includes step-by-step instructions for obtaining credentials, configuring the provider, and testing the integration. --- ## Configuration [#configuration] `Standalone Mode:` ```toml [plugins.oauth2] enabled = true # SECURITY NOTE: It is recommended to set the 'client_secret' for each provider via their # respective environment variables as shown in the .env.example file rather than hardcoding it here. [plugins.oauth2.providers.google] enabled = true client_id = "your-client-id" client_secret = "your-client-secret" redirect_url = "http://localhost:8080/auth/oauth2/callback/google" scopes = [] ``` `Library Mode:` ```go import ( oauth2plugin "github.com/Authula/authula/plugins/oauth2" oauth2plugintypes "github.com/Authula/authula/plugins/oauth2/types" ) oauth2plugin.New(oauth2plugintypes.OAuth2PluginConfig{ Enabled: true, Providers: map[string]oauth2plugintypes.ProviderConfig{ "google": { Enabled: true, ClientID: os.Getenv("GOOGLE_CLIENT_ID"), ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"), RedirectURL: "https://yourdomain.com/auth/oauth2/callback/google", }, }, }) ``` --- ## API Reference [#api-reference] | HTTP Method | Route Path | Description | | ----------- | ------------------------------ | ----------------------------------------- | | `GET` | `/oauth2/authorize/{provider}` | Initiate OAuth2 authorization flow | | `GET` | `/oauth2/callback/{provider}` | Handle OAuth2 callback and token exchange | --- ## Database Schema [#database-schema] This plugin doesn't create any database tables. --- ## Plugin Capabilities [#plugin-capabilities] This plugin doesn't have any hooks and capabilities. --- ## Security Recommendations [#security-recommendations] - **Use Environment Variables for Secrets** — Never hardcode `client_secret` values in your TOML configuration. Use environment variables as shown in the `.env.example` file. - **Secure Redirect URLs** — Ensure redirect URLs use HTTPS in production to prevent token interception. - **Validate Provider Scopes** — Only request the minimum scopes necessary for your application to reduce security exposure. --- ## Client Plugin [#client-plugin] If you're using the [Authula SDK](https://github.com/Authula/authula-node-sdk), add the plugin to the SDK like so: ```typescript import { createClient } from "authula"; import { OAuth2Plugin } from "authula/plugins"; export const authulaClient = createClient({ url: "http://localhost:8080/auth", plugins: [ // other plugins... new OAuth2Plugin(), ], }); ``` # Organizations Plugin ## Overview [#overview] The Organizations plugin provides multi-tenancy and team management capabilities. It enables users to create organizations, manage members with roles, organize members into teams, and send email-based invitations. ### Core Entities [#core-entities] - **Organizations**: Owner-based organizational units - **Invitations**: Email-based invitations for joining organizations - **Members**: Users assigned to organizations with roles - **Teams**: Group members into teams within organizations - **Team Members**: Users assigned to teams within organizations ### Features [#features] - Multi-tenant organization management - Role-based member management within organizations - Team organization and management within organizations - Email-based member invitations with configurable expiration - Team member management - Full lifecycle management (create, read, update, delete operations) This plugin has a dependency on the Access Control plugin for role-based access control. Make sure to configure the Access Control plugin to utilize all features of the Organizations plugin. --- ## Configuration [#configuration] `Standalone Mode:` ```toml [plugins.organizations] enabled = true organizations_limit = 10 # Optional limit on number of organizations (set to 0 for unlimited) members_limit = 100 # Optional limit on number of members per organization (set to 0 for unlimited) invitations_limit = 100 # Optional limit on number of invitations sent to a user (100 by default) invitation_expires_in = "24h" # Expiration time for organization invitations (24h = 1 day by default) require_email_verified_on_invitation = true # Whether to require the invited user's email to be verified before accepting or rejecting an organization invitation (false by default) ``` `Library Mode:` ```go import ( "time" authulamodels "github.com/Authula/authula/models" organizationsplugin "github.com/Authula/authula/plugins/organizations" organizationsplugintypes "github.com/Authula/authula/plugins/organizations/types" ) organizationsplugin.New(organizationsplugintypes.OrganizationsPluginConfig{ Enabled: true, OrganizationsLimit: new(10), MembersLimit: new(100), InvitationsLimit: new(100), InvitationExpiresIn: 7 * 24 * time.Hour, RequireEmailVerifiedOnInvitation: true, ServiceHooks: &organizationsplugintypes.OrganizationsServiceHooksConfig{ // Optional service hooks for custom logic on organization lifecycle events }, SendOrganizationInvitationEmail: func( params organizationsplugintypes.SendOrganizationInvitationEmailParams, reqCtx *authulamodels.RequestContext, ) error { // Optionally handle email sending here... return nil }, }) ``` --- ## API Reference [#api-reference] Each endpoint in this plugin requires the caller to have a specific hardcoded permission assigned to their role(s). ### Organizations [#organizations] | HTTP Method | Route Path | Description | Required Permission | | ----------- | ---------------------------------- | ------------------------- | ---------------------- | | `POST` | `/organizations` | Create organization | `None` | | `GET` | `/organizations` | List user's organizations | `None` | | `GET` | `/organizations/{organization_id}` | Get organization | `organizations:read` | | `PATCH` | `/organizations/{organization_id}` | Update organization | `organizations:update` | | `DELETE` | `/organizations/{organization_id}` | Delete organization | `organizations:delete` | ### Invitations [#invitations] | HTTP Method | Route Path | Description | Required Permission | | ----------- | --------------------------------------------------------------------- | ----------------- | ---------------------------------- | | `POST` | `/organizations/{organization_id}/invitations` | Create invitation | `organizations:invitations:create` | | `GET` | `/organizations/{organization_id}/invitations` | List invitations | `organizations:invitations:list` | | `GET` | `/organizations/{organization_id}/invitations/{invitation_id}` | Get invitation | `organizations:invitations:read` | | `PATCH` | `/organizations/{organization_id}/invitations/{invitation_id}/revoke` | Revoke invitation | `organizations:invitations:revoke` | | `PATCH` | `/organizations/{organization_id}/invitations/{invitation_id}/accept` | Accept invitation | `None` | | `PATCH` | `/organizations/{organization_id}/invitations/{invitation_id}/reject` | Reject invitation | `None` | ### Members [#members] | HTTP Method | Route Path | Description | Required Permission | | ----------- | ------------------------------------------------------------ | --------------------- | ------------------------------ | | `POST` | `/organizations/{organization_id}/members` | Add member | `organizations:members:add` | | `GET` | `/organizations/{organization_id}/members` | List members | `organizations:members:list` | | `GET` | `/organizations/{organization_id}/members/{member_id}` | Get member | `organizations:members:read` | | `GET` | `/organizations/{organization_id}/members/by-user/{user_id}` | Get member by user ID | `organizations:members:read` | | `PATCH` | `/organizations/{organization_id}/members/{member_id}` | Update member | `organizations:members:update` | | `DELETE` | `/organizations/{organization_id}/members/{member_id}` | Remove member | `organizations:members:remove` | ### Teams [#teams] | HTTP Method | Route Path | Description | Required Permission | | ----------- | -------------------------------------------------- | ----------- | ---------------------------- | | `POST` | `/organizations/{organization_id}/teams` | Create team | `organizations:teams:create` | | `GET` | `/organizations/{organization_id}/teams` | List teams | `organizations:teams:list` | | `GET` | `/organizations/{organization_id}/teams/{team_id}` | Get team | `organizations:teams:read` | | `PATCH` | `/organizations/{organization_id}/teams/{team_id}` | Update team | `organizations:teams:update` | | `DELETE` | `/organizations/{organization_id}/teams/{team_id}` | Delete team | `organizations:teams:delete` | ### Team Members [#team-members] | HTTP Method | Route Path | Description | Required Permission | | ----------- | ---------------------------------------------------------------------- | ----------------------- | ----------------------------------- | | `POST` | `/organizations/{organization_id}/teams/{team_id}/members` | Add member to team | `organizations:team-members:add` | | `GET` | `/organizations/{organization_id}/teams/{team_id}/members` | List team members | `organizations:team-members:list` | | `GET` | `/organizations/{organization_id}/teams/{team_id}/members/{member_id}` | Get team member | `organizations:team-members:read` | | `DELETE` | `/organizations/{organization_id}/teams/{team_id}/members/{member_id}` | Remove member from team | `organizations:team-members:remove` | --- ## Database Schema [#database-schema] This plugin creates the following database tables: ### Table: `organizations` [#table-organizations] | Field | Type | Key | Description | | ------------ | ----------- | ---- | --------------------------------------------- | | `id` | `uuid` | `PK` | Unique identifier for the organization | | `owner_id` | `uuid` | `FK` | Reference to the organization owner (user) | | `name` | `string` | - | Organization name | | `slug` | `string` | - | URL-friendly organization identifier (unique) | | `logo` | `string?` | - | Organization logo URL | | `metadata` | `JSON` | - | Additional organization metadata | | `created_at` | `timestamp` | - | Record creation time | | `updated_at` | `timestamp` | - | Record last update time | ### Table: `organization_invitations` [#table-organization_invitations] | Field | Type | Key | Description | | ----------------- | ----------- | ---- | --------------------------------------------- | | `id` | `uuid` | `PK` | Unique identifier for the invitation | | `organization_id` | `uuid` | `FK` | Reference to the organization | | `inviter_id` | `uuid` | `FK` | Reference to the user who sent the invitation | | `email` | `string` | - | Email address being invited | | `role` | `string` | - | Role assigned to the invited member | | `status` | `string` | - | Invitation status | | `expires_at` | `timestamp` | - | Invitation expiration time | | `created_at` | `timestamp` | - | Record creation time | ### Table: `organization_members` [#table-organization_members] | Field | Type | Key | Description | | ----------------- | ----------- | ---- | --------------------------------------- | | `id` | `uuid` | `PK` | Unique identifier for the member record | | `organization_id` | `uuid` | `FK` | Reference to the organization | | `user_id` | `uuid` | `FK` | Reference to the user | | `role` | `string` | - | Member's role within the organization | | `created_at` | `timestamp` | - | Record creation time | | `updated_at` | `timestamp` | - | Record last update time | ### Table: `organization_teams` [#table-organization_teams] | Field | Type | Key | Description | | ----------------- | ----------- | ---- | ------------------------------ | | `id` | `uuid` | `PK` | Unique identifier for the team | | `organization_id` | `uuid` | `FK` | Reference to the organization | | `name` | `string` | - | Team name | | `slug` | `string` | - | URL-friendly identifier | | `description` | `string?` | - | Description | | `metadata` | `JSON` | - | Additional metadata | | `created_at` | `timestamp` | - | Record creation time | | `updated_at` | `timestamp` | - | Record last update time | ### Table: `organization_team_members` [#table-organization_team_members] | Field | Type | Key | Description | | ------------ | ----------- | ---- | -------------------------------------------- | | `id` | `uuid` | `PK` | Unique identifier for the team member record | | `team_id` | `uuid` | `FK` | Reference to the team | | `member_id` | `uuid` | `FK` | Reference to the organization member | | `created_at` | `timestamp` | - | Record creation time | Migrations are automatically handled when the plugin is initialized. ### Service Hooks [#service-hooks] This plugin supports service-level hooks, providing lifecycle callbacks with access to the actor context. Hooks are only supported in Library Mode. The following hook configurations are available via `OrganizationsServiceHooksConfig`: **Organizations** — `OrganizationServiceHooksConfig`: | Hook | Signature | | -------------- | ------------------------------------------------------------------ | | `BeforeCreate` | `func(ctx, actor *models.Actor, organization *Organization) error` | | `AfterCreate` | `func(ctx, actor *models.Actor, organization *Organization) error` | | `BeforeUpdate` | `func(ctx, actor *models.Actor, organization *Organization) error` | | `AfterUpdate` | `func(ctx, actor *models.Actor, organization *Organization) error` | | `BeforeDelete` | `func(ctx, actor *models.Actor, organization *Organization) error` | | `AfterDelete` | `func(ctx, actor *models.Actor, organization *Organization) error` | **Members** — `OrganizationMemberServiceHooksConfig`: | Hook | Signature | | -------------- | ------------------------------------------------------------------ | | `BeforeCreate` | `func(ctx, actor *models.Actor, member *OrganizationMember) error` | | `AfterCreate` | `func(ctx, actor *models.Actor, member *OrganizationMember) error` | | `BeforeUpdate` | `func(ctx, actor *models.Actor, member *OrganizationMember) error` | | `AfterUpdate` | `func(ctx, actor *models.Actor, member *OrganizationMember) error` | | `BeforeDelete` | `func(ctx, actor *models.Actor, member *OrganizationMember) error` | | `AfterDelete` | `func(ctx, actor *models.Actor, member *OrganizationMember) error` | **Invitations** — `OrganizationInvitationServiceHooksConfig`: | Hook | Signature | | -------------- | -------------------------------------------------------------------------- | | `BeforeCreate` | `func(ctx, actor *models.Actor, invitation *OrganizationInvitation) error` | | `AfterCreate` | `func(ctx, actor *models.Actor, invitation *OrganizationInvitation) error` | | `BeforeUpdate` | `func(ctx, actor *models.Actor, invitation *OrganizationInvitation) error` | | `AfterUpdate` | `func(ctx, actor *models.Actor, invitation *OrganizationInvitation) error` | **Teams** — `OrganizationTeamServiceHooksConfig`: | Hook | Signature | | -------------- | -------------------------------------------------------------- | | `BeforeCreate` | `func(ctx, actor *models.Actor, team *OrganizationTeam) error` | | `AfterCreate` | `func(ctx, actor *models.Actor, team *OrganizationTeam) error` | | `BeforeUpdate` | `func(ctx, actor *models.Actor, team *OrganizationTeam) error` | | `AfterUpdate` | `func(ctx, actor *models.Actor, team *OrganizationTeam) error` | | `BeforeDelete` | `func(ctx, actor *models.Actor, team *OrganizationTeam) error` | | `AfterDelete` | `func(ctx, actor *models.Actor, team *OrganizationTeam) error` | **Team Members** — `OrganizationTeamMemberServiceHooksConfig`: | Hook | Signature | | -------------- | ---------------------------------------------------------------------- | | `BeforeCreate` | `func(ctx, actor *models.Actor, member *OrganizationTeamMember) error` | | `AfterCreate` | `func(ctx, actor *models.Actor, member *OrganizationTeamMember) error` | | `BeforeDelete` | `func(ctx, actor *models.Actor, member *OrganizationTeamMember) error` | | `AfterDelete` | `func(ctx, actor *models.Actor, member *OrganizationTeamMember) error` | Service hooks are only supported in Library Mode. --- ## Plugin Capabilities [#plugin-capabilities] Service hooks can be used to execute custom logic at various points in the organization, member, invitation, team, and team member lifecycle. See the [Service Hooks](#service-hooks) section for a full list of available hooks. --- ## Security Recommendations [#security-recommendations] - Ensure that the Access Control plugin is properly configured to manage permissions for organization-related actions. - Regularly review organization members and their roles to maintain proper access control. - Make sure to always require authentication for all organization-related API routes and enforce role-based access control for certain routes. --- ## Client Plugin [#client-plugin] If you're using the [Authula SDK](https://github.com/Authula/authula-node-sdk), add the plugin to the client instance as follows: ```typescript import { createClient } from "authula"; import { OrganizationsPlugin } from "authula/plugins"; export const authulaClient = createClient({ url: "http://localhost:8080/auth", plugins: [new OrganizationsPlugin()], }); ``` # Rate Limit Plugin ## Overview [#overview] The Rate Limit plugin provides request rate limiting with multiple storage backends and a flexible custom rule system to protect your application from abuse and brute force attacks. It uses a fixed-window counter algorithm and supports method-level rules, parameterized path matching, and wildcard route patterns. ### Features [#features] - **Multiple Storage Backends** — In-memory (default), Redis, or Database storage - **Custom Rules Per Endpoint** — Different limits for different paths or methods - **Method-Level Rules** — Apply rules to specific HTTP methods (e.g., `POST:/auth/login`) - **Parameterized Path Matching** — Rules match dynamic route parameters like `{id}` and wildcards via normalization - **Auto-Pattern Fallback** — Parameterized or wildcard routes automatically share a single rate limit counter - **Per-Key Stored Rules** — Other plugins can set rate limits for specific API keys or identifiers - **Client IP Hashing** — Optional SHA-256 hashing of client IPs for privacy compliance - **Client IP-Based Limiting** — Tracks requests by client IP address - **Standard HTTP Rate Limit Headers** — Client-aware headers for rate limit information - **Automatic Cleanup** — Removes expired entries to prevent memory leaks - **Fail-Open Behavior** — Allows requests when storage provider errors occur - **Production Auto-Enable** — Automatically enabled when `GO_ENVIRONMENT=production` --- ## Configuration [#configuration] `Standalone Mode:` ```toml [plugins.ratelimit] enabled = true window = "1m" # Time window for rate limiting (default: 1 minute) max = 100 # Max requests per window (default: 100) prefix = "ratelimit:" # Storage key prefix (default: "ratelimit:") provider = "memory" # Options: memory, redis, database (default: memory) hash_client_ip = false # Hash client IPs with SHA-256 (default: false) # In-Memory storage configuration (default provider) [plugins.ratelimit.memory] cleanup_interval = "1m" # How often to clean expired entries (default: 1 minute) # Database storage configuration (optional) [plugins.ratelimit.database] cleanup_interval = "1m" # How often to clean expired entries (default: 1 minute) # Custom rules for specific endpoints [plugins.ratelimit.custom_rules] # Method-specific rule: strict limit on POST /auth/sign-in "POST:/api/auth/sign-in" = { window = "15m", max = 10 } # Path-only rule: applies to all HTTP methods on /api/public "/api/health" = { disabled = true } # Parameterized path: matches any organization ID under /organizations/{organization_id} "/api/auth/organizations/{organization_id}" = { window = "5m", max = 30 } ``` `Library Mode:` ```go import ( ratelimitplugin "github.com/Authula/authula/plugins/rate-limit" ) ratelimitplugin.New(ratelimitplugin.RateLimitPluginConfig{ Enabled: true, Provider: ratelimitplugin.RateLimitProviderRedis, Window: time.Minute, Max: 100, Prefix: "ratelimit:", HashClientIP: true, CustomRules: map[string]ratelimitplugin.RateLimitRule{ "POST:/api/auth/sign-in": { Window: 15 * time.Minute, Max: 10, }, "/api/health": { Disabled: true, }, }, }) ``` --- ## Storage Providers [#storage-providers] | Provider | Performance | Persistence | Best For | | ------------ | ----------- | ----------- | --------------------------- | | **Memory** | Fastest | No | Single-instance deployments | | **Database** | Moderate | Yes | Strict audit requirements | | **Redis** | Fast | Yes | Multi-instance deployments | --- ## API Reference [#api-reference] The Rate Limit plugin does not expose its own HTTP endpoints. Instead, it applies rate limiting as a hook to all incoming requests. ### Custom Rules [#custom-rules] Custom rules override the default rate limit for specific endpoints using a multi-strategy matching algorithm. Each rule key is either: - A **method+path** pattern (e.g., `POST:/api/auth/sign-in`) — only applies to that HTTP method - A **path-only** pattern (e.g., `/api/health`) — applies to all HTTP methods on that path #### Rule Format [#rule-format] ```toml [plugins.ratelimit.custom_rules] # Method-specific: only applies to POST requests "POST:/api/auth/sign-in" = { window = "15m", max = 10 } # Path-only: applies to all HTTP methods on this path "/api/health" = { disabled = true } ``` #### Matching Algorithm [#matching-algorithm] When a request arrives, the plugin builds candidate patterns from the request and evaluates them in order. **The first match wins** and its rule is applied. **Patterns evaluated** (in order): 1. The router's registered path pattern (e.g., `GET:/api/auth/organizations/{organization_id}`) 2. The actual request method and path (e.g., `GET:/api/auth/organizations/my-org`) For each pattern, the plugin tries four matching strategies: | Priority | Strategy | Example | | -------- | --------------------------------------------- | --------------------------------------------------------------------------- | | **1** | Exact map lookup on the full pattern | `customRules["GET:/api/auth/organizations/{organization_id}"]` | | **2** | Normalized match with segment count guard | `{organization_id}` → `{*}`, then compare normalized keys and segment count | | **3** | Path-only exact match (strips method prefix) | `customRules["/api/auth/organizations/{organization_id}"]` | | **4** | Normalized path-only match with segment guard | Strip method, normalize params, verify segments | #### Parameter Normalization [#parameter-normalization] Path parameters like `{id}`, `{organization_id}`, and wildcards `*` are normalized to `{*}` during matching. This allows rules to match routes regardless of the parameter name: | Route Pattern | Normalized Form | | ------------------------------------------------------------------- | --------------------------------------------- | | `GET:/api/auth/organizations/{organization_id}/members/{member_id}` | `GET:/api/auth/organizations/{*}/members/{*}` | | `GET:/api/*` | `GET:/api/{*}` | | `/users/{id}` | `/users/{*}` | #### Segment Count Guard [#segment-count-guard] Before a normalized match is accepted, the plugin verifies the request path has the same number of segments as the rule key. This prevents a rule like `/api/auth/organizations/{organization_id}` from incorrectly matching a 3-segment path like `/api/auth/organizations`. #### Rule Application [#rule-application] Once a rule is matched: - If `disabled = true`, rate limiting is completely skipped for that endpoint - If `window` is set (> 0), it overrides the default window - If `max` is set (> 0), it overrides the default max limit - The rate limit counter is keyed by `{prefix}:{clientIP}:{ruleHash}` so each rule has its own counter per client #### Auto-Pattern Fallback [#auto-pattern-fallback] If no custom rule matches but the request's route pattern contains parameters (`{` or `*`), the plugin automatically keys the rate limit counter by the route pattern rather than the full path. This ensures all requests to a parameterized route (e.g., `/api/auth/organizations/{organization_id}`) share a single counter instead of each dynamic path having its own. Without this, an attacker could bypass limits by varying the path parameter. #### Per-Key Stored Rules [#per-key-stored-rules] Other plugins (e.g., API Key plugin) can set dynamic rate limits for specific identifiers at runtime by storing a `RateLimitRuleContext` in the request context. These rules are persisted by the storage provider and evaluated in the `HookBefore` phase, allowing per-client rate limits that survive restarts. ### HTTP Response Headers [#http-response-headers] The plugin adds standard rate limit headers to all responses: | Header | Description | | ----------------------- | ----------------------------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed in the window | | `X-RateLimit-Remaining` | Remaining requests in current window | | `X-RateLimit-Reset` | Unix timestamp when the window resets | | `X-Retry-After` | Seconds to wait before retry (only when rate limited) | When rate limited: ``` HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1738302000 X-Retry-After: 45 { "message": "rate limit exceeded", "retry_after": 45, "limit": 100, "remaining": 0 } ``` --- ## Database Schema [#database-schema] ### Table: `rate_limits` [#table-rate_limits] | Field | Type | Key | Description | | ------------ | --------- | --- | -------------------------------- | | `key` | string | PK | Rate limit key (e.g., client IP) | | `count` | int | - | Current request count | | `expires_at` | timestamp | - | When the entry expires | - SQLite: this table is set as a temp table to ensure it is automatically cleared on server restart. - PostgreSQL: this table is set as an unlogged table to improve performance, with a periodic cleanup process to remove expired entries. - MYSQL: this table is set as a MEMORY table to store data in memory for fast access, with automatic cleanup of expired entries. ### Table: `rate_limit_rules` [#table-rate_limit_rules] | Field | Type | Key | Description | | ---------------- | ------ | --- | ----------------------------------- | | `key` | string | PK | Rule key (e.g., API key identifier) | | `window_seconds` | int | - | Rate limit window in seconds | | `max_requests` | int | - | Maximum requests allowed per window | This table stores per-key rate limit rules set by other plugins at runtime (e.g., API Key plugin). When the same storage constraints apply (temp/unlogged/MEMORY) as `rate_limits` above. Migrations are automatically handled when the plugin is initialized. --- ## Plugin Capabilities [#plugin-capabilities] This plugin doesn't have any capabilities. However, it registers three hooks to provide rate limiting: | Hook | Order | Purpose | | --------------- | ----- | ------------------------------------------------------------------------------- | | `HookOnRequest` | 0 | Applies IP-based endpoint rate limiting using global and custom rules | | `HookBefore` | 15 | Evaluates per-key stored rules set by other plugins (e.g., API Key) | | `HookAfter` | 0 | Persists new per-key rules set via request context during the request lifecycle | --- ## Security Recommendations [#security-recommendations] - **Client IP Detection** — Uses client IP from the request. Configure `trusted_proxies` and `trusted_headers` in the global security config if behind a load balancer. - **Fail-Open** — Provider errors allow requests through to prevent denial-of-service from rate limiter failures. - **OPTIONS Requests** — Automatically skipped to prevent blocking CORS preflight requests. - **Stricter Limits for Auth Endpoints** — Apply lower limits to sensitive endpoints on other plugins to prevent brute force attacks. - **Use Redis for Multi-Instance** — When running multiple server instances, use Redis to share rate limit state across all instances. # Secondary Storage Plugin ## Overview [#overview] The Secondary Storage plugin allows you to use various key-value store options such as in-memory, database, and custom implementations (e.g. Redis) for rate limiting counters and other high-frequency records. This is useful for offloading intensive operations from your main database to a fast storage layer. ### Features [#features] - **Multiple Storage Providers** — In-memory, database, or Redis backends - **Scalability** — Offload high-churn data from your main database - **Flexible Adapters** — Implement custom storage adapters for Redis, Valkey, or any key-value store - **Automatic Cleanup** — Configurable cleanup intervals for expired entries - **Plugin Dependency** — Optionally used by various plugins for efficient storage and other transient/ephemeral data --- ## Configuration [#configuration] `Standalone Mode:` ```toml [plugins.secondary_storage] enabled = true provider = "memory" # Options: "memory", "database", "redis" [plugins.secondary_storage.memory] cleanup_interval = "1m" ``` `Library Mode:` ```go import ( "os" secondarystorageplugin "github.com/Authula/authula/plugins/secondary-storage" ) secondarystorageplugin.New(secondarystorageplugin.SecondaryStoragePluginConfig{ Enabled: true, Provider: secondarystorageplugin.SecondaryStorageProviderRedis, Redis: &secondarystorageplugin.RedisStorageConfig{ URL: os.Getenv("REDIS_URL"), }, }) ``` NOTE: The secondary storage plugin MUST be registered before the rate-limit plugin if you plan to use the Redis provider. This is due to the Rate Limit plugin needing the secondary storage service as a dependency. --- ## API Reference [#api-reference] This plugin doesn't expose any HTTP endpoints. --- ## Database Schema [#database-schema] ### Table: `key_value_store` [#table-key_value_store] | Field | Type | Key | Description | | ------------ | ------------ | ---- | ----------------------- | | `key` | `string` | `PK` | Storage key | | `value` | `string` | - | Serialized value | | `expires_at` | `timestamp?` | - | When the entry expires | | `created_at` | `timestamp` | - | Record creation time | | `updated_at` | `timestamp` | - | Record last update time | Migrations are automatically handled when the plugin is initialized. --- ## Plugin Capabilities [#plugin-capabilities] This plugin doesn't provide any hooks and capabilities. --- ## Storage Providers [#storage-providers] The Secondary Storage plugin provides a storage abstraction used by other plugins for high-frequency data. | Provider | Performance | Persistence | Best For | | ------------ | ----------- | ----------- | --------------------------------- | | **Memory** | Fastest | No | Single-instance, development | | **Database** | Moderate | Yes | Persistent storage, audit trails | | **Redis** | Fast | Yes | Multi-instance, distributed cache | ### Usage Example [#usage-example] The secondary storage plugin must be registered before plugins that depend on it (e.g. `rate-limit` plugin). ### Database Provider: [#database-provider] `Standalone Mode:` ```toml [plugins.secondary_storage] enabled = true provider = "database" [plugins.secondary_storage.database] cleanup_interval = "1m" ``` `Library Mode:` ```go import ( "os" authula "github.com/Authula/authula" authulamodels "github.com/Authula/authula/models" authulaconfig "github.com/Authula/authula/config" secondarystorageplugin "github.com/Authula/authula/plugins/secondary-storage" ) config := authulaconfig.NewConfig(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{ secondarystorageplugin.New(secondarystorageplugin.SecondaryStoragePluginConfig{ Enabled: true, Provider: secondarystorageplugin.SecondaryStorageProviderDatabase, Database: &secondarystorageplugin.DatabaseStorageConfig{ CleanupInterval: time.Minute, }, }), }, }) ``` ### Redis Provider: [#redis-provider] `Standalone Mode:` ```toml [plugins.secondary_storage] enabled = true provider = "redis" [plugins.secondary_storage.redis] # NOTE: It is highly recommended to set the Redis URL via the `REDIS_URL` environment variable rather than hardcoding it in the configuration file. # url = "redis://localhost:6379" # Optional: Specifies the maximum number of retries for failed Redis operations. Default is 3. max_retries = 3 # Optional: Specifies the size of the Redis connection pool. Default is 10. pool_size = 10 # Optional: Specifies the timeout for Redis connections. Default is 30s. pool_timeout = "30s" ``` `Library Mode:` ```go import ( "os" authula "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulaenv "github.com/Authula/authula/env" secondarystorageplugin "github.com/Authula/authula/plugins/secondary-storage" ) config := authulaconfig.NewConfig(/*...*/) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{ secondarystorageplugin.New(secondarystorageplugin.SecondaryStoragePluginConfig{ Enabled: true, Provider: secondarystorageplugin.SecondaryStorageProviderRedis, Redis: &secondarystorageplugin.RedisStorageConfig{ URL: os.Getenv(authulaenv.EnvRedisURL), // Optional Redis configuration parameters... }, }), }, }) ``` --- ## Security Recommendations [#security-recommendations] - **Use Environment Variables for Secrets** — Set Redis URLs and credentials via environment variables (e.g., `REDIS_URL`) rather than hardcoding them in configuration files. - **Connection Pool Limits** — Configure appropriate `pool_size` and `pool_timeout` values to prevent connection exhaustion. - **Retry Configuration** — Set `max_retries` to handle transient network failures gracefully. # Session Plugin ## Overview [#overview] The Session plugin provides cookie-based session authentication with automatic renewal and secure cookie management for your application. Whilst the main authentication configuration already configures the session and any properties to do with it, the Session plugin provides the actual implementation of cookie-based sessions, including issuing cookies, validating them on each request, and automatically renewing them as they approach expiration. ### Features [#features] - **Cookie-Based Sessions** — Secure HTTP-only cookies for session tracking - **Sliding Window Renewal** — Sessions automatically renew when approaching expiration - **Secure Cookie Attributes** — Configurable HttpOnly, Secure, and SameSite settings - **Automatic Cleanup** — Optionally, expired sessions are automatically purged - **Cooperative Authentication** — Works seamlessly with other auth plugins (e.g., JWT) without conflicts --- ## Configuration [#configuration] `Standalone Mode:` ```toml # Global Session Configuration [session] cookie_name = "authula.session_token" expires_in = "30m" update_age = "5m" cookie_max_age = "24h" secure = false http_only = true same_site = "lax" # Options: "lax", "strict", "none" auto_cleanup = false # Flag to automatically clean up expired sessions and enforce max sessions per user cleanup_interval = "1m" # Interval for automatic cleanup of expired sessions max_sessions_per_user = 5 # Maximum number of concurrent sessions per user (0 = unlimited) # Enable the Session plugin [plugins.session] enabled = true ``` `Library Mode:` ```go import ( "github.com/Authula/authula" authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" sessionplugin "github.com/Authula/authula/plugins/session" ) config := authulaconfig.NewConfig( authulaconfig.WithSession(authulamodels.SessionConfig{ CookieName: "authula.session_token", ExpiresIn: 24 * time.Hour, UpdateAge: 5 * time.Minute, CookieMaxAge: 24 * time.Hour, Secure: false, HttpOnly: true, SameSite: "lax", MaxSessionsPerUser: 5, AutoCleanup: true, CleanupInterval: time.Minute, }), ) auth := authula.New(&authula.AuthConfig{ Config: config, Plugins: []authulamodels.Plugin{ sessionplugin.New(sessionplugin.SessionPluginConfig{ Enabled: true, }), }, }) ``` --- ## API Reference [#api-reference] The Session plugin doesn't expose any HTTP endpoints. --- ## Database Schema [#database-schema] The Session plugin doesn't create any database tables. --- ## Plugin Capabilities [#plugin-capabilities] The Session plugin provides the following hooks and capabilities: - Global hook to issue session cookie after successful authentication - Global hook to remove session cookie on sign-out - `session.auth` - Validates session cookie; returns 401 Unauthorized if invalid or missing - `session.auth.optional` - Validates session if present; silently continues if invalid or missing ### Usage [#usage] `Standalone Mode:` ```toml [[route_mappings]] paths = ["GET:/some/path"] # Requires authentication - returns 401 if session is missing or invalid plugins = ["session.auth"] [[route_mappings]] paths = ["GET:/some/other/path"] # Optional authentication - allows access even if session is missing or invalid plugins = ["session.auth.optional"] ``` `Library Mode:` ```go import ( authulaconfig "github.com/Authula/authula/config" authulamodels "github.com/Authula/authula/models" sessionplugin "github.com/Authula/authula/plugins/session" ) authulaconfig.NewConfig( authulaconfig.WithRouteMappings([]authulamodels.RouteMapping{ { Paths: []string{"GET:/some/path"}, Plugins: []string{sessionplugin.HookIDSessionAuth.String()}, }, { Paths: []string{"GET:/some/other/path"}, Plugins: []string{sessionplugin.HookIDSessionAuthOptional.String()}, }, }), ) ``` --- ## Security Recommendations [#security-recommendations] - **Token Hashing** — Session tokens are hashed before storage in the database. - **HttpOnly Cookies** — Prevents XSS attacks by restricting JavaScript access. - **Secure Flag** — Ensures cookies are only transmitted over HTTPS. Set `secure = true` in production. - **SameSite Protection** — Defends against CSRF attacks. Use `strict` for maximum security, `lax` for typical web apps. - **Automatic Expiration** — Sessions cannot be used beyond their expiration time. Configure `expires_in` and `update_age` based on your security requirements. - **Session Cleanup** — Enable `auto_cleanup` to automatically remove expired sessions from the database. # TOTP Plugin ## Overview [#overview] The TOTP plugin provides Time-based One-Time Password (TOTP) authentication and backup codes for implementing two-factor authentication (2FA) in your application. This plugin enables users to secure their accounts with authenticator apps like Google Authenticator, Microsoft Authenticator, or Authy, along with backup codes for account recovery. ### Features [#features] - **TOTP Generation & Verification** — Supports standard TOTP algorithm with configurable digit count and period - **Backup Codes** — Automatically generate backup codes for account recovery - **Trusted Devices** — Option to trust devices to skip TOTP verification on subsequent logins - **Event Hooks** — Integration with the event bus for custom actions on TOTP events - **Configurable Settings** — Customizable expiry times, cookie settings, and security options --- ## Configuration [#configuration] `Standalone Mode:` ```toml [plugins.totp] enabled = true skip_verification_on_enable = false # Skip password verification on enable backup_code_count = 10 # Number of backup codes to generate (default: 10) trusted_device_duration = "720h" # How long to trust a device (30 days default) trusted_devices_auto_cleanup = true # Automatically clean up expired trusted devices (default: false) trusted_devices_cleanup_interval = "1h" # Interval for cleaning up expired trusted devices (default: 1 hour) pending_token_expiry = "5m" # Expiry time for pending token (default: 5 minutes) secure_cookie = true # Use secure flag on cookies (set to true in production) same_site = "lax" # SameSite cookie policy (lax, strict, none) ``` `Library Mode:` ```go import ( totpplugin "github.com/Authula/authula/plugins/totp" totpplugintypes "github.com/Authula/authula/plugins/totp/types" ) totpplugin.New(totpplugintypes.TOTPPluginConfig{ Enabled: true, SkipVerificationOnEnable: false, BackupCodeCount: 10, TrustedDeviceDuration: 30 * 24 * time.Hour, TrustedDevicesAutoCleanup: true, TrustedDevicesCleanupInterval: 1 * time.Hour, PendingTokenExpiry: 5 * time.Minute, SecureCookie: true, SameSite: "lax", }) ``` NOTE: Make sure to set the `AppName` in your `config.toml` in standalone mode or in your AuthConfig in library mode, as it is used in the TOTP URI for authenticator apps. --- ## API Reference [#api-reference] | HTTP Method | Route Path | Description | | ----------- | ----------------------------- | ------------------------------- | | `POST` | `/totp/enable` | Enable TOTP for user | | `POST` | `/totp/disable` | Disable TOTP for user | | `GET` | `/totp/get-uri` | Get TOTP URI | | `POST` | `/totp/verify` | Verify TOTP code during login | | `POST` | `/totp/verify-backup-code` | Verify backup code during login | | `POST` | `/totp/generate-backup-codes` | Generate new backup codes | ### Authentication Notes [#authentication-notes] - **Authenticated endpoints** (`enable`, `disable`, `get-uri`, `generate-backup-codes`): Standard authentication required. User must be authenticated via session or other auth mechanism. - **Pending-token endpoints** (`verify`, `verify-backup-code`): Standard authentication NOT required. These routes require a valid TOTP pending token cookie set by the initial authentication attempt. ### Endpoint Details [#endpoint-details] `POST /totp/enable:` No request body required. `Response:` ```json { "totp_uri": "otpauth://totp/user@example.com?secret=ABCXYZ...&issuer=MyApp", "backup_codes": ["aBcXyZ", ...] } ``` `POST /totp/disable:` No request body required. `Response:` ```json { "message": "..." } ``` `GET /totp/get-uri:` `Response:` ```json { "totp_uri": "otpauth://totp/user@example.com?secret=ABCXYZ...&issuer=MyApp" } ``` `POST /totp/verify:` `Request:` ```json { "code": "123456", "trust_device": true } ``` - `code`: (Required) The 6-digit TOTP code to verify - `trust_device`: (Optional) If true, marks the device as trusted to skip TOTP verification on future logins for the configured duration `Response:` ```json { "user": { /* ... */ }, "session": { /* ... */ } } ``` `POST /totp/verify-backup-code:` `Request:` ```json { "code": "3oz4vtq7ci2n", "trust_device": true } ``` - `code`: (Required) The backup code to verify. Backup codes are single-use and will be invalidated after successful verification. - `trust_device`: (Optional) If true, marks the device as trusted to skip TOTP verification on future logins for the configured duration `Response:` ```json { "user": { /* ... */ }, "session": { /* ... */ } } ``` `POST /totp/generate-backup-codes:` No request body required. `Response:` ```json { "backup_codes": ["aBcXyZ", ...] } ``` --- ## Database Schema [#database-schema] ### Table: `totp` [#table-totp] | Field | Type | Key | Description | | -------------- | --------- | --- | ------------------------------------- | | `id` | string | PK | Unique identifier for the TOTP record | | `user_id` | string | FK | Reference to the user | | `secret` | string | - | Encrypted TOTP secret key | | `backup_codes` | string | - | Serialized hashed backup codes | | `enabled` | boolean | - | Whether TOTP is enabled | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | ### Table: `trusted_devices` [#table-trusted_devices] | Field | Type | Key | Description | | ------------ | --------- | --- | ---------------------------------------- | | `id` | string | PK | Unique identifier for the trusted device | | `user_id` | string | FK | Reference to the user | | `token` | string | - | Hashed device trust token | | `user_agent` | string | - | Device user agent | | `expires_at` | timestamp | - | When the device trust expires | | `created_at` | timestamp | - | Record creation time | Migrations are automatically handled when the plugin is initialized. --- ## Plugin Capabilities [#plugin-capabilities] This plugin has the following hooks and capabilities: - `totp.intercept` — Intercepts authentication flow by setting the user a pending token cookie and returning a simple JSON response indicating a TOTP redirection. The client can then check for this response and redirect the user to the appropriate TOTP verification page. --- ## Events [#events] The TOTP plugin emits events that can be used to trigger custom business logic or integrate with other plugins via the event bus. ### Event Hooks [#event-hooks] The TOTP plugin emits events for the following operations: - `totp.enabled` — Fired when TOTP is enabled for a user - `totp.disabled` — Fired when TOTP is disabled for a user - `totp.verified` — Fired when a TOTP code is successfully verified - `totp.backup_code_used` — Fired when a backup code is used - `totp.device_trusted` — Fired when a device is marked as trusted These events can be subscribed to via the event bus for logging, analytics, or custom business logic. ### Route Mappings [#route-mappings] When using the TOTP plugin, keep `/totp/verify` and `/totp/verify-backup-code` accessible to the pending-token flow (do not force authentication on these routes). --- ## Security Recommendations [#security-recommendations] - **Secure Cookies** — In production, always set `secure_cookie = true` to ensure cookies are only sent over HTTPS. - **Pending Token Expiry** — The pending token used to access verify routes expires in 5 minutes by default. Configure based on your security requirements. - **Backup Codes** — Store backup codes securely. They are single-use and provide account recovery. - **Trusted Devices** — Use appropriate `trusted_device_duration` values. Shorter durations increase security but require more frequent TOTP verification. - **Password Verification** — Set `skip_verification_on_enable = false` to require password verification when enabling TOTP. --- ## Client Plugin [#client-plugin] If you're using the [Authula SDK](https://github.com/Authula/authula-node-sdk), add the plugin to the SDK like so: ```typescript import { createClient } from "authula"; import { TOTPPlugin } from "authula/plugins"; export const authulaClient = createClient({ url: "http://localhost:8080/auth", plugins: [ // other plugins... new TOTPPlugin(), ], }); ``` # Config The Authula config is the main configuration of the framework. It is very flexible and comes with default configurations for you to customise. Authula is very flexible in that each plugin comes with its own config too providing you with more control over your authentication system. This also keeps the main config here clean and easy to maintain with less chances for bugs and issues as you are not forced to include plugin-specific settings if you don't need them. You can view the full config here: [Source Code](https://github.com/Authula/authula/blob/main/models/config.go) You can view the configs of each plugin within their respective folders in the [plugins directory](https://github.com/Authula/authula/tree/main/plugins). # Database Schema Authula comes with a few core tables that power authentication, account linking, session persistence, and verification workflows. These tables are created automatically by the migrator and are shared across the framework. ## How These Tables Fit Together [#how-these-tables-fit-together] - `users` stores the primary identity record for each authenticated person. - `accounts` stores login methods and external provider credentials linked to a user. - `sessions` stores active sessions for request authentication. - `verifications` stores short-lived workflow records such as email verification, password reset, magic link sign-in and more. In practice, a user may have one or more linked accounts, one or more sessions, and zero or more verification records depending on which Authula features are enabled. ## Database Schema [#database-schema] ### Table: `users` [#table-users] The `users` table is the root identity record in Authula. It stores profile data and metadata that can be shared across authentication methods. | Field | Type | Key | Description | | ---------------- | ---------- | --- | ------------------------------------- | | `id` | string | PK | Unique identifier for the user | | `name` | string | - | Display name for the user | | `email` | string | - | Primary email address | | `email_verified` | boolean | - | Whether the email address is verified | | `image` | string? | - | Optional profile image URL | | `metadata` | json/jsonb | - | Arbitrary user metadata | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | ### Table: `accounts` [#table-accounts] The `accounts` table stores authentication credentials and linked provider data for each user. It is used for both local credentials and external identity providers. | Field | Type | Key | Description | | -------------------------- | ---------- | --- | ------------------------------------- | | `id` | string | PK | Unique identifier for the account | | `user_id` | string | FK | User that owns this account | | `account_id` | string | - | Provider-specific account identifier | | `provider_id` | string | - | Authentication provider identifier | | `access_token` | string? | - | Optional provider access token | | `refresh_token` | string? | - | Optional provider refresh token | | `id_token` | string? | - | Optional OIDC ID token | | `access_token_expires_at` | timestamp? | - | When the access token expires | | `refresh_token_expires_at` | timestamp? | - | When the refresh token expires | | `scope` | string? | - | Granted OAuth scope | | `password` | string? | - | Password hash for email/password auth | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | `accounts.user_id` belongs to `users.id`. For external providers, `account_id` and `provider_id` identify the linked identity. For email/password auth, the `password` column stores the credential material used for password verification. ### Table: `sessions` [#table-sessions] The `sessions` table stores active authenticated sessions so Authula can validate requests and maintain login state across requests. | Field | Type | Key | Description | | ------------ | --------- | --- | ------------------------------------- | | `id` | string | PK | Unique identifier for the session | | `user_id` | string | FK | User that owns the session | | `token` | string | - | Session token used for authentication | | `expires_at` | timestamp | - | When the session expires | | `ip_address` | string? | - | Optional source IP address | | `user_agent` | string? | - | Optional user agent string | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | `sessions.user_id` belongs to `users.id`. Sessions are short-lived compared with users and are typically revoked or expired independently of the user record. ### Table: `verifications` [#table-verifications] The `verifications` table stores transient workflow records used for email and multi-step authentication flows. | Field | Type | Key | Description | | ------------ | ----------------- | --- | --------------------------------------------- | | `id` | string | PK | Unique identifier for the verification record | | `user_id` | string? | FK | Optional related user | | `identifier` | string | - | Email or other identifier | | `token` | string | - | Verification token | | `type` | verification_type | - | Verification workflow type | | `expires_at` | timestamp | - | When the verification expires | | `created_at` | timestamp | - | Record creation time | | `updated_at` | timestamp | - | Record last update time | `verifications.user_id` belongs to `users.id` when a verification is tied to an existing user. The `identifier` field usually stores an email address, and `type` indicates which workflow created the record. ### Verification Types [#verification-types] The `type` field in `verifications` stores one of the following values: | Type | Meaning | | ---------------------------- | ---------------------------------------------- | | `email_verification` | Confirms ownership of an email address | | `password_reset_request` | Tracks a password reset flow | | `email_reset_request` | Tracks a request to change the email address | | `magic_link_sign_in_request` | Starts a magic link sign-in flow | | `magic_link_exchange_code` | Stores the exchange step for a magic link flow | | `totp_pending_auth` | Tracks a pending TOTP authentication step | ## Relationship Summary [#relationship-summary] - `users` is the canonical identity table. - `accounts` connects a user to local or external sign-in methods. - `sessions` tracks authenticated browser or API sessions for a user. - `verifications` stores temporary records for email and multi-step auth flows. Together, these tables cover the core Authula lifecycle: create a user, attach one or more accounts, issue sessions after sign-in, and persist temporary verification state when a workflow requires it. # Email Templates Authula uses Go's `html/template` and `text/template` engines for all email rendering. Templates are embedded at build time via `go:embed`, with optional runtime overrides via environment variables. ## Architecture [#architecture] Every email type (verify email, password reset, magic link sign-in, etc.) has three template files under its plugin's `templates/` directory: | File | Engine | Purpose | | -------------- | --------------- | -------------------------------------------- | | `subject.tmpl` | `text/template` | Email subject line | | `text.tmpl` | `text/template` | Plain-text body | | `html.tmpl` | `html/template` | HTML body (auto-escaped, `missingkey=error`) | Templates are parsed eagerly during plugin `Init()`. If parsing fails, the plugin fails to start (fail-fast). Environment variable overrides are read once at startup and are immutable for the process lifetime. ### Available template functions [#available-template-functions] | Function | Signature | Description | | --------------- | ----------------------- | ---------------------------------------------------------------- | | `humanDuration` | `humanDuration .Expiry` | Formats a `time.Duration` as e.g. "1 hour", "24 hours", "2 days" | | `plural` | `plural "item" .Count` | Returns singular or plural form | | `upper` | `upper .String` | Uppercases the string | | `lower` | `lower .String` | Lowercases the string | | `title` | `title .String` | Title-cases the first character | | `currentYear` | `currentYear` | Returns the current year (e.g. 2026) | ### Context structs [#context-structs] Each template receives a context struct that embeds `CommonContext`: **CommonContext:** - `AppName` — the application name (from config) - `BaseURL` — the base URL of the API --- ## Email Types & Contexts [#email-types--contexts] ### 1. Email/Password Plugin [#1-emailpassword-plugin] #### Verify Email [#verify-email] | Field | Type | Description | | ------------------ | --------------- | -------------------------- | | `UserEmail` | `string` | Recipient's email | | `VerificationLink` | `string` | One-click verification URL | | `Expiry` | `time.Duration` | Link expiration duration | #### Password Reset [#password-reset] | Field | Type | Description | | ----------- | --------------- | ------------------------ | | `UserEmail` | `string` | Recipient's email | | `ResetLink` | `string` | Password reset URL | | `Expiry` | `time.Duration` | Link expiration duration | #### Password Changed [#password-changed] | Field | Type | Description | | ----------- | -------- | ----------------- | | `UserEmail` | `string` | Recipient's email | #### Email Change Request [#email-change-request] | Field | Type | Description | | ------------ | --------------- | ----------------------------- | | `UserEmail` | `string` | Current email | | `OldEmail` | `string` | Old email address | | `NewEmail` | `string` | New email address | | `ChangeLink` | `string` | Email change confirmation URL | | `Expiry` | `time.Duration` | Link expiration duration | #### Email Changed [#email-changed] | Field | Type | Description | | ----------- | -------- | ---------------------------------------- | | `UserEmail` | `string` | Recipient's email (varies per recipient) | | `OldEmail` | `string` | Old email address | | `NewEmail` | `string` | New email address | This template is rendered twice: once for the old address (`UserEmail` = old) and once for the new address (`UserEmail` = new). --- ### 2. Magic Link Plugin [#2-magic-link-plugin] #### Magic Link Sign-In [#magic-link-sign-in] | Field | Type | Description | | ----------- | --------------- | ------------------------ | | `UserEmail` | `string` | User's email | | `MagicLink` | `string` | One-click sign-in URL | | `Expiry` | `time.Duration` | Link expiration duration | --- ### 3. Organizations Plugin [#3-organizations-plugin] #### Organization Invitation [#organization-invitation] | Field | Type | Description | | ------------------ | --------------- | ------------------------------ | | `InvitationEmail` | `string` | Invitee's email | | `OrganizationName` | `string` | Organization name | | `Role` | `string` | Assigned role | | `AcceptLink` | `string` | Invitation acceptance URL | | `Expiry` | `time.Duration` | Invitation expiration duration | --- ## Overriding Templates via Environment Variables [#overriding-templates-via-environment-variables] Every email template can be overridden at startup using environment variables. The naming convention is: ``` AUTHULA_TEMPLATE__SUBJECT AUTHULA_TEMPLATE__TEXT AUTHULA_TEMPLATE__HTML ``` Where `` is the uppercase, underscore-separated email type name. ### Email/Password Plugin [#emailpassword-plugin] | Env Variable | Overrides | | ------------------------------------------------- | --------------------------------- | | `AUTHULA_TEMPLATE_VERIFY_EMAIL_SUBJECT` | Verify email subject | | `AUTHULA_TEMPLATE_VERIFY_EMAIL_TEXT` | Verify email plain text | | `AUTHULA_TEMPLATE_VERIFY_EMAIL_HTML` | Verify email HTML | | `AUTHULA_TEMPLATE_PASSWORD_RESET_REQUEST_SUBJECT` | Password reset request subject | | `AUTHULA_TEMPLATE_PASSWORD_RESET_REQUEST_TEXT` | Password reset request plain text | | `AUTHULA_TEMPLATE_PASSWORD_RESET_REQUEST_HTML` | Password reset request HTML | | `AUTHULA_TEMPLATE_PASSWORD_CHANGED_SUBJECT` | Password changed subject | | `AUTHULA_TEMPLATE_PASSWORD_CHANGED_TEXT` | Password changed plain text | | `AUTHULA_TEMPLATE_PASSWORD_CHANGED_HTML` | Password changed HTML | | `AUTHULA_TEMPLATE_EMAIL_CHANGE_REQUEST_SUBJECT` | Email change request subject | | `AUTHULA_TEMPLATE_EMAIL_CHANGE_REQUEST_TEXT` | Email change request plain text | | `AUTHULA_TEMPLATE_EMAIL_CHANGE_REQUEST_HTML` | Email change request HTML | | `AUTHULA_TEMPLATE_EMAIL_CHANGED_SUBJECT` | Email changed subject | | `AUTHULA_TEMPLATE_EMAIL_CHANGED_TEXT` | Email changed plain text | | `AUTHULA_TEMPLATE_EMAIL_CHANGED_HTML` | Email changed HTML | ### Magic Link Plugin [#magic-link-plugin] | Env Variable | Overrides | | --------------------------------------------- | --------------------- | | `AUTHULA_TEMPLATE_MAGIC_LINK_SIGN_IN_SUBJECT` | Magic link subject | | `AUTHULA_TEMPLATE_MAGIC_LINK_SIGN_IN_TEXT` | Magic link plain text | | `AUTHULA_TEMPLATE_MAGIC_LINK_SIGN_IN_HTML` | Magic link HTML | ### Organizations Plugin [#organizations-plugin] | Env Variable | Overrides | | -------------------------------------------------- | ------------------------- | | `AUTHULA_TEMPLATE_ORGANIZATION_INVITATION_SUBJECT` | Org invitation subject | | `AUTHULA_TEMPLATE_ORGANIZATION_INVITATION_TEXT` | Org invitation plain text | | `AUTHULA_TEMPLATE_ORGANIZATION_INVITATION_HTML` | Org invitation HTML | ### Example: Override the verify email HTML template [#example-override-the-verify-email-html-template] ```bash # .env AUTHULA_TEMPLATE_VERIFY_EMAIL_HTML="

Hi {{.UserEmail}},

Click here to verify your email.

This link expires in {{humanDuration .Expiry}}.

" ``` ## Viewing Default Templates [#viewing-default-templates] The built-in templates are embedded in each plugin's `templates/` directory: ``` plugins/email-password/templates/* plugins/magic-link/templates/* plugins/organizations/templates/* ``` ## Template Syntax [#template-syntax] Templates use Go's standard template syntax (`{{.FieldName}}`). The HTML templates automatically escape HTML characters (e.g., `<` becomes `<`), so you never need to manually escape values in the HTML body. ### subject.tmpl example [#subjecttmpl-example] ``` Verify your email ``` ### text.tmpl example [#texttmpl-example] ``` Verify your email by clicking the following link: {{.VerificationLink}}. ``` ### html.tmpl example [#htmltmpl-example] ```html

Hello {{.UserEmail}},

Please verify your email address by clicking the following link: Verify your email

This link will expire in {{humanDuration .Expiry}}.

If you did not request this, please ignore this email.

``` # Playground [Authula Playground](https://github.com/Authula/authula-playground) is a repository that showcases a full-stack authentication system. It is designed as a playground to explore secure authentication flows, including user registration, login, and protected routes powered by **Authula**. ## Getting Started [#getting-started] ### Clone the Repository [#clone-the-repository] ```bash $ git clone https://github.com/Authula/authula-playground.git $ cd authula-playground ``` Then follow the instructions in the README of the repository to set up and get started with the playground. # SDKs ### Node.js SDK [#nodejs-sdk] The Node.js TypeScript SDK for Authula provides a seamless way to interact with the Authula server. With support for client-side plugins, it gives you powerful tools to enhance your authentication workflows and interact with your Authula server in a flexible and extensible manner. You can view the SDK here: [Node.js SDK](https://www.npmjs.com/package/authula) ---