Configuration scenarios
Copy-paste goauth.Config examples for every common deployment. Each scenario lists required fields, provider setup, and what happens at runtime.
Scenario matrix
| # | Scenario | Adapter | Strategy | Providers | Tokens | MFA |
|---|---|---|---|---|---|---|
| 1 | OAuth only (demo) | No | JWT | GitHub | No | No |
| 2 | OAuth + DB sessions | Yes | Database | GitHub | No | No |
| 3 | Credentials + JWT | No | JWT | Credentials | No | No |
| 4 | Credentials + MFA | Yes* | JWT | Credentials | Optional | Yes |
| 5 | Email magic link | Yes | Database | No | No | |
| 6 | Email OTP code | Yes | Database | OTP | Optional | No |
| 7 | Mobile API (tokens) | Yes | Database | OAuth + Credentials | Yes | Optional |
| 8 | Passkeys | Yes | Database | Passkey | Optional | No |
| 9 | Full enterprise | Yes | Database | All | Yes | Yes |
*Adapter recommended for MFA OTP storage; see MFA guide.
1. OAuth only (simplest)
Stateless GitHub login. No database.
auth, err := goauth.New(goauth.Config{
Secret: []string{os.Getenv("AUTH_SECRET")},
TrustHost: true,
Providers: []goauth.Provider{
github.New(clientID, clientSecret),
},
})
Runtime: JWT cookie after OAuth. User id comes from GitHub profile mapping.
2. OAuth + database sessions
Persist users and sessions in PostgreSQL.
adapter := postgres.New(db)
auth, err := goauth.New(goauth.Config{
Secret: []string{secret},
URL: "https://app.example.com",
Adapter: adapter,
Session: goauth.SessionConfig{
MaxAge: 14 * 24 * time.Hour,
},
Providers: []goauth.Provider{
github.New(clientID, clientSecret),
google.New(clientID, clientSecret),
},
Callbacks: goauth.Callbacks{
SignIn: func(ctx context.Context, p goauth.SignInCallbackParams) (bool, error) {
return strings.HasSuffix(p.User.Email, "@company.com"), nil
},
},
})
Runtime: resolveUser creates/links user in goauth_users, session row in goauth_sessions.
3. Credentials only (JWT)
Username/password API or form; no adapter required.
auth, err := goauth.New(goauth.Config{
Secret: []string{secret},
URL: "https://api.example.com",
Providers: []goauth.Provider{
credentials.New(credentials.Options{
Authorize: func(ctx context.Context, creds map[string]string, r *http.Request) (*goauth.User, error) {
u, ok := myApp.ValidatePassword(creds["email"], creds["password"])
if !ok {
return nil, nil
}
return &goauth.User{ID: u.ID, Email: u.Email, Name: u.Name}, nil
},
}),
},
})
Sign-in: POST /auth/callback/credentials with email and password.
4. Credentials + MFA
Password first, then OTP. You implement SendCode.
adapter := postgres.New(db)
auth, err := goauth.New(goauth.Config{
Secret: []string{secret},
URL: "https://app.example.com",
Adapter: adapter,
Providers: []goauth.Provider{
credentials.New(credentials.Options{Authorize: authorize}),
},
MFA: goauth.MFAConfig{
Enabled: true,
CodeLength: 6,
MaxAge: 10 * time.Minute,
TrustDeviceMaxAge: 90 * 24 * time.Hour,
SendCode: func(ctx context.Context, p goauth.MFASendCodeParams) error {
return mailer.SendOTP(p.Email, p.Code)
},
},
})
Client flow:
# Step 1 — password
POST /auth/callback/credentials
→ { "challenge": "eyJ...", "expiresIn": 600 }
# Step 2 — OTP (your SendCode already delivered the code)
POST /auth/mfa/verify
Body: challenge=...&code=123456&trustDevice=true
→ session cookie or tokens
Full detail: OTP & verification.
5. Email magic link
Passwordless sign-in. You implement SendVerificationRequest.
adapter := postgres.New(db)
auth, err := goauth.New(goauth.Config{
Secret: []string{secret},
URL: "https://app.example.com",
Adapter: adapter,
Pages: goauth.Pages{
VerifyRequest: "/check-your-email",
},
Providers: []goauth.Provider{
email.New(email.Options{
MaxAge: int((24 * time.Hour).Seconds()),
SendVerificationRequest: func(ctx context.Context, p goauth.SendVerificationRequestParams) error {
return mailer.Send(p.Identifier, "Sign in", "Click: "+p.URL)
},
}),
},
})
Flow:
POST /auth/signin/email
Form: email=user@example.com
→ redirect to VerifyRequest page
GET /auth/callback/email?email=...&token=...
→ session created (user auto-created if new)
6. Email OTP provider (sign-in code)
Uses same EmailProvider internally but sends a numeric code, not a link.
adapter := postgres.New(db)
auth, err := goauth.New(goauth.Config{
Secret: []string{secret},
URL: "https://app.example.com",
Adapter: adapter,
Providers: []goauth.Provider{
otp.New(otp.Options{
CodeLength: 6,
MaxAge: 600,
SendCode: func(ctx context.Context, p goauth.SendVerificationRequestParams) error {
// p.Token is the 6-digit code
// p.Identifier is the email
return smsOrEmail.Send(p.Identifier, "Code: "+p.Token)
},
}),
},
Tokens: goauth.TokensConfig{Enabled: true}, // optional for mobile
})
Flow:
# Request code
POST /auth/signin/otp
Form: email=user@example.com
# Verify code (NOT /mfa/verify — this is the otp provider callback)
POST /auth/callback/otp
Form: email=user@example.com&code=123456
:::danger Do not confuse with MFA
POST /auth/mfa/verify is only for MFA after credentials password. Email OTP uses POST /auth/callback/otp.
:::
7. Mobile / SPA bearer tokens
auth, err := goauth.New(goauth.Config{
Secret: []string{secret},
URL: "https://api.example.com",
Adapter: postgres.New(db),
Providers: []goauth.Provider{
credentials.New(credentials.Options{Authorize: authorize}),
},
Tokens: goauth.TokensConfig{
Enabled: true,
AccessTokenMaxAge: 15 * time.Minute,
RefreshTokenMaxAge: 30 * 24 * time.Hour,
CallbackPage: "https://app.example.com/auth/done",
AlwaysReturn: false, // opt-in per request
},
})
Mobile sign-in:
POST /auth/callback/credentials
X-Auth-Flow: token
Content-Type: application/x-www-form-urlencoded
email=user@example.com&password=secret
Refresh:
POST /auth/token
Content-Type: application/x-www-form-urlencoded
refresh_token=eyJ...
8. Passkeys
adapter := postgres.New(db)
auth, err := goauth.New(goauth.Config{
Secret: []string{secret},
URL: "https://app.example.com",
Adapter: adapter,
Providers: []goauth.Provider{
passkey.New(passkey.Options{
RelyingPartyID: "app.example.com",
RelyingPartyName: "My App",
}),
},
})
Requires AuthenticatorStore on adapter. See Passkey provider.
9. Full enterprise stack
Everything together: OAuth, credentials + MFA, OTP email, passkey, tokens, callbacks.
adapter := postgres.New(db)
auth, err := goauth.New(goauth.Config{
Secret: []string{secret},
URL: "https://app.example.com",
Adapter: adapter,
Session: goauth.SessionConfig{MaxAge: 30 * 24 * time.Hour},
Tokens: goauth.TokensConfig{
Enabled: true,
CallbackPage: "https://app.example.com/dashboard",
},
MFA: goauth.MFAConfig{
Enabled: true,
SendCode: sendMFACode,
},
Pages: goauth.Pages{
SignIn: "/login",
Error: "/login?error=1",
},
Cookies: goauth.CrossSubdomainCookies(".example.com"),
Providers: []goauth.Provider{
azuread.New(azuread.Options{ /* ... */ }),
credentials.New(credentials.Options{Authorize: authorize}),
otp.New(otp.Options{SendCode: sendLoginOTP}),
passkey.New(passkey.Options{RelyingPartyID: "app.example.com"}),
},
Callbacks: goauth.Callbacks{
SignIn: allowSignIn,
ResolveUser: upsertUserInAppDB,
JWT: addRolesToToken,
Session: stripSensitiveFields,
Redirect: safeRedirect,
},
Events: goauth.Events{
SignIn: auditSignIn,
CreateUser: auditNewUser,
SignOut: auditSignOut,
},
Debug: os.Getenv("AUTH_DEBUG") == "1",
})
Walk through each callback: Developer cookbook.
Secret rotation scenario
Secret: []string{
os.Getenv("AUTH_SECRET_2026_06"), // new
os.Getenv("AUTH_SECRET_2026_01"), // still valid for old cookies
},
New sessions use the first secret; decodeJWT tries all entries.
Cross-subdomain SSO scenario
Cookies: goauth.CrossSubdomainCookies(".example.com"),
URL: "https://www.example.com",
App on www.example.com and admin.example.com share session cookies.
Custom JWT encoder scenario
JWT: goauth.JWTConfig{
Encode: myEncode,
Decode: myDecode,
},
Rare — only when you must plug a corporate KMS. Default goauth/jwt is Auth.js wire-compatible.