JWT Authentication in Go: Access Tokens, Refresh Tokens, and Secure Storage
JWT is everywhere for carrying identity in microservices and monoliths. But “we use JWT” is not the same as “we are secure”: without storage, lifetime, rotation, and revocation, production risk grows fast.
This article focuses on JWT authentication in Go — access vs refresh and practical hardening.
Quick JWT recap
Three segments: header.payload.signature. The payload is base64url JSON — not encrypted. Anyone can read it; never put secrets there.
Access token vs refresh token
| Access | Refresh | |
|---|---|---|
| Lifetime | Short (minutes–hours) | Longer (days–weeks), but treat carefully |
| Usage | API calls | Obtain new access tokens |
| Storage | Memory / short-lived | Secure store + rotation |
If an access token leaks, a short TTL limits damage. A leaked refresh token is far worse.
Signing and verifying in Go
Popular library: github.com/golang-jwt/jwt/v5.
import "github.com/golang-jwt/jwt/v5"
type Claims struct {
UserID string `json:"uid"`
jwt.RegisteredClaims
}
func SignAccess(userID string, secret []byte, ttl time.Duration) (string, error) {
claims := Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return t.SignedString(secret)
}
Verification:
func Parse(tokenString string, secret []byte) (*Claims, error) {
tok, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(t *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil { return nil, err }
if c, ok := tok.Claims.(*Claims); ok && tok.Valid {
return c, nil
}
return nil, jwt.ErrSignatureInvalid
}
HS256 uses a shared secret; multi-service setups often prefer RS256 with public-key verification at the edge.
Storing refresh tokens safely
Avoid: localStorage for refresh tokens (XSS).
Prefer:
- HttpOnly + Secure + SameSite cookies for refresh
- Short-lived access tokens in memory or aligned cookie strategy
- Refresh token rotation: issue a new refresh on each use; invalidate the old one
Track refresh records in Redis or Postgres for logout and suspicious activity.
Common mistakes
- Long-lived access tokens stuffed with claims
- Trusting the payload without signature verification
- Algorithm confusion — lock allowed algorithms in the library
- Leaked secrets — use env vars and a secret manager
Summary
- JWT authentication in Go needs a clear access / refresh split and short access TTL.
- Protect refresh tokens with HttpOnly cookies + rotation.
- Prefer RS256 and centralized keys in production when services multiply.
Pair this with edge rate limiting (Redis rate limiting in Go): identity and abuse controls belong together.
Related posts
gRPC vs REST: When Should You Use Which? A Comparative Guide with Go
gRPC and REST in microservices: protobuf, HTTP/2, browser constraints, and Go examples — complements our Go vs Node.js service comparison.
Rate Limiting with Redis: Token Bucket and Sliding Window in Go
Production-oriented golang rate limiting with Redis — token bucket and sliding window using Lua scripts, atomic updates, and operational tips.
Building CLIs in Go: A Deep Dive into Command-Line Tools
Subcommands with Cobra, flag parsing, stdin/stdout, exit codes, cross-compilation, and testing — a practical guide to production-grade Go CLI development.