monorepo/cloud/maplepress-backend/internal/service/gateway/refresh.go

123 lines
3.9 KiB
Go

package gateway
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/jwt"
)
// RefreshTokenService handles token refresh operations
type RefreshTokenService interface {
RefreshToken(ctx context.Context, input *RefreshTokenInput) (*RefreshTokenResponse, error)
}
// RefreshTokenInput represents the input for token refresh
type RefreshTokenInput struct {
RefreshToken string
}
// RefreshTokenResponse represents the response after successful token refresh
type RefreshTokenResponse struct {
// User details
UserID string `json:"user_id"`
UserEmail string `json:"user_email"`
UserName string `json:"user_name"`
UserRole string `json:"user_role"`
// Tenant details
TenantID string `json:"tenant_id"`
// Session and new tokens
SessionID string `json:"session_id"`
AccessToken string `json:"access_token"`
AccessExpiry time.Time `json:"access_expiry"`
RefreshToken string `json:"refresh_token"`
RefreshExpiry time.Time `json:"refresh_expiry"`
RefreshedAt time.Time `json:"refreshed_at"`
}
type refreshTokenService struct {
sessionService service.SessionService
jwtProvider jwt.Provider
logger *zap.Logger
}
// NewRefreshTokenService creates a new refresh token service
func NewRefreshTokenService(
sessionService service.SessionService,
jwtProvider jwt.Provider,
logger *zap.Logger,
) RefreshTokenService {
return &refreshTokenService{
sessionService: sessionService,
jwtProvider: jwtProvider,
logger: logger.Named("refresh-token-service"),
}
}
// RefreshToken validates the refresh token and generates new access/refresh tokens
// CWE-613: Validates session still exists before issuing new tokens
func (s *refreshTokenService) RefreshToken(ctx context.Context, input *RefreshTokenInput) (*RefreshTokenResponse, error) {
s.logger.Info("processing token refresh request")
// Validate the refresh token and extract session ID
sessionID, err := s.jwtProvider.ValidateToken(input.RefreshToken)
if err != nil {
s.logger.Warn("invalid refresh token", zap.Error(err))
return nil, fmt.Errorf("invalid or expired refresh token")
}
s.logger.Debug("refresh token validated", zap.String("session_id", sessionID))
// Retrieve the session to ensure it still exists
// CWE-613: This prevents using a refresh token after logout/session deletion
session, err := s.sessionService.GetSession(ctx, sessionID)
if err != nil {
s.logger.Warn("session not found or expired",
zap.String("session_id", sessionID),
zap.Error(err))
return nil, fmt.Errorf("session not found or expired")
}
s.logger.Info("session retrieved for token refresh",
zap.String("session_id", sessionID),
zap.String("user_id", session.UserUUID.String()),
zap.String("tenant_id", session.TenantID.String()))
// Generate new JWT access and refresh tokens
// Both tokens are regenerated to maintain rotation best practices
accessToken, accessExpiry, refreshToken, refreshExpiry, err := s.jwtProvider.GenerateTokenPair(
session.ID,
AccessTokenDuration,
RefreshTokenDuration,
)
if err != nil {
s.logger.Error("failed to generate new token pair", zap.Error(err))
return nil, fmt.Errorf("failed to generate new tokens")
}
s.logger.Info("token refresh completed successfully",
zap.String("user_id", session.UserUUID.String()),
zap.String("tenant_id", session.TenantID.String()),
zap.String("session_id", session.ID))
return &RefreshTokenResponse{
UserID: session.UserUUID.String(),
UserEmail: session.UserEmail,
UserName: session.UserName,
UserRole: session.UserRole,
TenantID: session.TenantID.String(),
SessionID: session.ID,
AccessToken: accessToken,
AccessExpiry: accessExpiry,
RefreshToken: refreshToken,
RefreshExpiry: refreshExpiry,
RefreshedAt: time.Now().UTC(),
}, nil
}