package services import ( "errors" "time" "gauth-central/config" "gauth-central/internal/models" "github.com/golang-jwt/jwt/v5" ) type JWTClaim struct { UserID string `json:"sub"` Email string `json:"email"` Permissions []string `json:"permissions,omitempty"` jwt.RegisteredClaims } type JWTService struct{} func NewJWTService() *JWTService { return &JWTService{} } func (s *JWTService) GenerateToken(user models.User) (string, error) { expirationTime := time.Now().Add(24 * time.Hour) claims := &JWTClaim{ UserID: user.ID.String(), Email: user.Email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), Issuer: "gauth-central", IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(config.AppConfig.JWTSecret)) } func (s *JWTService) GenerateTokenPair(user models.User) (string, string, error) { // Extract permissions permissionMap := make(map[string]bool) for _, role := range user.Roles { for _, perm := range role.Permissions { permissionMap[perm.Name] = true } } var permissions []string for p := range permissionMap { permissions = append(permissions, p) } // Access Token (15 min) accessTokenExp := time.Now().Add(15 * time.Minute) accessClaims := &JWTClaim{ UserID: user.ID.String(), Email: user.Email, Permissions: permissions, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(accessTokenExp), Issuer: "gauth-central", IssuedAt: jwt.NewNumericDate(time.Now()), }, } accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) signedAccessToken, err := accessToken.SignedString([]byte(config.AppConfig.JWTSecret)) if err != nil { return "", "", err } // Refresh Token (7 days) refreshTokenExp := time.Now().Add(7 * 24 * time.Hour) refreshClaims := &JWTClaim{ UserID: user.ID.String(), Email: user.Email, Permissions: nil, // Refresh token doesn't need permissions usually, or keep them if needed RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(refreshTokenExp), Issuer: "gauth-central", IssuedAt: jwt.NewNumericDate(time.Now()), }, } refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) signedRefreshToken, err := refreshToken.SignedString([]byte(config.AppConfig.JWTSecret)) if err != nil { return "", "", err } return signedAccessToken, signedRefreshToken, nil } func (s *JWTService) ValidateToken(signedToken string) (*JWTClaim, error) { token, err := jwt.ParseWithClaims( signedToken, &JWTClaim{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("unexpected signing method") } return []byte(config.AppConfig.JWTSecret), nil }, ) if err != nil { return nil, err } claims, ok := token.Claims.(*JWTClaim) if !ok { return nil, errors.New("could not parse claims") } if claims.ExpiresAt.Time.Before(time.Now()) { return nil, errors.New("token expired") } return claims, nil } // GenerateVerificationToken generates a JWT token for email verification (24 hours expiry) func (s *JWTService) GenerateVerificationToken(userID, email string) (string, error) { expirationTime := time.Now().Add(24 * time.Hour) claims := &JWTClaim{ UserID: userID, Email: email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), Issuer: "gauth-central", IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(config.AppConfig.JWTSecret)) } // ValidateVerificationToken validates a verification token and returns user ID and email func (s *JWTService) ValidateVerificationToken(tokenString string) (string, string, error) { claims, err := s.ValidateToken(tokenString) if err != nil { return "", "", err } return claims.UserID, claims.Email, nil }