Authentication
Modern, flexible authentication system with passwordless and traditional password flows
Authentication
goilerplate includes a complete, production-ready authentication system that gives your users flexible sign-in options with strong default security.
Overview
The modern auth stack that sells:
- ✅ Passwordless Magic Links - Modern, secure, email-based authentication
- ✅ Traditional Password Auth - Optional password login for users who prefer it
- ✅ Full Password Management - Set, change, or remove passwords anytime
- ✅ Secure by Default - 12+ character passwords and common pattern detection based on NIST recommendations
- ✅ Email Verification - Built-in email verification flow
- ✅ Password Reset - Secure token-based password recovery
- ✅ Email Change - Verified email change with confirmation
- ✅ Progressive Onboarding - Streamlined user experience
- ✅ OAuth Ready - Google and GitHub placeholders (coming soon)
- ✅ No JavaScript Required - Progressive enhancement for core auth flows
User Flows
Magic Link Authentication (Passwordless)
The primary, recommended authentication method. Zero passwords to remember, maximum security.
How it works:
- User enters email on
/auth - Receives magic link via email
- Clicks link → automatically logged in
- New users complete onboarding (name input)
- Free subscription created automatically
Benefits:
- No password to remember or leak
- Immune to password-based attacks
- Modern UX (Slack, Notion, Linear use this)
- Faster signup flow
Password Authentication (Optional)
Traditional email + password login for users who prefer it.
How it works:
- User clicks “Sign in with password instead” on
/auth - Navigates to
/auth/password - Enters email + password → logged in
Password Requirements:
- Minimum 12 characters (NIST recommendation)
- No special character requirements (encourages passphrases)
- Automatic detection of common/weak passwords
- Validates against patterns: “password”, “123456”, “qwerty”, etc.
Flexible Password Management
Users can switch between passwordless and password-based auth anytime.
Set Password (Passwordless → Password)
- User starts with magic link account
- Goes to Settings → Security → Password
- Sets a password (no current password required)
- Now can use both magic links AND password to login
Remove Password (Password → Passwordless)
- User has password-based account
- Goes to Settings → Security → Password
- Clicks “Remove Password” with confirmation
- Returns to passwordless (magic links only)
Change Password
- Standard password change flow
- Requires current password verification
- Same strong password requirements
Technical Implementation
Password Validation
Located in internal/validation/password.go:
func ValidatePassword(password string) error {
// Minimum 12 characters
if len(password) < 12 {
return errors.New("password must be at least 12 characters")
}
// Check for common/weak patterns
lower := strings.ToLower(password)
commonPatterns := []string{
"password", "123456", "qwerty", "admin", "letmein",
"welcome", "monkey", "dragon", "master", "sunshine",
}
for _, pattern := range commonPatterns {
if strings.Contains(lower, pattern) {
return errors.New("password is too common, please choose a stronger one")
}
}
return nil
}
Note: All input validators (email, password, name, files) are now centralized in the internal/validation package for reusability across services and handlers.
Why this approach:
- NIST recommends 12-15 character minimum
- No “must have uppercase + number + symbol” rules (proven to create weaker passwords)
- Encourages passphrases: “my cat loves pizza 2024” = secure + memorable
- Simple implementation, no external dependencies
This means the password rules are based on NIST guidance for length-first password policies. It does not mean the product is claiming general NIST compliance.
Magic Link Flow
Magic links are secure, time-limited tokens sent via email.
Token Generation:
- 32-byte random token (hex-encoded = 64 characters)
- Stored hashed in database
- Configurable expiry (default: 15 minutes)
- Single-use (marked as used after verification)
Security Features:
- Tokens expire automatically
- One-time use only
- Email enumeration protection (always shows success message)
- Rate limiting ready
Session Management
Uses JWT (JSON Web Tokens) stored in HttpOnly cookies.
JWT Claims:
{
"user_id": "uuid",
"email": "[email protected]",
"exp": "expiry_timestamp",
"iat": "issued_at_timestamp"
}
Cookie Settings:
- HttpOnly: true (prevents XSS)
- Secure: true (production only, HTTPS required)
- SameSite: Lax (CSRF protection)
- Default expiry: 7 days
Middleware
Three middleware functions handle auth requirements:
AuthMiddleware
Runs on every request. Checks JWT, loads user/profile/subscription into context.
// internal/middleware/auth.go
func AuthMiddleware(...)
RequireAuth
Protects routes that need authentication. Redirects to /auth if not logged in.
mux.HandleFunc("GET /app/dashboard", middleware.RequireAuth(dashboard.Page))
Handles:
- Unauthenticated users → redirect to
/auth - Incomplete onboarding → redirect to
/onboarding - HTMX requests →
HX-Redirectheader
RequireGuest
Prevents authenticated users from accessing auth pages.
mux.HandleFunc("GET /auth", middleware.RequireGuest(auth.AuthPage))
Routes
Public Auth Routes
// Primary auth methods
GET /auth → Magic link page
POST /auth/magic-link → Send magic link
GET /auth/magic-link/{token} → Verify magic link
GET /auth/password → Password login page
POST /auth/password → Password authentication
// Email verification
GET /verify/{token} → Verify email (new accounts)
// Password reset
GET /forgot-password → Forgot password page
POST /forgot-password → Request password reset
GET /reset-password/{token} → Reset password page
POST /reset-password → Set new password
// Onboarding
GET /onboarding → Onboarding page (requires auth)
POST /onboarding → Complete onboarding
// Logout
POST /logout → Clear session
Protected Account Routes (Settings)
// Email management
PATCH /app/account/email → Request email change
GET /auth/verify-email-change/{token} → Verify new email
// Password management
POST /app/account/password → Change password
POST /app/account/password/set → Set password (passwordless → password)
DELETE /app/account/password → Remove password (password → passwordless)
// Account deletion
DELETE /app/account → Delete account
Services
AuthService
internal/service/auth.go
Key Methods:
SendMagicLink(email)- Creates user if needed, sends magic linkVerifyMagicLink(token)- Validates token, returns userLogin(email, password)- Traditional password loginSetPassword(userID, password)- Adds password to passwordless accountRemovePassword(userID)- Removes password from accountValidatePassword(password)- Validates password strengthSendForgotPasswordLink(email)- Sends magic link that removes password and logs user inGenerateJWT(user)- Creates JWT tokenVerifyJWT(token)- Validates JWT token
UserService
internal/service/user.go
Key Methods:
UpdatePassword(userID, current, new)- Change password (requires current password)DeleteAccount(userID)- Account deletion with safety checks
Email Templates
Authentication emails are sent via Resend using HTML templates:
- Magic Link - Clean, simple email with secure link
- Email Verification - Welcome email with verification link
- Password Reset - Secure reset link with expiry notice
- Email Change - Confirmation for new email address
Located in internal/service/email.go
Security Best Practices
Password Storage
- Bcrypt hashing with default cost (10)
- Passwords stored as nullable field (supports passwordless)
- Never logged or exposed in responses
Token Security
- Cryptographically secure random generation
- Time-limited expiry
- Single-use enforcement
- Hashed storage in database
Protection Against
- ✅ Brute Force - Rate limiting ready
- ✅ Email Enumeration - Always returns success messages
- ✅ Password Spray - Minimum 12 character passwords
- ✅ Common Passwords - Pattern detection
- ✅ Session Hijacking - HttpOnly + Secure cookies
- ✅ CSRF - SameSite cookie policy
- ✅ XSS - HttpOnly cookies, CSP headers
Customization
Adjust Token Expiry
In .env:
TOKEN_MAGIC_LINK_EXPIRY=15m # Magic link validity
TOKEN_EMAIL_VERIFY_EXPIRY=24h # Email verification
TOKEN_PASSWORD_RESET_EXPIRY=1h # Password reset
TOKEN_EMAIL_CHANGE_EXPIRY=24h # Email change
JWT_EXPIRY=168h # Session duration (7 days)
Modify Password Requirements
Edit internal/validation/password.go:
func ValidatePassword(password string) error {
// Change minimum length
if len(password) < 16 { // Increase to 16
return errors.New("password must be at least 16 characters")
}
// Add more common patterns
commonPatterns := append(commonPatterns, "mycompany", "changeme")
// Add custom validation
if !hasNumber(password) {
return errors.New("password must contain a number")
}
return nil
}
OAuth Setup (Google & GitHub)
OAuth integration is built into the authentication system - you just need to configure your OAuth apps.
Google OAuth
1. Create OAuth Credentials:
- Go to Google Cloud Console
- Create or select a project
- Click “Create Credentials” → “OAuth 2.0 Client ID”
- Application type: “Web application”
- Add authorized redirect URI:
{your-app-url}/auth/google/callback- Development:
http://localhost:8090/auth/google/callback - Production:
https://yourdomain.com/auth/google/callback
- Development:
2. Configure Environment Variables:
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
3. Enable Google+ API:
- In Google Cloud Console, enable “Google+ API” for your project
- This allows your app to fetch user profile information
GitHub OAuth
1. Register OAuth App:
- Go to GitHub Developer Settings
- Click “New OAuth App”
- Fill in details:
- Application name: Your app name
- Homepage URL: Your app URL
- Authorization callback URL:
{your-app-url}/auth/github/callback- Development:
http://localhost:8090/auth/github/callback - Production:
https://yourdomain.com/auth/github/callback
- Development:
2. Configure Environment Variables:
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
Testing OAuth
Set credentials in .env, run task dev, navigate to /auth, and click the OAuth button. For production, set credentials in your hosting environment and update callback URLs to production domain.
OAuth Security
Never commit OAuth credentials to git. Use separate apps for development and production.
Example Usage
Protect routes:
mux.HandleFunc("GET /app/dashboard", middleware.RequireAuth(handler))
Access user in handlers:
user := ctxkeys.User(r.Context())
profile := ctxkeys.Profile(r.Context())
subscription := ctxkeys.Subscription(r.Context())