jwt Package
JWT token generation and validation for LaResto authentication.
Features
- Access & Refresh Tokens: Dual-token authentication
- Custom Claims: User ID, email, role support
- Token Validation: Signature verification with expiration checking
- Token Refresh: Generate new tokens using refresh token
- Configurable TTL: Separate lifetimes for access/refresh tokens
- Extract User ID: Get user ID without full validation (for logging)
Installation
import "github.com/LaRestoOU/laresto-go-common/pkg/jwt"
Quick Start
Create Token Manager
cfg := jwt.Config{
AccessSecret: os.Getenv("JWT_ACCESS_SECRET"),
RefreshSecret: os.Getenv("JWT_REFRESH_SECRET"),
Issuer: "laresto-auth-service",
AccessTTL: 15 * time.Minute,
RefreshTTL: 7 * 24 * time.Hour,
}
tm, err := jwt.NewTokenManager(cfg)
if err != nil {
log.Fatal("Failed to create token manager", err)
}
Generate Tokens
tokens, err := tm.GenerateTokenPair("user-123", "user@example.com", "admin")
if err != nil {
return err
}
// Returns:
// tokens.AccessToken - Use for API requests (15 min)
// tokens.RefreshToken - Store in client (7 days)
// tokens.ExpiresAt - When access token expires
Validate Access Token
claims, err := tm.ValidateAccessToken(accessToken)
if err != nil {
// Token invalid, expired, or tampered
return errors.ErrUnauthorized
}
// Access user info
userID := claims.UserID
email := claims.Email
role := claims.Role
Refresh Tokens
newTokens, err := tm.RefreshTokens(refreshToken)
if err != nil {
// Refresh token invalid or expired
return errors.ErrUnauthorized
}
// Returns new access + refresh tokens
Configuration
type Config struct {
// AccessSecret is the secret for access tokens (required)
AccessSecret string
// RefreshSecret is the secret for refresh tokens (required)
RefreshSecret string
// Issuer identifies the token issuer (required)
Issuer string
// AccessTTL is access token lifetime (default: 15 minutes)
AccessTTL time.Duration
// RefreshTTL is refresh token lifetime (default: 7 days)
RefreshTTL time.Duration
}
Recommended values:
- AccessTTL: 15 minutes (short-lived for security)
- RefreshTTL: 7 days (convenient but revocable)
- Secrets: 32+ character random strings
Token Structure
Claims
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
// Standard JWT claims
jwt.RegisteredClaims
}
Token Payload (decoded)
{
"user_id": "123",
"email": "user@example.com",
"role": "admin",
"iss": "laresto-auth-service",
"sub": "123",
"exp": 1672531200,
"iat": 1672530300,
"nbf": 1672530300
}
Usage Patterns
Login Flow
func (s *AuthService) Login(ctx context.Context, email, password string) (*jwt.TokenPair, error) {
// Verify credentials
user, err := s.db.FindUserByEmail(ctx, email)
if err != nil {
return nil, errors.ErrUnauthorized
}
if !bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(password)) {
return nil, errors.ErrUnauthorized
}
// Generate tokens
tokens, err := s.tokenManager.GenerateTokenPair(
user.ID,
user.Email,
user.Role,
)
if err != nil {
return nil, err
}
// Store refresh token in Redis
s.cache.Set(ctx,
fmt.Sprintf("refresh:%s", user.ID),
tokens.RefreshToken,
7*24*time.Hour,
)
return tokens, nil
}
Protected Endpoint
func (h *Handler) GetProfile(c *gin.Context) {
// Get token from header
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{"error": "Missing authorization header"})
return
}
// Extract token (format: "Bearer <token>")
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(401, gin.H{"error": "Invalid authorization format"})
return
}
// Validate token
claims, err := h.tokenManager.ValidateAccessToken(parts[1])
if err != nil {
c.JSON(401, gin.H{"error": "Invalid or expired token"})
return
}
// Use claims
user, err := h.userService.GetUser(c.Request.Context(), claims.UserID)
if err != nil {
c.JSON(500, gin.H{"error": "Internal error"})
return
}
c.JSON(200, user)
}
Token Refresh Flow
func (s *AuthService) RefreshTokens(ctx context.Context, refreshToken string) (*jwt.TokenPair, error) {
// Validate refresh token
claims, err := s.tokenManager.ValidateRefreshToken(refreshToken)
if err != nil {
return nil, errors.ErrUnauthorized
}
// Check if refresh token exists in Redis (not revoked)
storedToken, err := s.cache.Get(ctx,
fmt.Sprintf("refresh:%s", claims.UserID),
&tokenData,
)
if err != nil || storedToken != refreshToken {
return nil, errors.ErrUnauthorized
}
// Generate new tokens
newTokens, err := s.tokenManager.RefreshTokens(refreshToken)
if err != nil {
return nil, err
}
// Update Redis with new refresh token
s.cache.Set(ctx,
fmt.Sprintf("refresh:%s", claims.UserID),
newTokens.RefreshToken,
7*24*time.Hour,
)
return newTokens, nil
}
Logout (Token Revocation)
func (s *AuthService) Logout(ctx context.Context, userID string) error {
// Delete refresh token from Redis
return s.cache.Delete(ctx, fmt.Sprintf("refresh:%s", userID))
}
Middleware
func AuthMiddleware(tm *jwt.TokenManager) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(401, gin.H{"error": "Invalid token format"})
return
}
claims, err := tm.ValidateAccessToken(parts[1])
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "Invalid token"})
return
}
// Store claims in context
c.Set("user_id", claims.UserID)
c.Set("email", claims.Email)
c.Set("role", claims.Role)
c.Next()
}
}
// Usage
router.Use(AuthMiddleware(tokenManager))
router.GET("/profile", GetProfile)
Role-Based Authorization
func RequireRole(role string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("role")
if !exists {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
if userRole != role {
c.AbortWithStatusJSON(403, gin.H{"error": "Forbidden"})
return
}
c.Next()
}
}
// Usage
router.GET("/admin/users", RequireRole("admin"), ListUsers)
Token Lifecycle
1. Login
↓
Generate access (15m) + refresh (7d) tokens
↓
Store refresh token in Redis
↓
Return both tokens to client
2. API Request
↓
Client sends access token in header
↓
Server validates access token
↓
Process request
3. Access Token Expires (after 15m)
↓
Client sends refresh token to /refresh
↓
Server validates refresh token
↓
Generate new access + refresh tokens
↓
Update Redis
↓
Return new tokens
4. Logout
↓
Delete refresh token from Redis
↓
Access token expires naturally (15m)
Security Best Practices
DO ✅
// Use environment variables for secrets
cfg := jwt.Config{
AccessSecret: os.Getenv("JWT_ACCESS_SECRET"),
RefreshSecret: os.Getenv("JWT_REFRESH_SECRET"),
}
// Use different secrets for access and refresh
AccessSecret: "access-secret-XYZ..."
RefreshSecret: "refresh-secret-ABC..." // Different!
// Store refresh tokens server-side (Redis)
cache.Set(ctx, fmt.Sprintf("refresh:%s", userID), refreshToken, ttl)
// Use short access token TTL
AccessTTL: 15 * time.Minute
// Validate tokens on every protected request
claims, err := tm.ValidateAccessToken(token)
// Use HTTPS for token transmission
// Tokens sent over HTTP can be intercepted!
DON'T ❌
// Don't hardcode secrets
AccessSecret: "my-secret" // BAD!
// Don't use same secret for both
AccessSecret: "same-secret"
RefreshSecret: "same-secret" // BAD!
// Don't store tokens in localStorage (XSS vulnerable)
// Use httpOnly cookies or secure storage
// Don't use long access token TTL
AccessTTL: 24 * time.Hour // TOO LONG!
// Don't skip validation
// claims := extractClaims(token) // BAD! No validation
// Don't trust expired tokens
// Validation fails automatically
// Don't expose secrets in logs
log.Info("Token", "secret", secret) // BAD!
Token Storage (Client-Side)
Mobile App (iOS):
// Access token: Memory (15 min TTL)
var accessToken: String?
// Refresh token: Keychain (7 day TTL)
KeychainWrapper.standard.set(refreshToken, forKey: "refresh_token")
Web App:
// Access token: Memory or sessionStorage
sessionStorage.setItem('access_token', token);
// Refresh token: httpOnly cookie (recommended)
// Set by server: Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Strict
Testing
func TestAuth(t *testing.T) {
cfg := jwt.Config{
AccessSecret: "test-access-secret",
RefreshSecret: "test-refresh-secret",
Issuer: "test-issuer",
AccessTTL: 15 * time.Minute,
RefreshTTL: 7 * 24 * time.Hour,
}
tm, _ := jwt.NewTokenManager(cfg)
// Generate tokens
tokens, err := tm.GenerateTokenPair("user-123", "user@example.com", "user")
require.NoError(t, err)
// Validate access token
claims, err := tm.ValidateAccessToken(tokens.AccessToken)
require.NoError(t, err)
assert.Equal(t, "user-123", claims.UserID)
// Refresh tokens
newTokens, err := tm.RefreshTokens(tokens.RefreshToken)
require.NoError(t, err)
assert.NotEqual(t, tokens.AccessToken, newTokens.AccessToken)
}
Troubleshooting
"Token is expired"
- Access token expired (15 min) → Use refresh token
- Refresh token expired (7 days) → User must login again
"Invalid signature"
- Token was tampered with
- Using wrong secret to validate
- Token from different environment (dev vs prod)
"Token used before issued"
- Clock skew between servers
- Token not yet valid (nbf claim)
License
MIT License - see LICENSE file for details