Add refresh token authentication system
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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user