SKIP TO CONTENT
JWTRBACSECURITY

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.

01JWT — Do It Right

Store access tokens in memory (not localStorage — 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 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
}

02RBAC Middleware

Roles map to permission sets. Check permissions in middleware, not in business logic — this 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)