bbe0757ab6
Implement complete refresh token flow for improved security: - Short-lived access tokens (configurable, default 1h) - Long-lived refresh tokens (configurable, default 7d) - Automatic token rotation on refresh - Token revocation for proper logout Domain layer: - Add RefreshToken entity with validation and revocation - Add RefreshTokenRepository interface Application layer: - Add RefreshTokenHandler for token refresh operations - Add RevokeTokenHandler for single token revocation - Add RevokeAllTokensHandler for user-wide revocation Infrastructure layer: - Implement SQLite RefreshTokenRepository - Add refresh_tokens table migration with indexes - Add parseDuration helper for flexible time configuration HTTP layer: - Add POST /api/v1/auth/refresh endpoint - Add POST /api/v1/auth/logout endpoint - Update login/register to return refresh tokens - Improve Swagger documentation with clear descriptions Configuration: - Update .env.example with secure token expiry defaults - Add support for minute/hour/day duration formats Tests: - Fix test suite to work with new signatures - All existing tests passing
83 lines
1.8 KiB
Go
83 lines
1.8 KiB
Go
package queries
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"time"
|
|
|
|
"apocapoc-api/internal/domain/entities"
|
|
"apocapoc-api/internal/domain/repositories"
|
|
"apocapoc-api/internal/shared/errors"
|
|
)
|
|
|
|
type RefreshTokenQuery struct {
|
|
RefreshToken string
|
|
}
|
|
|
|
type RefreshTokenResult struct {
|
|
UserID string
|
|
Email string
|
|
Timezone string
|
|
}
|
|
|
|
type RefreshTokenHandler struct {
|
|
refreshTokenRepo repositories.RefreshTokenRepository
|
|
userRepo repositories.UserRepository
|
|
}
|
|
|
|
func NewRefreshTokenHandler(
|
|
refreshTokenRepo repositories.RefreshTokenRepository,
|
|
userRepo repositories.UserRepository,
|
|
) *RefreshTokenHandler {
|
|
return &RefreshTokenHandler{
|
|
refreshTokenRepo: refreshTokenRepo,
|
|
userRepo: userRepo,
|
|
}
|
|
}
|
|
|
|
func (h *RefreshTokenHandler) Handle(ctx context.Context, query RefreshTokenQuery) (*RefreshTokenResult, error) {
|
|
if query.RefreshToken == "" {
|
|
return nil, errors.ErrInvalidInput
|
|
}
|
|
|
|
refreshToken, err := h.refreshTokenRepo.FindByToken(ctx, query.RefreshToken)
|
|
if err != nil {
|
|
return nil, errors.ErrNotFound
|
|
}
|
|
|
|
if !refreshToken.IsValid() {
|
|
return nil, errors.ErrNotFound
|
|
}
|
|
|
|
user, err := h.userRepo.FindByID(ctx, refreshToken.UserID)
|
|
if err != nil {
|
|
return nil, errors.ErrNotFound
|
|
}
|
|
|
|
return &RefreshTokenResult{
|
|
UserID: user.ID,
|
|
Email: user.Email,
|
|
Timezone: user.Timezone,
|
|
}, nil
|
|
}
|
|
|
|
func GenerateRefreshToken() (string, error) {
|
|
b := make([]byte, 32)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("failed to generate refresh token: %w", err)
|
|
}
|
|
return base64.URLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
func CreateRefreshToken(userID string, expiryDuration time.Duration) (*entities.RefreshToken, error) {
|
|
token, err := GenerateRefreshToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
expiresAt := time.Now().Add(expiryDuration)
|
|
return entities.NewRefreshToken(userID, token, expiresAt), nil
|
|
}
|