package auth import ( "fmt" "time" "github.com/golang-jwt/jwt/v5" ) // TokenPair represents a pair of Public and Access tokens type TokenPair struct { PublicToken string `json:"public_token"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` TokenType string `json:"token_type"` ExpiresIn int64 `json:"expires_in"` } // Claims represents JWT access token claims type Claims struct { UserID string `json:"user_id"` Email string `json:"email"` Roles []string `json:"roles"` MFA bool `json:"mfa_verified"` jwt.RegisteredClaims } // TokenService handles token generation and validation type TokenService struct { publicToken string refreshSecret string accessSecret string accessExpiry time.Duration refreshExpiry time.Duration } // TokenConfig holds configuration for token service type TokenConfig struct { PublicToken string RefreshSecret string AccessSecret string AccessExpiry time.Duration RefreshExpiry time.Duration } // NewTokenService creates a new TokenService instance func NewTokenService(config TokenConfig) *TokenService { return &TokenService{ publicToken: config.PublicToken, refreshSecret: config.RefreshSecret, accessSecret: config.AccessSecret, accessExpiry: config.AccessExpiry, refreshExpiry: config.RefreshExpiry, } } // ValidatePublicToken validates the public token func (s *TokenService) ValidatePublicToken(publicToken string) bool { return publicToken == s.publicToken } // GeneratePublicToken returns the configured public token. func (s *TokenService) GeneratePublicToken(userID, email string, roles []string) string { return s.publicToken } // GenerateTokenPair generates a new token pair func (s *TokenService) GenerateTokenPair(userID, email string, roles []string) (*TokenPair, error) { // Generate refresh token (JWT) refreshClaims := jwt.RegisteredClaims{ Subject: userID, Audience: []string{"xcontrol-refresh"}, ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.refreshExpiry)), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "xcontrol-account", } refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) refreshTokenString, err := refreshToken.SignedString([]byte(s.refreshSecret)) if err != nil { return nil, fmt.Errorf("failed to sign refresh token: %w", err) } // Generate access token (JWT) claims := Claims{ UserID: userID, Email: email, Roles: roles, MFA: true, // Assume MFA is verified for now RegisteredClaims: jwt.RegisteredClaims{ Subject: userID, Audience: []string{"xcontrol-access"}, ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.accessExpiry)), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "xcontrol-account", }, } accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) accessTokenString, err := accessToken.SignedString([]byte(s.accessSecret)) if err != nil { return nil, fmt.Errorf("failed to sign access token: %w", err) } return &TokenPair{ PublicToken: s.publicToken, AccessToken: accessTokenString, RefreshToken: refreshTokenString, TokenType: "Bearer", ExpiresIn: int64(s.accessExpiry.Seconds()), }, nil } // ValidateAccessToken validates and parses an access token func (s *TokenService) ValidateAccessToken(accessToken string) (*Claims, error) { token, err := jwt.ParseWithClaims(accessToken, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(s.accessSecret), nil }) if err != nil { return nil, fmt.Errorf("failed to parse access token: %w", err) } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, fmt.Errorf("invalid access token") } return claims, nil } // RefreshAccessToken generates a new access token using refresh token func (s *TokenService) RefreshAccessToken(refreshToken string) (string, error) { token, err := jwt.ParseWithClaims(refreshToken, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(s.refreshSecret), nil }) if err != nil { return "", fmt.Errorf("failed to parse refresh token: %w", err) } claims, ok := token.Claims.(*jwt.RegisteredClaims) if !ok || !token.Valid { return "", fmt.Errorf("invalid refresh token") } // Verify issuer and audience if claims.Issuer != "xcontrol-account" { return "", fmt.Errorf("invalid token issuer") } if !contains(claims.Audience, "xcontrol-refresh") { return "", fmt.Errorf("invalid token audience") } // Generate new access token newClaims := Claims{ UserID: claims.Subject, Email: "", // Will be populated from user store Roles: []string{"user"}, MFA: true, RegisteredClaims: jwt.RegisteredClaims{ Subject: claims.Subject, Audience: []string{"xcontrol-access"}, ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.accessExpiry)), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "xcontrol-account", }, } accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, newClaims) accessTokenString, err := accessToken.SignedString([]byte(s.accessSecret)) if err != nil { return "", fmt.Errorf("failed to sign access token: %w", err) } return accessTokenString, nil } // GetAccessTokenExpiry returns the access token expiry duration func (s *TokenService) GetAccessTokenExpiry() time.Duration { return s.accessExpiry } // Helper function to check if a slice contains a string func contains(slice []string, str string) bool { for _, s := range slice { if s == str { return true } } return false }