Mert Tosun
← Posts
JWT Authentication in Go: Access Tokens, Refresh Tokens, and Secure Storage

JWT Authentication in Go: Access Tokens, Refresh Tokens, and Secure Storage

Mert TosunGo

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

  1. Long-lived access tokens stuffed with claims
  2. Trusting the payload without signature verification
  3. Algorithm confusion — lock allowed algorithms in the library
  4. 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.