Frontend Labs
⋯

48. Auth service: password hashing and JWT issuing/verification

48.1 Never store plaintext or naively-hashed passwords

import "golang.org/x/crypto/bcrypt"

func hashPassword(plain string) (string, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
	return string(hash), err
}

func checkPassword(hash, plain string) bool {
	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil
}

bcrypt (or argon2id for stronger, memory-hard hashing) is deliberately slow and salted per-call — a single unsalted SHA-256 hash of a password is crackable via precomputed rainbow tables in seconds; bcrypt at a reasonable cost factor takes meaningfully longer per guess, at scale.

48.2 Issuing a JWT

import "github.com/golang-jwt/jwt/v5"

var signingKey = []byte(os.Getenv("JWT_SECRET")) // never hardcode this

func issueToken(userID string) (string, error) {
	claims := jwt.MapClaims{
		"sub": userID,
		"exp": time.Now().Add(15 * time.Minute).Unix(),
		"iat": time.Now().Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(signingKey)
}

48.3 Verifying a JWT — and the pitfalls that matter

func verifyToken(tokenStr string) (string, error) {
	token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
		// CRITICAL: verify the signing method yourself. Never trust the
		// token's own "alg" header blindly — that's the classic
		// "alg: none" / algorithm-confusion JWT attack.
		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
		}
		return signingKey, nil
	})
	if err != nil {
		return "", err
	}
	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok || !token.Valid {
		return "", fmt.Errorf("invalid token")
	}
	sub, _ := claims["sub"].(string)
	return sub, nil
}

48.4 Auth middleware

func requireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		authHeader := r.Header.Get("Authorization")
		tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
		userID, err := verifyToken(tokenStr)
		if err != nil {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		ctx := context.WithValue(r.Context(), userIDKey{}, userID)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

48.5 Real-world example

Short-lived access tokens (minutes, as above) paired with a separate, longer-lived refresh token stored server-side (so it can be revoked) is the standard production pattern — a stolen access token expires quickly on its own, and revocation only has to be checked at the much less-frequent refresh step.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36