feat: fully implemented Refresh method;
fix: Improve error handling in Refresh method for token validation; fix: Update Refresh route to use correct request model; fix: Correct request model for password reset complete route; fix: Redis pipeline error handling in AuthService constructor; fix: Refresh method wanted access token; refactor: Enhance error handling for unexpected token validation errors; refactor: Simplify claims extraction in ValidateToken method; fix: Ensure session termination state is correctly dereferenced; refactor: Return structured session info in ValidateToken method; feat: New util method to check if an error is one of multiple given ones;
This commit is contained in:
@@ -155,8 +155,16 @@ func (a *authControllerImpl) Refresh(c *gin.Context) {
|
|||||||
|
|
||||||
response, err := a.auth.Refresh(request.Body)
|
response, err := a.auth.Refresh(request.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errs.ErrUnauthorized) {
|
if errors.Is(err, errs.ErrTokenExpired) {
|
||||||
c.Status(http.StatusUnauthorized)
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is expired"})
|
||||||
|
} else if errors.Is(err, errs.ErrTokenInvalid) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is invalid"})
|
||||||
|
} else if errors.Is(err, errs.ErrWrongTokenType) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token type"})
|
||||||
|
} else if errors.Is(err, errs.ErrSessionNotFound) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Could not find session in database"})
|
||||||
|
} else if errors.Is(err, errs.ErrSessionTerminated) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Session is terminated"})
|
||||||
} else {
|
} else {
|
||||||
c.Status(http.StatusInternalServerError)
|
c.Status(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@@ -233,7 +241,7 @@ func (a *authControllerImpl) RegisterRoutes(group *gin.RouterGroup) {
|
|||||||
group.POST("/registrationBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.RegistrationBegin)
|
group.POST("/registrationBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.RegistrationBegin)
|
||||||
group.POST("/registrationComplete", middleware.RequestMiddleware[models.RegistrationCompleteRequest](enums.GuestRole), a.RegistrationComplete)
|
group.POST("/registrationComplete", middleware.RequestMiddleware[models.RegistrationCompleteRequest](enums.GuestRole), a.RegistrationComplete)
|
||||||
group.POST("/login", middleware.RequestMiddleware[models.LoginRequest](enums.GuestRole), a.Login)
|
group.POST("/login", middleware.RequestMiddleware[models.LoginRequest](enums.GuestRole), a.Login)
|
||||||
group.POST("/refresh", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.UserRole), a.Refresh)
|
group.POST("/refresh", middleware.RequestMiddleware[models.RefreshRequest](enums.GuestRole), a.Refresh)
|
||||||
group.POST("/passwordResetBegin", middleware.RequestMiddleware[models.PasswordResetBeginRequest](enums.GuestRole), a.PasswordResetBegin)
|
group.POST("/passwordResetBegin", middleware.RequestMiddleware[models.PasswordResetBeginRequest](enums.GuestRole), a.PasswordResetBegin)
|
||||||
group.POST("/passwordResetComplete", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.PasswordResetComplete)
|
group.POST("/passwordResetComplete", middleware.RequestMiddleware[models.PasswordResetCompleteRequest](enums.GuestRole), a.PasswordResetComplete)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ func NewAuthService(_log *zap.Logger, _dbctx database.DbContext, _redis *redis.C
|
|||||||
// FIXME: review possible problems due to a large pipeline request
|
// FIXME: review possible problems due to a large pipeline request
|
||||||
pipe := _redis.Pipeline()
|
pipe := _redis.Pipeline()
|
||||||
for _, guid := range guids {
|
for _, guid := range guids {
|
||||||
if err := pipe.Set(ctx, fmt.Sprintf("session::%s::is_terminated", guid), true, 0); err != nil {
|
if err := pipe.Set(ctx, fmt.Sprintf("session::%s::is_terminated", guid), true, 0).Err(); err != nil {
|
||||||
panic("Failed to cache terminated session: " + err.Err().Error())
|
panic("Failed to cache terminated session: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_log.Info("Cached terminated sessions' GUIDs in Redis", zap.Int("amount", len(guids)))
|
_log.Info("Cached terminated sessions' GUIDs in Redis", zap.Int("amount", len(guids)))
|
||||||
@@ -509,8 +509,23 @@ func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginRespo
|
|||||||
|
|
||||||
func (a *authServiceImpl) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) {
|
func (a *authServiceImpl) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) {
|
||||||
|
|
||||||
sessionInfo, err := a.ValidateToken(request.RefreshToken, enums.JwtAccessTokenType); if err != nil {
|
sessionInfo, err := a.ValidateToken(request.RefreshToken, enums.JwtRefreshTokenType); if err != nil {
|
||||||
return nil, err
|
|
||||||
|
if utils.ErrorIsOneOf(
|
||||||
|
err,
|
||||||
|
errs.ErrInvalidToken,
|
||||||
|
errs.ErrTokenExpired,
|
||||||
|
errs.ErrWrongTokenType,
|
||||||
|
errs.ErrSessionNotFound,
|
||||||
|
errs.ErrSessionTerminated,
|
||||||
|
) {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
a.log.Error(
|
||||||
|
"Encountered an unexpected error while validating token",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, refreshToken, err := utils.GenerateTokens(
|
accessToken, refreshToken, err := utils.GenerateTokens(
|
||||||
@@ -558,7 +573,7 @@ func (a *authServiceImpl) ValidateToken(jwtToken string, tokenType enums.JwtToke
|
|||||||
return nil, errs.ErrInvalidToken
|
return nil, errs.ErrInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims, ok := token.Claims.(*dto.UserClaims); ok && token.Valid {
|
claims, ok := token.Claims.(*dto.UserClaims); if ok && token.Valid {
|
||||||
|
|
||||||
if claims.Type != tokenType {
|
if claims.Type != tokenType {
|
||||||
return nil, errs.ErrWrongTokenType
|
return nil, errs.ErrWrongTokenType
|
||||||
@@ -597,7 +612,7 @@ func (a *authServiceImpl) ValidateToken(jwtToken string, tokenType enums.JwtToke
|
|||||||
if err := a.redis.Set(
|
if err := a.redis.Set(
|
||||||
ctx,
|
ctx,
|
||||||
fmt.Sprintf("session::%s::is_terminated", claims.Session),
|
fmt.Sprintf("session::%s::is_terminated", claims.Session),
|
||||||
session.Terminated,
|
*session.Terminated,
|
||||||
time.Duration(8 * time.Hour), // XXX: magic number
|
time.Duration(8 * time.Hour), // XXX: magic number
|
||||||
).Err(); err != nil {
|
).Err(); err != nil {
|
||||||
a.log.Error(
|
a.log.Error(
|
||||||
@@ -615,7 +630,14 @@ func (a *authServiceImpl) ValidateToken(jwtToken string, tokenType enums.JwtToke
|
|||||||
return nil, errs.ErrSessionTerminated
|
return nil, errs.ErrSessionTerminated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, errs.ErrNotImplemented
|
|
||||||
|
sessionInfo := dto.SessionInfo{
|
||||||
|
Username: claims.Username,
|
||||||
|
Session: claims.Session,
|
||||||
|
Role: claims.Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sessionInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *authServiceImpl) PasswordResetBegin(request models.PasswordResetBeginRequest) (bool, error) {
|
func (a *authServiceImpl) PasswordResetBegin(request models.PasswordResetBeginRequest) (bool, error) {
|
||||||
|
|||||||
29
backend/internal/utils/errors.go
Normal file
29
backend/internal/utils/errors.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
|
//
|
||||||
|
// This file is part of Easywish
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||||
|
// the GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func ErrorIsOneOf(err error, ignoreErrors ...error) bool {
|
||||||
|
for _, ignore := range ignoreErrors {
|
||||||
|
if errors.Is(err, ignore) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user