Skip to content
Hiren Limbasiya
Email me
Writing

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

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.

go
// 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.

go
// 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)

Written by Hiren Limbasiya, tech lead and full-stack engineer. Questions or corrections are welcome.