As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Building a system that knows who you are, and knows it quickly and securely, is one of the most fundamental tasks in modern software. When you log into an app, you're asking it to remember you, to trust that you are who you say you are, and to give you access to your private data. In the world of Go, or Golang, we have powerful tools to build this gatekeeping logic. Today, I want to walk you through how we can construct a system that is both a vigilant guard and a gracious host, using JSON Web Tokens (JWT) and session management.
Let's start with the main problem. You need a way to give a user a temporary key after they prove their identity. This key should be verifiable by your servers without having to check the database every single time. That's where JWT comes in. Think of a JWT as a sealed letter. Inside the letter is information like your user ID and when the letter expires. The seal is a cryptographic signature. If anyone tampers with the contents, the seal breaks, and we know not to trust it.
But JWTs alone aren't a complete solution. If we only use them, we run into a tricky issue: we can't easily invalidate that key before it expires. If a user logs out, or if we detect suspicious activity, we want to be able to immediately reject their key. This is where we bring in sessions. A session is like a ledger entry that says, "This user's key is currently valid." By combining short-lived JWTs with a session store we can control, we get the best of both worlds: fast verification and the ability to revoke access instantly.
The code I'll share with you creates a central AuthManager. This manager is the brain of the operation. When you first build it, you give it a secret key for signing JWTs and the address for a Redis database. Redis is fantastic here because it's incredibly fast and lets us store our session ledger in a way that all our application servers can share.
Here's how we kick things off:
func NewAuthManager(jwtSecret string, redisAddr string) *AuthManager {
redisClient := redis.NewClient(&redis.Options{
Addr: redisAddr,
Password: "",
DB: 0,
})
return &AuthManager{
jwtSecret: []byte(jwtSecret),
redisClient: redisClient,
sessionStore: &SessionStore{
sessions: make(map[string]*UserSession),
redisKey: "sessions",
},
tokenCache: &TokenCache{
cache: make(map[string]*TokenInfo),
ttl: 5 * time.Minute,
},
rateLimiter: NewRateLimiter(5, time.Minute),
}
}
Now, let's follow the journey of a user named Alice. She opens her app and enters her username and password. Her device sends this to our /login endpoint. The AuthenticateUser method swings into action.
First, it checks a rate limiter. We don't want someone to try thousands of passwords per second against Alice's account. The limiter might say, "You can only try 5 times per minute from this username."
If that passes, we look up Alice's user record from our main database. We don't store her password. We store a scrambled version called a hash, created with the bcrypt algorithm. It's designed to be slow to crack. We use bcrypt.CompareHashAndPassword to see if the password she sent matches the hash we have on file.
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
atomic.AddUint64(&am.stats.failedAttempts, 1)
return nil, fmt.Errorf("invalid credentials")
}
If the password is correct, the real work begins. We need to create three things for Alice:
- A Session Token: A long, random string that will identify this specific login session on her device.
- An Access Token: A JWT that her device will send with every request to prove its right to access things.
- A Refresh Token: A special JWT with a longer life, used solely to get a new Access Token when the old one expires.
We create the session token using Go's crypto/rand package, which gives us cryptographically strong random numbers. This is crucial. A weak, predictable session token would be like having a house key that's easy to copy.
func (am *AuthManager) generateSessionToken() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
Next, we generate the two JWTs. We use the github.com/golang-jwt/jwt/v5 library. We put Alice's user ID and an expiration time inside the token and then sign it with our secret key.
accessClaims := jwtClaims{
UserID: userID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: now.Add(1 * time.Hour).Unix(),
IssuedAt: now.Unix(),
Issuer: "auth-service",
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessSigned, err := accessToken.SignedString(am.jwtSecret)
Now, we have to remember that Alice is logged in. We store a record of her session in Redis, keyed by her user ID. We set it to expire in 24 hours. We also keep a copy in a local in-memory map in our SessionStore for very fast access. This is our hybrid approach. Checking local memory is faster than a network call to Redis, but Redis ensures all our servers see the same active sessions.
session := &UserSession{
UserID: user.ID,
CreatedAt: time.Now(),
LastAccess: time.Now(),
IPAddress: getClientIP(ctx),
UserAgent: getUserAgent(ctx),
IsValid: true,
}
err := am.redisClient.Set(ctx, sessionKey, token, 24*time.Hour).Err()
am.sessionStore.Set(token, session)
We send the Access Token, Refresh Token, and Session Token back to Alice's device. Her app will now store these securely.
A few minutes later, Alice wants to see her profile. Her app sends the Access Token in the HTTP request header, like this: Authorization: Bearer eyJhbGc.... This hits our AuthMiddleware. The middleware's job is to protect certain routes. It extracts the token and calls ValidateToken.
This is where our performance tricks come in. Validating a JWT signature takes a bit of computing power. If we have thousands of requests per second, doing this every time adds up. So, we use a TokenCache. It's a simple map in memory that stores recently validated tokens for a short time (like 5 minutes).
When a request comes in, we check the cache first. If the token is there and hasn't expired, we instantly know it's valid. We skip the heavy cryptographic verification. This simple trick can dramatically cut down response times.
func (am *AuthManager) ValidateToken(ctx context.Context, tokenString string) (*TokenClaims, error) {
// Check token cache first
if cached, ok := am.tokenCache.Get(tokenString); ok {
if time.Now().Before(cached.ExpiresAt) {
return &TokenClaims{UserID: cached.UserID, Valid: true}, nil
}
am.tokenCache.Remove(tokenString)
}
// ... proceed with full JWT validation if not in cache
}
If it's not in the cache, we do the full JWT validation. We parse the token, check its signature with our secret, and make sure it hasn't expired. But we're not done. Remember the session ledger? We must check Redis to see if this user's session is still active. If Alice logged out, we would have deleted her session from Redis. Even if her JWT is technically valid, this check will fail, and we deny the request.
sessionKey := fmt.Sprintf("session:%s", claims.UserID)
exists, err := am.redisClient.Exists(ctx, sessionKey).Result()
if err != nil || exists == 0 {
return nil, fmt.Errorf("session expired")
}
After an hour, Alice's Access Token expires. Her app will notice this and call our /refresh endpoint using the Refresh Token. The RefreshToken method is careful. It validates the Refresh Token, which has a longer life (like 7 days). Crucially, it then blacklists that used Refresh Token in Redis before issuing a new pair.
Why? This "refresh token rotation" is a security feature. If someone stole Alice's Refresh Token, they could use it to get new Access Tokens. But the moment the real Alice or the attacker uses it, the old one is blacklisted. If the attacker tries to use it again, we'll see it's on the blacklist and reject the request. This helps us detect and stop token theft.
func (am *AuthManager) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) {
// ... validate the refresh token ...
blacklisted, err := am.isTokenBlacklisted(ctx, refreshToken)
if err != nil || blacklisted {
return nil, fmt.Errorf("invalid refresh token")
}
// ... generate new tokens ...
if err := am.blacklistToken(ctx, refreshToken, claims.ExpiresAt.Sub(time.Now())); err != nil {
log.Printf("Failed to blacklist token: %v", err)
}
return newTokens, nil
}
Finally, when Alice taps "Log Out," her app calls our logout endpoint. The Logout method does the most important job of all: it removes the session from Redis. Instantly, any further requests with her Access Tokens will fail the session check in ValidateToken. It's an immediate invalidation. We also clean up the local session store copy and update our statistics.
This system is built for a busy world. The two-tier cache (in-memory for sessions, Redis for distributed sync) keeps things fast. The rate limiter stops brute-force attacks. Token rotation limits the damage from theft. Every piece has a purpose, working together to create a secure, scalable gateway for your users.
Building this taught me that good authentication isn't about one perfect technology. It's about layers. JWT gives you stateless claims. Sessions give you control. Caches give you speed. Redis gives you consistency across servers. When you combine them thoughtfully, you build a system that users can trust, and that can handle the pressure of real-world use. You're not just checking a password; you're maintaining a secure, ongoing conversation with every user, and that conversation needs to be both efficient and completely reliable.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)