Security Deep Dive: Auth, 2FA, WAF & Rate Limiting
Most applications are vulnerable because security is added as an afterthought — a middleware bolted on after launch, a rate limiter added after an attack. Grit builds security in from day one. In this course, you will explore every security layer in a Grit application: password hashing, JWT tokens, role-based access, two-factor authentication, backup codes, trusted devices, a web application firewall, rate limiting, security headers, and OAuth2.
Why Security Matters
Before we look at Grit's security features, let's understand what we're defending against. Here are the most common attacks against web applications:
- • Brute Force: An attacker tries thousands of password combinations until one works. Without rate limiting, they can try millions of passwords per hour
- • SQL Injection: An attacker sends malicious SQL in form fields or URL parameters, tricking the database into executing unintended commands. Can dump your entire database
- • XSS (Cross-Site Scripting): An attacker injects JavaScript into your pages that runs in other users' browsers, stealing cookies, tokens, or personal data
- • CSRF (Cross-Site Request Forgery): An attacker tricks a logged-in user's browser into making requests to your API — transferring money, changing passwords, deleting data
- • Credential Stuffing: Attackers use leaked email/password combinations from other breaches to try logging into your app. Most people reuse passwords across sites
Challenge: Know Your Enemy
Name 3 common web attacks and explain how each one works in your own words. For each attack, describe: (1) what the attacker does, (2) what they gain if successful, and (3) how you think it can be prevented.
Layer 1: Password Security (bcrypt)
The most fundamental security rule: never store passwords in plain text. If an attacker gains access to your database (through SQL injection, a backup leak, or an insider threat), plain text passwords give them immediate access to every account. Instead, Grit stores passwords as one-way hashes.
Here's the process when a user registers:
- 1.User submits password:
myPassword123 - 2.bcrypt generates a random salt:
$2a$10$N9qo8uLOickgx2ZMRZoMye - 3.bcrypt hashes password + salt through 1,024 iterations
- 4.Result stored in database:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
// Hashing a password during registration
hashedPassword, err := bcrypt.GenerateFromPassword(
[]byte(input.Password),
bcrypt.DefaultCost, // Cost factor 10 = 2^10 iterations
)
// Verifying a password during login
err := bcrypt.CompareHashAndPassword(
[]byte(user.Password), // stored hash
[]byte(input.Password), // submitted password
)Challenge: Find the Hash
Open the auth service code in your project. Find where bcrypt hashes the password during registration and where it compares during login. What cost factor is used? What would happen if you changed it to 14? (Hint: each increment doubles the time.)
Layer 2: JWT Authentication
After verifying a password, Grit issues two tokens: an access token (short-lived, 15 minutes) and a refresh token (long-lived, 7 days). This dual-token approach limits the damage if a token is stolen.
Why two tokens? If an access token is intercepted by an attacker, it only works for 15 minutes. The refresh token is stored in an HTTP-only cookie (inaccessible to JavaScript), so XSS attacks cannot steal it. When the access token expires, the frontend silently uses the refresh token to get a new one.
Login successful
├── Access Token (JWT, 15 min expiry)
│ └── Sent in Authorization header: Bearer <token>
│ └── Contains: user ID, email, role
│ └── Stateless — no database lookup needed
│
└── Refresh Token (JWT, 7 day expiry)
└── Stored in HTTP-only cookie
└── Used only to get new access tokens
└── Cannot be read by JavaScript (XSS protection)The auth middleware runs on every protected route. It extracts the JWT from the Authorization header, verifies the signature, checks the expiration, and attaches the user information to the request context.
Challenge: Decode a JWT
Login to your Grit API and copy the access token. Go to jwt.io and paste it. What information is in the payload? What algorithm is used for signing? What is the expiration time (exp claim)?
Layer 3: Role-Based Access Control
Authentication answers "who are you?" Authorization answers "what are you allowed to do?" Grit uses role-based access control (RBAC) with three built-in roles:
| Role | Permissions |
|---|---|
| ADMIN | Full access — manage users, view all data, access admin panel, system settings |
| EDITOR | Create and edit content — manage resources, upload files, but cannot manage users |
| USER | Basic access — view content, manage own profile, limited write access |
The RequireRole middleware enforces access control at the route level:
// Public routes — no auth required
auth := api.Group("/auth")
auth.POST("/login", h.Login)
auth.POST("/register", h.Register)
// Protected routes — any authenticated user
users := api.Group("/users")
users.Use(middleware.AuthMiddleware(cfg))
users.GET("/me", h.GetMe)
// Admin-only routes
admin := api.Group("/admin")
admin.Use(middleware.AuthMiddleware(cfg))
admin.Use(middleware.RequireRole("ADMIN"))
admin.GET("/users", h.ListUsers)
admin.DELETE("/users/:id", h.DeleteUser)Challenge: Test Role Enforcement
Log in as a user with the USER role. Try to access an ADMIN-only endpoint like GET /api/admin/users. What HTTP status code do you get? What does the error message say? Now log in as an ADMIN and try the same endpoint.
Layer 4: Two-Factor Authentication (TOTP)
Passwords can be stolen through phishing, keyloggers, or data breaches. Two-factor authentication (2FA) adds a second layer: something you know (password) plus something you have (your phone). Even if an attacker has your password, they cannot log in without your phone.
The 2FA setup flow in Grit:
- 1.Generate secret. The API creates a random TOTP secret and returns it as a QR code URL.
- 2.Scan QR code. The user scans the QR code with their authenticator app, which stores the secret.
- 3.Verify code. The user enters the current 6-digit code from their app to prove it's working.
- 4.2FA enabled. The secret is saved to the database. Future logins require both password and TOTP code.
Step 1: POST /api/auth/login
Body: { "email": "user@example.com", "password": "..." }
Response: { "requires_2fa": true, "temp_token": "..." }
Step 2: POST /api/auth/verify-2fa
Body: { "temp_token": "...", "code": "482917" }
Response: { "access_token": "...", "refresh_token": "..." }Challenge: Enable 2FA
Enable two-factor authentication on your account using Google Authenticator, Authy, or any TOTP-compatible app. Follow the setup flow: generate the secret, scan the QR code, enter the verification code. Then log out and log back in — you should be prompted for a 6-digit code.
Layer 5: Backup Codes
What happens if you lose your phone? Without backup codes, you're locked out of your account forever. Grit generates 10 one-time backup codes when 2FA is enabled. Each code can be used exactly once as a substitute for the TOTP code.
Backup codes are:
- • 10 codes generated when 2FA is enabled
- • One-time use — each code is invalidated after use
- • bcrypt-hashed in the database — if the database is compromised, the raw codes are not exposed
- • Shown once — the user sees the plain text codes only at generation time and must save them
Your backup codes (save these somewhere safe):
1. a8f2-k9m3-p4x7
2. b3n6-j7w2-q8r1
3. c5t9-h1y4-s6v3
4. d2p8-g3z7-u9e5
5. e7k1-f6x4-w2m8
6. f4j3-e9n6-y7t2
7. g1w7-d2p5-z3k9
8. h8y4-c7m1-a6j3
9. i6z2-b4t8-e1w5
10. j3x9-a1s7-f4n6
Each code can only be used once.
Store them in a password manager or print them.Challenge: Test Backup Codes
With 2FA enabled, use a backup code to log in instead of your authenticator app. Then try the same backup code again. What happens? How many backup codes do you have remaining?
Layer 6: Trusted Devices
Entering a 2FA code every time you log in on the same computer gets annoying fast. Trusted devices solve this: after you verify 2FA once, the server sets a 30-day cookie on your browser. On subsequent logins from that device, the 2FA step is skipped.
The trusted device cookie is:
- • Secure — only sent over HTTPS (in production)
- • HTTP-only — JavaScript cannot read it (XSS protection)
- • 30-day expiry — after 30 days, you'll need to enter a 2FA code again
- • Per-device — trusting your laptop does not trust your phone's browser
Challenge: Inspect the Cookie
After completing a 2FA verification, open your browser's DevTools (F12) and go to the Application tab → Cookies. Find the trusted device cookie. What is its name? What is its expiry date? Is it marked as HTTP-only and Secure?
Layer 7: Sentinel WAF
Grit includes Sentinel, a built-in WAF that runs as middleware. It inspects every request and blocks:
- • SQL injection patterns — detects
' OR 1=1 --,UNION SELECT, and other injection attempts in query parameters and request bodies - • XSS attempts — blocks
<script>tags, event handlers likeonerror=, and other JavaScript injection vectors - • Suspicious user agents — blocks known attack tools like sqlmap, nikto, and dirbuster
- • Path traversal — blocks attempts to access files outside the web root using
../../
Sentinel also includes a dashboard at /sentinel/ui where you can see:
- • Total requests processed
- • Blocked requests and the reasons they were blocked
- • Top attacking IPs
- • Attack type distribution
Challenge: Explore the WAF Dashboard
Visit /sentinel/ui in your browser. What metrics does the dashboard show? How many requests have been processed? Have any been blocked? Try sending a request with a SQL injection pattern in a query parameter and check if Sentinel catches it.
Layer 8: Rate Limiting
Grit's Sentinel middleware includes configurable rate limiting. Limits are applied per IP address and can be configured per route or globally:
// Default rate limits in Grit
Global: 100 requests per minute per IP
Auth: 10 requests per minute per IP (login, register)
Upload: 20 requests per minute per IP
API: 60 requests per minute per IP
// When a client exceeds the limit:
// HTTP 429 Too Many Requests
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please try again later.",
"details": {
"retry_after": 45
}
}
}The auth endpoints have a stricter limit (10/minute) because brute force attacks target login endpoints. An attacker trying 10,000 passwords per minute would be blocked after the first 10 attempts.
Challenge: Find the Rate Limits
What rate limits are configured in your Grit project by default? Look at the Sentinel configuration in your code. What happens when you exceed the limit? What HTTP status code is returned? What does the retry_after field tell the client?
Layer 9: Security Headers
HTTP response headers can instruct browsers to enable security features. Grit sets these headers on every response:
| Header | Value | Prevents |
|---|---|---|
| Strict-Transport-Security | max-age=31536000 | Forces HTTPS for 1 year, prevents downgrade attacks |
| X-Frame-Options | DENY | Prevents your site from being embedded in iframes (clickjacking) |
| X-Content-Type-Options | nosniff | Prevents browsers from guessing content types (MIME sniffing) |
| Content-Security-Policy | default-src 'self' | Controls which resources can load, blocks inline scripts (XSS) |
| Referrer-Policy | strict-origin-when-cross-origin | Limits referrer information sent to external sites (privacy) |
Challenge: Check Your Headers
Open DevTools (F12) in your browser. Go to the Network tab. Make any request to your API. Click on the request and look at the Response Headers section. List all the security headers you see. Are all five headers from the table present?
Layer 10: JWT Secret Management
The JWT secret is the key used to sign and verify tokens. If an attacker knows your secret, they can forge tokens for any user — including admin accounts. The secret must be:
- • Long — at least 32 characters (256 bits)
- • Random — generated by a cryptographic random number generator, not a human-chosen phrase
- • Secret — stored in environment variables, never committed to Git
- • Unique per environment — development, staging, and production should each have different secrets
# Generate a 64-character hex string (256 bits of entropy)
openssl rand -hex 32
# Example output:
# a3f8b2c1e9d7f4a6b8c2e1d3f5a7b9c1e3d5f7a9b1c3e5d7f9a1b3c5e7d9f1a3
# Add to your .env file (NEVER commit this file)
JWT_SECRET=a3f8b2c1e9d7f4a6b8c2e1d3f5a7b9c1e3d5f7a9b1c3e5d7f9a1b3c5e7d9f1a3Challenge: Secure Your Secret
Generate a strong JWT secret using openssl rand -hex 32. Replace the default JWT_SECRET in your .env file. Restart the API. Are you still logged in? (You should not be — the old tokens are now invalid.)
OAuth2 Security
Social login (Google, GitHub, etc.) is not just convenient — it's a security feature. When users log in with Google, your application never sees their password. This prevents:
- • Password reuse attacks — if a user's password is leaked from another site, your app is unaffected because you never stored a password
- • Phishing — users authenticate on Google's domain, not on a page you control, so fake login pages are harder to create
- • Weak passwords — Google enforces its own password policies and 2FA, which are typically stronger than what most apps enforce
Grit links OAuth accounts by email. If a user registers with email/password and later logs in with Google using the same email, the accounts are automatically linked. One user, multiple login methods.
Challenge: Explore OAuth Config
Look at the OAuth configuration in your project. What scopes does Google OAuth request? (Scopes define what information your app can access from Google.) What happens if a user logs in with Google using an email that already has a password account?
Summary: The 10-Layer Security Stack
| Layer | Feature | Protects Against |
|---|---|---|
| 1 | bcrypt Passwords | Database leaks, rainbow tables |
| 2 | JWT Tokens | Session hijacking, token theft (short expiry) |
| 3 | RBAC | Privilege escalation, unauthorized access |
| 4 | TOTP 2FA | Stolen passwords, phishing |
| 5 | Backup Codes | Lost device lockout |
| 6 | Trusted Devices | 2FA fatigue (usability without sacrificing security) |
| 7 | Sentinel WAF | SQL injection, XSS, path traversal |
| 8 | Rate Limiting | Brute force, DDoS, API abuse |
| 9 | Security Headers | Clickjacking, MIME sniffing, protocol downgrade |
| 10 | JWT Secret Management | Token forgery, session compromise |
Challenge: Security Audit — Part 1
Perform the first half of a security audit on your Grit application:
- Check your
JWT_SECRET— is it at least 32 characters? Is it random (not a dictionary word)? - Verify password hashing — register a user, then look at the users table in GORM Studio. Is the password a bcrypt hash (starts with
$2a$)? - Test 2FA — enable it, log out, log back in. Does it require the 6-digit code?
- Test a backup code — use one, then try it again. Is it rejected the second time?
Challenge: Security Audit — Part 2
Complete the second half of the security audit:
- Test rate limiting — send 15 rapid login requests. Are you rate limited?
- Check security headers — inspect response headers in DevTools. Are all 5 headers present?
- Test RBAC — log in as USER, attempt an ADMIN endpoint. Is it blocked?
- Check the Sentinel dashboard — are any attacks logged?
Challenge: Final Challenge: Write a Security Report
Write a one-page security report for your Grit application. For each of the 10 security layers, document: (1) whether it's enabled, (2) how it's configured, and (3) any improvements you would make. This is the kind of document you would give to a security auditor or include in your project's documentation.
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.