jwt.go 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. package jwt
  2. import (
  3. "crypto/rand"
  4. "encoding/hex"
  5. "errors"
  6. "fmt"
  7. "net/url"
  8. "time"
  9. "github.com/golang-jwt/jwt/v5"
  10. )
  11. // JWTClient handles JWT operations for email verification
  12. type JWTClient struct {
  13. secret []byte
  14. }
  15. // EmailVerificationClaims contains claims specific to email verification tokens
  16. type EmailVerificationClaims struct {
  17. Email string `json:"email"`
  18. jwt.RegisteredClaims
  19. }
  20. func NewJWTClient(secret []byte) *JWTClient {
  21. return &JWTClient{secret: secret}
  22. }
  23. // GenerateEmailVerificationToken creates a JWT token for email verification
  24. // The token includes the email in both the claims and subject for additional security
  25. // and expires after 24 hours
  26. func (j *JWTClient) GenerateEmailVerificationToken(email string) (string, error) {
  27. uniqueID, err := generateUniqueID()
  28. if err != nil {
  29. return "", fmt.Errorf("failed to generate unique ID: %w", err)
  30. }
  31. claims := EmailVerificationClaims{
  32. Email: email,
  33. RegisteredClaims: jwt.RegisteredClaims{
  34. ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(24 * time.Hour)),
  35. IssuedAt: jwt.NewNumericDate(time.Now().UTC()),
  36. NotBefore: jwt.NewNumericDate(time.Now().UTC()),
  37. Subject: email,
  38. ID: uniqueID,
  39. },
  40. }
  41. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  42. return token.SignedString(j.secret)
  43. }
  44. func (j *JWTClient) GenerateEmailVerificationURL(email, baseURL string) (string, error) {
  45. token, err := j.GenerateEmailVerificationToken(email)
  46. if err != nil {
  47. return "", fmt.Errorf("failed to generate token: %w", err)
  48. }
  49. // Ensure the token is URL-safe
  50. return fmt.Sprintf("%s?token=%s", baseURL, url.QueryEscape(token)), nil
  51. }
  52. func (j *JWTClient) VerifyEmailToken(tokenString string) (string, error) {
  53. token, err := jwt.ParseWithClaims(
  54. tokenString,
  55. &EmailVerificationClaims{},
  56. func(token *jwt.Token) (interface{}, error) {
  57. if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
  58. return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
  59. }
  60. return j.secret, nil
  61. },
  62. )
  63. if err != nil {
  64. return "", fmt.Errorf("invalid token: %w", err)
  65. }
  66. if !token.Valid {
  67. return "", errors.New("token is not valid")
  68. }
  69. claims, ok := token.Claims.(*EmailVerificationClaims)
  70. if !ok {
  71. return "", errors.New("invalid claims type")
  72. }
  73. if claims.Subject != claims.Email {
  74. return "", errors.New("token subject does not match email")
  75. }
  76. return claims.Email, nil
  77. }
  78. func generateUniqueID() (string, error) {
  79. bytes := make([]byte, 16)
  80. if _, err := rand.Read(bytes); err != nil {
  81. return "", err
  82. }
  83. return hex.EncodeToString(bytes), nil
  84. }