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.