Day 5: Secure Login: Accessing Protected Endpoints with JWT Tokens

Lesson 5 60 min

Secure Login: Accessing Protected Endpoints with JWT Tokens

Welcome back, future architects of hyperscale systems!

In our last session, Day 3, we laid the groundwork by setting up PostgreSQL and creating our first user. That was crucial because, let's be honest, a CRM without users is just a fancy database. But a CRM without secure users? That's a ticking time bomb.

Today, we're diving deep into the heartbeat of any modern, distributed application: secure authentication and authorization using JSON Web Tokens (JWTs). This isn't just about letting someone log in; it's about building a system that can scale to millions of users, protect sensitive customer data, and seamlessly integrate with a constellation of microservices, all while keeping performance snappy.

Agenda for Today

  1. The "Why" of JWTs: Understanding why traditional session management falls short in distributed systems.

  2. JWT Anatomy: Deconstructing the token – Header, Payload, Signature.

  3. Authentication vs. Authorization: Clarifying these critical concepts.

  4. Control Flow: How a user logs in and accesses protected resources.

  5. Architecture Fit: Where our Auth Service sits in the grand CRM picture.

  6. Production Insights: Real-world considerations for security and scalability.

  7. Hands-on Build: Implementing a secure login service in Go.

Component Architecture: Our Authentication Gateway

Component Architecture

JWT Microservices Communication Flow Client App Browser/Mobile API Gateway (Nginx/Kong) Auth Service (Go Lang) PostgreSQL User Store CRM Service Protected Data 1. Login Verify 2. JWT 3. Req + JWT 4. Route Identity Sharedvia Public Key Note: API Gateway validates the JWT signature before routing to downstream services.

Imagine our CRM growing from a single monolith to a collection of specialized services: a Lead Management Service, a Sales Pipeline Service, an Analytics Service, and so on. In such an ecosystem, we can't have each service independently verifying user credentials against the database every time. That's a recipe for bottlenecks and inconsistency.

This is where our Authentication Service (which we'll build today) comes in. It acts as the gatekeeper, the bouncer at the club.

How it Fits in the Overall System

Our Go-based Authentication Service will be a dedicated microservice. When a user tries to log in, their request first hits this service. If credentials are valid, the Auth Service issues a JWT. This token then becomes the user's passport for all subsequent interactions with any protected CRM service. The beauty? Other services don't need to talk to the database for every request; they just need to verify the JWT's signature. This stateless verification is the secret sauce for scalability.

Core Concepts: JWT, Authentication, and Authorization

JWT: The Stateless Passport

A JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed.

It has three parts, separated by dots: Header.Payload.Signature.

  1. Header: Typically specifies the token type (JWT) and the signing algorithm (e.g., HS256, RS256).

  2. Payload: Contains the "claims" – statements about an entity (usually the user) and additional data. Common claims include iss (issuer), exp (expiration time), sub (subject/user ID), and custom claims like roles or permissions.

  3. Signature: This is the cryptographic magic. It's created by taking the encoded header, the encoded payload, a secret key, and the algorithm specified in the header, then signing them. This signature is what ensures the token hasn't been tampered with. If anyone changes the header or payload, the signature verification will fail.

Authentication vs. Authorization: A Crucial Distinction

  • Authentication: Who are you? This is the process of verifying a user's identity. When you type your username and password, you are authenticating. Our /login endpoint handles this.

  • Authorization: What are you allowed to do? Once authenticated, the system needs to know what resources or actions you have permission to access. For example, a sales rep can view leads, but only a manager can approve discounts. Our /protected endpoint will demonstrate a basic form of this, ensuring only authenticated users can access it. In a real CRM, the JWT's payload would carry roles or permissions claims, which downstream services would use for fine-grained authorization checks.

Control Flow: The Journey of a Login Request

Flowchart

I. AUTHENTICATION (LOGIN) II. AUTHORIZATION (ACCESS) START User Login Req Valid? 401 Error Generate JWT Client Stores JWT (Local/Cookie) Req + JWT Header Verify JWT Sign Expired? Access Resource 403 END No Yes SubsequentRequests Yes No
  1. User Initiates Login: The user sends their username and password to our /login endpoint.

  2. Authentication Service Validates: Our Go service receives these credentials. It queries our PostgreSQL database (setup in Day 3) to verify if the user exists and the password is correct.

  3. JWT Issuance: If valid, the service generates a JWT, signs it with a secret key, and sends it back to the user.

  4. Client Stores Token: The user's browser or application securely stores this JWT (e.g., in an HTTP-only cookie or memory, not local storage for security reasons, but for this lesson, we'll use a simple curl demo).

  5. Accessing Protected Resources: For any subsequent request to a protected endpoint (e.g., /api/v1/leads), the client attaches the JWT in the Authorization header (as Bearer ).

  6. JWT Verification: Before processing the request, the target service (or an API Gateway upstream) extracts the JWT and verifies its signature using the same secret key. If the signature is valid and the token hasn't expired, the user is authorized for general access.

  7. Resource Access: The request proceeds to the intended CRM service endpoint.

Data Flow & State Changes

State Machine

Start Unauthenticated Authenticating Authenticated Invalid /Expired App Init Submit Login Success Wrong Creds Token Expired Redirect to Login Manual Logout Authentication State Transitions
  • Data Flow: Credentials -> Auth Service -> DB -> JWT -> Client -> Protected Endpoint -> JWT verification -> Resource.

  • State Changes:

  • Unauthenticated: User has no valid token.

  • Authenticating: User submits credentials.

  • Authenticated: User holds a valid, unexpired JWT.

  • Token Expired/Invalid: The JWT is no longer usable; user needs to re-authenticate (or use a refresh token, a more advanced topic we'll cover later).

Real-time Production System Application: Why This Matters at Scale

For a CRM handling 100 million requests per second, JWTs are indispensable:

  • Statelessness: Crucial for horizontal scalability. Each API request carries its own authentication context, so any server instance can handle it without needing sticky sessions or shared session storage. This means you can scale your backend services independently and dynamically.

  • Microservices Harmony: JWTs allow different microservices, potentially written in different languages, to trust each other's authentication without direct communication, as long as they share the secret key (or public key for asymmetric signing).

  • Performance: Verifying a JWT's signature is computationally much cheaper than a database lookup for every request.

  • Cross-Domain/Mobile Support: JWTs are excellent for SPAs (Single Page Applications) and mobile apps, where traditional cookie-based sessions can be problematic due to CORS or platform limitations.

  • Refresh Tokens: In a production system, you'd use short-lived access tokens (for API calls) and longer-lived refresh tokens (to obtain new access tokens). This limits the window of opportunity for attackers if an access token is compromised.

Assignment: Build Your Secure Login Service

Your mission, should you choose to accept it, is to implement a barebones secure login service in Go.

Steps:

  1. Project Setup: Create a new Go module for your authentication service.

  2. Database Integration (Simplified): For this lesson, we'll simplify. Instead of a full PG connection, create a db.go that has a hardcoded User struct and a simple AuthenticateUser(username, password string) function that checks against a mock user (e.g., "admin" / "password123"). In a real system, this would connect to your PostgreSQL from Day 3.

  3. JWT Secret: Define a strong, environment-variable-driven secret key for signing JWTs.

  4. /login Endpoint:

  • Accepts POST requests with username and password in the JSON body.

  • Calls your AuthenticateUser function.

  • If authentication succeeds, generate a JWT with a short expiration time (e.g., 5 minutes) and a user_id claim.

  • Return the JWT in the response body.

  • If authentication fails, return a 401 Unauthorized status.

  1. /protected Endpoint:

  • Accepts GET requests.

  • Implements a middleware that extracts the JWT from the Authorization: Bearer header.

  • Verifies the JWT's signature and expiration.

  • If valid, allow access and return a success message (e.g., "Welcome, authenticated user!").

  • If invalid or missing, return 401 Unauthorized.

  1. main.go: Set up your HTTP server and routes using gorilla/mux.

  2. Test with curl:

  • Attempt to access /protected without a token (should fail).

  • Log in via /login to get a token.

  • Use the obtained token to access /protected (should succeed).

Solution Hints

  • Go JWT Library: Use github.com/golang-jwt/jwt/v5 for JWT handling.

  • Middleware: A common pattern for JWT validation is to create an AuthMiddleware function that wraps your protected handlers. It looks something like:

go
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Extract token, validate, then call next(w, r) if valid
    }
}
  • Error Handling: Always handle errors gracefully (e.g., invalid credentials, malformed token, expired token).

  • Environment Variables: Use os.Getenv("JWT_SECRET") for your secret key. Set a default for development but ensure it's overridden in production.

  • JSON Handling: Use json.NewDecoder(r.Body).Decode(&credentials) for incoming requests and json.NewEncoder(w).Encode(response) for outgoing.

This lesson empowers you not just to write code, but to understand the fundamental security and scalability mechanisms driving modern web applications. You're building the backbone of a resilient, high-performance CRM!

Need help?