Store access tokens in memory (not localStorage, which is XSS vulnerable). Use short expiry with refresh tokens in httpOnly cookies. Never store sensitive data in JWT payload.
Writinggo go
Backend
Where to Store a JWT (and Why Not localStorage)
localStorage is one XSS away from account takeover. Access tokens in memory, refresh tokens in httpOnly cookies, and why permission checks belong in middleware, not business logic.
JWT done right
// Go: JWT generation with short-lived access + refresh tokens
func (s *AuthService) GenerateTokens(user *User) (*TokenPair, error) {
// Access token: 15 min, stored in memory on the client
accessClaims := jwt.MapClaims{
"sub": user.ID,
"role": user.Role,
"org": user.OrgID,
"exp": time.Now().Add(15 * time.Minute).Unix(),
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
access, err := accessToken.SignedString(s.accessSecret)
if err != nil { return nil, err }
// Refresh token: 7 days, httpOnly cookie
refreshClaims := jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refresh, err := refreshToken.SignedString(s.refreshSecret)
if err != nil { return nil, err }
return &TokenPair{Access: access, Refresh: refresh}, nil
}RBAC Middleware
Roles map to permission sets. Check permissions in middleware, not in business logic. That lets you change access rules without touching code.
// Permission check middleware
func (a *Auth) HasPermission(permission string) fiber.Handler {
return func(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
orgID := c.Locals("orgID").(string)
// Fetch from cache first, then DB
perms, err := a.permCache.Get(userID + ":" + orgID)
if err != nil {
perms, err = a.repo.GetUserPermissions(userID, orgID)
if err != nil { return c.Status(500).JSON(errInternal) }
a.permCache.Set(userID+":"+orgID, perms, 5*time.Minute)
}
if !perms.Has(permission) {
return c.Status(403).JSON(fiber.Map{
"error": "insufficient permissions",
"required": permission,
})
}
return c.Next()
}
}
// Usage in routes
tickets.Post("/", auth.HasPermission("tickets:create"), h.CreateTicket)
tickets.Delete("/:id", auth.HasPermission("tickets:delete"), h.DeleteTicket)