JWT auth
Login, refresh, middleware.
Grit ships JWT auth out of the box: login, register, refresh, logout, and a middleware that validates tokens on protected routes. This lesson walks how it's wired and the two extension points you'll actually touch.
The flow
POST /api/auth/register {email, password} â 201 + {access_token, refresh_token}POST /api/auth/login {email, password} â 200 + {access_token, refresh_token}POST /api/auth/refresh {refresh_token} â 200 + {access_token}POST /api/auth/logout Authorization header â 204GET /api/auth/me Authorization header â 200 + {user}
Two tokens: a 15-minute access token sent on every request, and a 7-day refresh token used to get new access tokens without re-prompting login.
The access token shape
// Decoded JWT payload{"sub": "9b4d-...", // user UUID"role": "user", // role for RBAC (covered in lesson 3.4)"exp": 1730000000, // expiry timestamp"iat": 1729000000}
Signed with JWT_SECRET from .env using HS256. Grit's middleware pins the algorithm â alg:none attacks are impossible.
The Auth middleware
api := r.Group("/api")// Public â no auth requiredauth := api.Group("/auth"){auth.POST("/login", authHandler.Login)auth.POST("/register", authHandler.Register)auth.POST("/refresh", authHandler.Refresh)}// Protected â Auth middleware appliedprotected := api.Group("")protected.Use(middleware.Auth(authService)){protected.GET("/auth/me", authHandler.Me)protected.POST("/auth/logout", authHandler.Logout)protected.GET("/users", userHandler.List)// âĻ}
The middleware reads Authorization: Bearer <token>, verifies signature + expiry, and puts the user ID + role on the request context. Handlers grab it via:
userID, _ := c.Get("user_id")role, _ := c.Get("role")
What to actually extend
Two places you'll touch:
- What gets returned with /auth/me â add fields to the response in
authHandler.Me. Avatar URL, default tenant, feature flags. - Custom claims â add fields to the JWT payload (e.g., tenant_id for multi-tenancy). Edit
authService.GenerateToken+ the verifier.
Refresh-token rotation
Each successful refresh issues a NEW refresh token and invalidates the old one (stored in a small DB table). This protects against stolen refresh tokens â a thief who uses the token rotates it, then the legitimate client's next refresh fails, and you know there's an incident.
Quick check
Try it
Try the full auth flow with curl:
# 1. Register$curl -X POST http://localhost:8080/api/auth/register \$ -H 'Content-Type: application/json' \$ -d '{"email":"alex@example.com","password":"alexsecret123","name":"Alex"}'# 2. Login (or save the access_token from register response)$curl -X POST http://localhost:8080/api/auth/login \$ -H 'Content-Type: application/json' \$ -d '{"email":"alex@example.com","password":"alexsecret123"}'# 3. Hit /me with the token$curl http://localhost:8080/api/auth/me \$ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
Paste the /me response in notes.md.
What's next
Email + password is one path. Most users prefer OAuth2 â Sign in with Google / GitHub. Next lesson wires both in 10 minutes.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled â suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub