package auth import ( "crypto/rand" "encoding/hex" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" ) // JWTManager handles JWT token creation and verification type JWTManager struct { secretKey string issuer string duration time.Duration } // JWTClaims represents custom JWT claims type JWTClaims struct { UserID string `json:"user_id"` Email string `json:"email"` Username string `json:"username"` jwt.RegisteredClaims } // RefreshTokenClaims represents refresh token claims type RefreshTokenClaims struct { UserID string `json:"user_id"` jwt.RegisteredClaims } // NewJWTManager creates a new JWT manager func NewJWTManager(secretKey, issuer string, duration time.Duration) *JWTManager { return &JWTManager{ secretKey: secretKey, issuer: issuer, duration: duration, } } // GenerateAccessToken generates a new access token func (m *JWTManager) GenerateAccessToken(userID, email, username string) (string, error) { claims := JWTClaims{ UserID: userID, Email: email, Username: username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.duration)), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: m.issuer, }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(m.secretKey)) } // GenerateRefreshToken generates a new refresh token func (m *JWTManager) GenerateRefreshToken(userID string) (string, error) { claims := RefreshTokenClaims{ UserID: userID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour * 7)), // 7 days IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: m.issuer, }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(m.secretKey)) } // VerifyAccessToken verifies and parses an access token func (m *JWTManager) VerifyAccessToken(tokenString string) (*JWTClaims, error) { claims := &JWTClaims{} token, err := jwt.ParseWithClaims(tokenString, 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(m.secretKey), nil }) if err != nil { return nil, err } if !token.Valid { return nil, errors.New("invalid token") } return claims, nil } // VerifyRefreshToken verifies and parses a refresh token func (m *JWTManager) VerifyRefreshToken(tokenString string) (*RefreshTokenClaims, error) { claims := &RefreshTokenClaims{} token, err := jwt.ParseWithClaims(tokenString, 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(m.secretKey), nil }) if err != nil { return nil, err } if !token.Valid { return nil, errors.New("invalid token") } return claims, nil } // GenerateRandomToken generates a random token for password reset, etc. func GenerateRandomToken(length int) (string, error) { token := make([]byte, length) if _, err := rand.Read(token); err != nil { return "", err } return hex.EncodeToString(token), nil } // GeneratePKCEChallenge generates a PKCE code challenge func GeneratePKCEChallenge() (codeVerifier, codeChallenge string, err error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", "", err } codeVerifier = hex.EncodeToString(bytes) // For code_challenge, we'd need base64url encoding of SHA256(verifier) // For simplicity in this example, using hex, but in production use base64url(sha256(verifier)) codeChallenge = codeVerifier return } // GenerateStateToken generates a state token for OAuth/OIDC flows func GenerateStateToken() (string, error) { return GenerateRandomToken(16) }