Sign-in Methods
Email/password, magic-link (passwordless), and password-reset flows are built in — see the API Endpoints reference. The sections below cover the additional sign-in methods.
OAuth2 & OIDC Providers
ezauth ships presets for the most common providers — Google, GitHub, Facebook, Discord, GitLab, Slack, LinkedIn, and Spotify. Each is enabled by setting its EZAUTH_OAUTH2_<NAME>_* variables (see Configuration > OAuth2 Settings); the routes live under /auth/oauth2/{provider}/login and /auth/oauth2/{provider}/callback (see API Endpoints). You can also register arbitrary custom or OIDC providers, as shown below.
[!IMPORTANT] OAuth2 auto-linking requires the provider to return
email_verified: truein the user info response. If a provider does not return this field (or returnsfalse), the user will be prompted to log in with their existing password rather than being automatically linked. This prevents account takeover via unverified email addresses.
Custom / OIDC Providers
You can register custom providers dynamically via environment variables (Standalone-service mode) or in Go code (Library mode).
Standalone-service mode (via Env Vars)
- Add your provider's name to
EZAUTH_OAUTH2_PROVIDERS(comma-separated). - Configure prefix variables for each provider (
EZAUTH_OAUTH2_<NAME>_): CLIENT_ID,CLIENT_SECRET,REDIRECT_URL(required)SCOPES(optional, comma-separated)- Either
ISSUER_URL(for automatic OIDC discovery) or manual endpoint parameters (AUTH_URL,TOKEN_URL,USERINFO_URL,ID_FIELD(defaultid),EMAIL_FIELD(defaultemail)).
Library Mode (Go Code)
Register providers programmatically with the RegisterOAuth2Provider API. We ship pre-made presets (Discord, Slack, GitLab) and a generic OIDC discovery helper in the optional github.com/josuebrunel/ezauth/pkg/service/providers package:
import (
"github.com/josuebrunel/ezauth/pkg/service/providers"
)
// 1. OIDC Discovery
oktaProvider, err := providers.OIDC(ctx, "https://your-domain.okta.com", "client-id", "client-secret", "http://localhost:8080/auth/oauth2/okta/callback", []string{"openid", "profile", "email"})
if err == nil {
auth.RegisterOAuth2Provider("okta", oktaProvider)
}
// 2. Out-of-the-box Preset
discordProvider := providers.Discord("client-id", "client-secret", "http://localhost:8080/auth/oauth2/discord/callback")
auth.RegisterOAuth2Provider("discord", discordProvider)
SMS OTP
ezauth supports SMS-based one-time-password login, mirroring the passwordless (magic link) flow but via a 6-digit SMS code. An unrecognized phone number gets a temporary, unverified account, same as an unrecognized email does for passwordless. Requires EZAUTH_SMS_TWILIO_ACCOUNT_SID/_AUTH_TOKEN/_FROM; falls back to a mock sender otherwise. Phone numbers are enforced unique at the database level.
err := auth.Service.SMSOTPRequest(ctx, service.RequestSMSOTP{Phone: "+15551234567"})
tokens, err := auth.Service.SMSOTPVerify(ctx, service.RequestSMSOTPVerify{
Phone: "+15551234567",
Code: "123456",
})
EZAUTH_SMS_OTP_BODY customizes the SMS message template ({{.Code}}, {{.Phone}} available).
[!NOTE] Verifying a code flips the account's contact flag: a successful
SMSOTPVerifymarks the phone verified (PhoneVerified), and a successful first magic-link (passwordless) login marks the email verified (EmailVerified/EmailVerifiedAt) the same way. So the temporary, unverified account created on first contact becomes verified on first successful login.[!NOTE]
SMSOTPRequestandPasswordlessRequestrequire no authentication and each call sends a real, billable SMS or email — so both enforce a 60-second per-phone/per-address resend cooldown (ErrResendTooSoon) independent of the per-IP rate limiters (EZAUTH_RATE_LIMIT_ENABLEDsite-wide,EZAUTH_RATE_LIMIT_SENSITIVE_ENABLEDon these specific endpoints — see Configuration), since a caller spreading requests across many source IPs could otherwise spam or SMS-bomb a single target regardless of any per-IP throttle.Because either flow creates an account on first contact with no verification step required to request a code/link, an address/phone that's spammed but never completes a login accumulates as an unverified, otherwise-inert row (
EmailVerified/PhoneVerifiedboth false). Neither flow deletes these automatically. If you expect meaningful volumes of unsolicited requests, periodically clean them up yourself: page throughUsersListfiltered on an oldCreatedAfter/CreatedBeforewindow, skip any row whereEmailVerified/PhoneVerifiedis true, andUserDeletethe rest -- there's no default retention window baked intoezauth, since what counts as "never completed" is deployment-specific.
WebAuthn / Passkeys
ezauth supports WebAuthn/FIDO2 passkey registration and login. Login is discoverable (usernameless) — the browser's platform UI lets the user pick a passkey, so no prior email/username is required. WebAuthn is disabled unless EZAUTH_WEBAUTHN_RP_ID and EZAUTH_WEBAUTHN_RP_ORIGINS are both set, and ceremonies always require client-side JavaScript (navigator.credentials.create()/.get()) regardless of cookie vs. Bearer auth style.
// Registration (user already authenticated):
creation, sessionKey, err := auth.WebauthnBeginRegistration(ctx, user)
// Send creation as JSON for the browser to call navigator.credentials.create(),
// keep sessionKey for the finish step.
// r's body must be the browser's raw navigator.credentials.create() response.
cred, err := auth.WebauthnFinishRegistration(ctx, user, sessionKey, r, "YubiKey 5")
// Login:
assertion, sessionKey, err := auth.WebauthnBeginLogin(ctx)
// Send assertion as JSON for the browser to call navigator.credentials.get().
// r's body must be the browser's raw navigator.credentials.get() response.
user, tokens, err := auth.WebauthnFinishLogin(ctx, sessionKey, r)
// Managing credentials:
creds, err := auth.WebauthnCredentials(ctx, user.ID)
err = auth.WebauthnDeleteCredential(ctx, user, credentialRecordID)