Add refresh token authentication system
CI/CD Pipeline / Test (push) Has been cancelled
CI/CD Pipeline / Lint (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Image (push) Has been cancelled

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
This commit is contained in:
2025-11-27 09:57:54 +01:00
parent 10d45fc34e
commit bbe0757ab6
17 changed files with 3839 additions and 34 deletions
@@ -0,0 +1,82 @@
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
}