feat: add session expiration tracking and validation
feat: implement Redis caching for terminated sessions feat: add new session GUID queries for validation refactor: extend Session model with last_refresh_exp_time refactor: update token generation to include role and session refactor: modify auth middleware to validate session status refactor: replace GetUserSessions with GetValidUserSessions chore: add uuid/v5 dependency fix: update router to pass dependencies to auth middleware chore: update SQL schema and queries for new expiration field
This commit is contained in:
@@ -18,25 +18,32 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"easywish/config"
|
||||
"easywish/internal/database"
|
||||
"easywish/internal/utils/enums"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
Username string `json:"username"`
|
||||
Role enums.Role `json:"role"`
|
||||
Username string `json:"username"`
|
||||
Role enums.Role `json:"role"`
|
||||
Type enums.JwtTokenType `json:"type"`
|
||||
Session string `json:"session"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// TODO: validate token type
|
||||
// TODO: validate session guid
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
// XXX: cluttered; move cache & database check to auth service
|
||||
func AuthMiddleware(log *zap.Logger, dbctx database.DbContext, redisClient *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cfg := config.GetConfig()
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
@@ -71,6 +78,70 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
|
||||
if claims.Type != enums.JwtAccessTokenType {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Not an access token"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
isTerminated, redisErr := redisClient.Get(ctx, fmt.Sprintf("session::%s::is_terminated", claims.Session)).Bool()
|
||||
if redisErr != nil && redisErr != redis.Nil {
|
||||
log.Error(
|
||||
"Failed to lookup cache to check whether session is not terminated",
|
||||
zap.Error(redisErr))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Cache if nil
|
||||
if redisErr == redis.Nil {
|
||||
db := database.NewDbHelper(dbctx)
|
||||
|
||||
session, err := db.Queries.GetSessionByGuid(db.CTX, claims.Session)
|
||||
if err != nil {
|
||||
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
log.Warn(
|
||||
"Session does not exist or was deleted",
|
||||
zap.String("session", claims.Session))
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
log.Error(
|
||||
"Failed to lookup session in database",
|
||||
zap.String("session", claims.Session),
|
||||
zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := redisClient.Set(
|
||||
ctx,
|
||||
fmt.Sprintf("session::%s::is_terminated", claims.Session),
|
||||
session.Terminated,
|
||||
time.Duration(8 * time.Hour), // XXX: magic number
|
||||
).Err(); err != nil {
|
||||
log.Error(
|
||||
"Failed to cache session's is_terminated state",
|
||||
zap.String("session", claims.Session),
|
||||
zap.Error(err))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
isTerminated = *session.Terminated
|
||||
}
|
||||
|
||||
if isTerminated {
|
||||
log.Warn(
|
||||
"Attempt to access resource from a terminated session",
|
||||
zap.String("session", claims.Session))
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
c.Next()
|
||||
|
||||
Reference in New Issue
Block a user