rustauth-plugins
Official server-side plugin modules for RustAuth.
What It Is
rustauth-plugins groups Better Auth-inspired server features translated into
RustAuth's Rust plugin contracts. Use it when you want optional auth behavior
without pulling each feature into rustauth-core.
The deprecated upstream oidc-provider and MCP authorization-server plugins are
not implemented here. Use rustauth-oauth-provider for OAuth 2.1, OpenID
Connect provider behavior, and MCP protected-resource metadata.
What It Provides
Current modules include access control, additional fields, admin, anonymous users, API keys, bearer sessions, CAPTCHA hooks, custom sessions, device authorization, email OTP, generic OAuth, Have I Been Pwned checks, JWT, last login method, magic links, multi-session, OAuth proxy, one-tap, one-time tokens, OpenAPI, organizations, phone number, SIWE, two-factor, and username.
Some plugins are pure helpers. Many require an RustAuth adapter because they store users, sessions, keys, organizations, tokens, or verification state.
Quick Start
use RustAuth;
use *;
let auth = builder
.secret
.plugin
.plugin
.build?;
# let _ = auth;
# Ok::
Import factories from prelude when wiring several plugins.
Each plugin exposes one factory: plugin_name(options) (or plugin_name(options)?
when validation can fail). Dev-only presets such as magic_link_dev_log() and
siwe_dev() remain for local development.
Register plugins on RustAuth::builder():
.plugin(x)— append one plugin (chain as needed)..plugins(vec![...])— append a batch (same as chaining.plugin).
When building RustAuthOptions directly,
.plugin(x) and .plugins(vec![...]) both append. Use .set_plugins(vec![...])
to replace the full list.
use RustAuth;
use *;
let core = vec!;
let auth = builder
.secret
.plugins
.plugin
.build?;
# let _ = auth;
# Ok::
email_otp and phone_number resolve the database adapter from the auth
context at runtime; pass the adapter only to RustAuth::builder().adapter(...).
Use module-specific options when a plugin needs application callbacks such as email sending, OTP delivery, CAPTCHA verification, SIWE verification, or custom authorization policy.
Plugin factory conventions
foo(options)— single factory per plugin; always pass an options value (FooOptions::default()when defaults are valid).- Fallible factories — return
Result<AuthPlugin, RustAuthError>when options validation can fail (admin,api_key,captcha,email_otp,jwt, …). - Dev presets —
magic_link_dev_log(),siwe_dev(),siwe_dev_domain()for local development only (not general zero-arg factories).
Configuring options
All three styles are supported and equivalent:
| Style | When to use | Example |
|---|---|---|
| Struct literal | Full control, small configs | email_otp(EmailOtpOptions { sender: Some(s), ..Default::default() })? |
| Builder | Incremental or optional fields | email_otp(EmailOtpOptions::builder().sender(s).build()?)? |
Options::new(...) |
Required callbacks only | email_otp(EmailOtpOptions::new(sender))? |
Callback-required plugins (email_otp, phone_number, magic_link, captcha, siwe)
also offer FooOptions::new(...) or provider helpers such as
CaptchaOptions::cloudflare_turnstile(secret).
Email, SMS, and magic-link senders are async — return OutboundSendFuture
(Box::pin(async move { ... })). RustAuth dispatches delivery in the background;
do not block handlers on SMTP/SMS. See
docs/security-outbound-delivery.md.
new
Use RustAuth::builder() for the auth instance. In rustauth-core, prefer
TypeName::new() for core option structs (not Options::builder() on core types).
Plugin factory table
| Plugin | Factory | Dev preset |
|---|---|---|
| Additional fields | additional_fields(AdditionalFieldsOptions) |
— |
| Admin | admin(AdminOptions) → Result |
— |
| Anonymous | anonymous(AnonymousOptions) |
— |
| API key | api_key(ApiKeyOptions) → Result |
— |
| Bearer | bearer(BearerOptions) |
— |
| Captcha | captcha(CaptchaOptions) → Result |
— |
| Custom session | custom_session(CustomSessionOptions, handler) |
— |
| Device authorization | device_authorization(DeviceAuthorizationOptions) → Result |
— |
| Email OTP | email_otp(EmailOtpOptions) → Result |
— |
| Generic OAuth | generic_oauth(GenericOAuthOptions) |
presets in generic_oauth/providers/ |
| Have I Been Pwned | have_i_been_pwned(HaveIBeenPwnedOptions) (disabled by default) |
— |
| JWT | jwt(JwtOptions) → Result |
— |
| Last login method | last_login_method(LastLoginMethodOptions) |
— |
| Magic link | magic_link(MagicLinkOptions) |
magic_link_dev_log() |
| Multi-session | multi_session(MultiSessionOptions) |
— |
| OAuth proxy | oauth_proxy(OAuthProxyOptions) |
— |
| One Tap | one_tap(OneTapOptions) |
— |
| One-time token | one_time_token(OneTimeTokenOptions) |
— |
| OpenAPI | open_api(OpenApiOptions) |
— |
| Organization | organization(OrganizationOptions) |
— |
| Phone number | phone_number(PhoneNumberOptions) → Result |
— |
| SIWE | siwe(SiweOptions) → Result |
siwe_dev() → Result |
| Two-factor | two_factor(TwoFactorOptions) |
— |
| Username | username(UsernameOptions) |
— |
Enterprise crates (re-exported from rustauth::prelude behind features) follow the
same contract: passkey(PasskeyOptions), sso(SsoOptions), scim(ScimOptions),
stripe(StripeOptions)?, and oauth_provider(OAuthProviderOptions)?. Register
jwt(JwtOptions)? before oauth_provider(...) when JWT/OIDC signing is required;
use OAuthProviderOptions::with_external_jwt() (or disable_jwt_plugin: true) to
run without the jwt plugin.
Time units
Public plugin and core option timeouts use time::Duration.
| Location | Field | Type |
|---|---|---|
SessionOptions |
expires_in, update_age, fresh_age |
Option<Duration> |
CookieCacheOptions / CookieAttributesOverride |
max_age |
Option<Duration> |
RateLimitOptions / RateLimitRule |
window |
Duration |
EmailVerificationOptions |
expires_in |
Option<Duration> |
PasswordOptions |
reset_password_token_expires_in |
Option<Duration> |
DeleteUserOptions |
delete_token_expires_in |
Option<Duration> |
OneTimeTokenOptions |
expires_in |
Duration |
ApiKeyRateLimitOptions |
time_window |
Duration (JSON ms via serde) |
ApiKeyExpirationOptions |
default_expires_in |
Option<Duration> (JSON seconds via serde) |
MagicLinkOptions / MagicLinkRateLimit |
expires_in, window |
Duration |
EmailOtpOptions |
expires_in |
Duration |
PhoneNumberOptions |
expires_in |
Duration |
OrganizationOptions |
invitation_expires_in |
Duration |
TwoFactorOptions |
two_factor_cookie_max_age, trust_device_max_age |
Duration |
TotpOptions / OtpOptions |
period |
Duration |
LastLoginMethodOptions |
max_age |
Option<Duration> |
AdminOptions |
default_ban_expires_in, impersonation_session_duration |
Option<Duration> / Duration |
OAuthProxyOptions |
max_age |
Duration (JSON maxAge seconds via serde) |
PasskeyRateLimit / PasskeyChallengeRateLimit |
window |
Duration |
DeviceAuthorizationOptions |
expires_in, interval |
Duration |
HTTP JSON responses such as OAuth expires_in remain RFC-defined seconds and
are not converted to Duration.
Time units
Public plugin and core option timeouts use time::Duration.
| Location | Field | Type |
|---|---|---|
SessionOptions |
expires_in, update_age, fresh_age |
Option<Duration> |
CookieCacheOptions / CookieAttributesOverride |
max_age |
Option<Duration> |
RateLimitOptions / RateLimitRule |
window |
Duration |
EmailVerificationOptions |
expires_in |
Option<Duration> |
PasswordOptions |
reset_password_token_expires_in |
Option<Duration> |
DeleteUserOptions |
delete_token_expires_in |
Option<Duration> |
OneTimeTokenOptions |
expires_in |
Duration |
ApiKeyRateLimitOptions |
time_window |
Duration (JSON ms via serde) |
ApiKeyExpirationOptions |
default_expires_in |
Option<Duration> (JSON seconds via serde) |
MagicLinkOptions / MagicLinkRateLimit |
expires_in, window |
Duration |
EmailOtpOptions |
expires_in |
Duration |
PhoneNumberOptions |
expires_in |
Duration |
OrganizationOptions |
invitation_expires_in |
Duration |
TwoFactorOptions |
two_factor_cookie_max_age, trust_device_max_age |
Duration |
TotpOptions / OtpOptions |
period |
Duration |
LastLoginMethodOptions |
max_age |
Option<Duration> |
AdminOptions |
default_ban_expires_in, impersonation_session_duration |
Option<Duration> / Duration |
OAuthProxyOptions |
max_age |
Duration (JSON maxAge seconds via serde) |
PasskeyRateLimit / PasskeyChallengeRateLimit |
window |
Duration |
DeviceAuthorizationOptions |
expires_in, interval |
Duration |
HTTP JSON responses such as OAuth expires_in remain RFC-defined seconds and
are not converted to Duration.
Naming conventions
All plugin HTTP JSON request and response bodies use camelCase (userId,
emailVerified, callbackURL) for Better Auth parity (0.2.0+). Protocol
tables keep RFC field names unchanged: device authorization (/device/*),
OAuth provider (/oauth2/*, .well-known/*), and SCIM v2 (/scim/v2/*).
- Database logical names (adapter queries, schema metadata):
snake_case(device_code,wallet_address,two_factor). - HTTP JSON (request/response bodies, OpenAPI):
camelCase(userId,walletAddress) for Better Auth parity. - OAuth protocol endpoints (device authorization, token grants): RFC-defined
snake_case(device_code,expires_in) — not converted to camelCase.
Plugin options metadata JSON keeps camelCase keys (for example
schema.walletAddress on SIWE).
Operational Notes
- Run adapter migrations after adding plugins that contribute schema.
- Prefer these plugins for server behavior; helper SDKs should stay outside this crate.
- API key storage can use the database and selected secondary-storage paths.
- In pure
SecondaryStoragemode (no database fallback) theapi-key:by-ref:*listing index is mutated through atomiccompare_and_set/delete_if_value. Multi-process deployments need a secondary-storage backend that implements those methods with real backend atomicity, or the database fallback, to keep/api-key/listfrom dropping concurrently written keys. - OpenAPI support serves generated auth schemas and optional Scalar reference UI.
Status
Experimental beta. Individual plugin APIs, schemas, endpoints, hooks, and error codes may change before stable release.
Better Auth compatibility
Server-side official plugin behavior is aligned with Better Auth 1.6.9 where it matters; RustAuth is not a line-by-line port. For route-level parity, test counts, differences, and gaps, see UPSTREAM.md.