We're reaching the 12th installment of our microservices architecture patterns series. As always, I encourage you to check out the full series of posts:

  1. Microservices Architecture Patterns: What Are They and What Benefits Do They Offer?
  2. Architecture Patterns: Organization and Structure of Microservices.
  3. Architecture Patterns: Microservices Communication and Coordination.
  4. Microservices Architecture Patterns: SAGA, API Gateway, and Service Discovery.
  5. Microservices Architecture Patterns: Event Sourcing and Event-Driven Architecture (EDA).
  6. Microservices Architecture Patterns: Communication and Coordination with CQRS, BFF, and Outbox.
  7. Microservices Patterns: Scalability and Resource Management with Auto Scaling.
  8. Architecture Patterns: From Monolith to Microservices.
  9. Externalized Configuration.
  10. Architecture Patterns in Microservices: Consumer-Driven Contract Testing
  11. Microservices Architecture Patterns: Security

Let's pick it up again

In the previous post, we introduced security patterns and explored Token-based Authentication and OAuth (Open Authorization).
In this post, we continue with JWT.

Note: The code examples are meant to illustrate the concepts. Some parts may be incomplete or contain demo snippets that wouldn't apply to real-world environments.

JWT (JSON Web Tokens)

Overview and structure

JSON Web Tokens (JWT) is a standard (RFC 7519) for securely representing claims using JSON and digital signatures. A typical JWT consists of three parts:

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

Header (Base64-url): {"alg":"HS256","typ":"JWT"}

  1. Payload: contains the claims, which are the data describing the subject (sub), the expiration time (exp) in timestamp format, the issuer (iss), roles, etc.

Payload (Base64-url): {"sub":"5","email":"usuario@demo.com","roles":["ROLE_USER"],"exp":1700515039}

  1. Signature: generated by applying a cryptographic algorithm with a secret key or a key pair (private/public) to the Header + Payload.

Signature (Base64-url): generated using HMAC-SHA256.

Once formed, a JWT might look like this (simplified):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiI1IiwiZW1haWwiOiJ1c3VhcmlvQGRlbW8uY29tIiwicm9sZXMiOlsiUk9MRV9VU0VSIl0sImV4cCI6MTcwMDUxNTAzOX0
.
NLk__3FHxpb3ZzRdC0s5EZhWpTnzKnLxK4bEB-tkL28

e-Commerce Scenario

Let’s assume our e-commerce platform includes an authentication service that, instead of issuing opaque tokens and storing them in a database, directly issues signed JWTs.

The sequence diagram:

  1. The client sends credentials to POST /auth/login.
  2. The Auth Service validates the credentials and generates a JWT with user ID, email, roles, and expiration date.
  3. The client stores the JWT locally (in a secure localStorage, an HttpOnly cookie, etc.).
  4. Every time it accesses a microservice (Cart, Orders, Catalog), it includes the JWT in the Authorization: Bearer header.
  5. The microservice, upon receiving the token, validates the signature and expiration, extracts the claims, and decides whether the user is allowed to perform the operation.
Sequence diagram.

The flow diagram:

Flow diagram.

The process is the same, but with a bit more detail:

The big advantage: we don’t need to call the Auth Service on every request.
The main disadvantage is that if we want to revoke a token before it expires, we need to implement an additional strategy (blacklist, password change, very short expiration intervals, etc.).

Implementation in Spring Boot

In this section, we’ll walk through how to integrate JWT (JSON Web Tokens) into a Spring Boot application step-by-step.

Dependencies

To begin, it’s essential to include the necessary dependencies for Spring Security and a library to handle JWT tokens. Typically, these are:

These dependencies allow the project to configure endpoint protection using Spring Security and to sign and parse JWT tokens.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Class for Generating JWT (JwtTokenProvider)

At this point, we create a class (e.g., JwtTokenProvider) responsible for generating the token once the user has successfully authenticated.

Typically, a method like generateToken(Authentication authentication) is used, which:

The token's payload usually contains:

This class is also responsible for:

@Service
public class JwtTokenProvider {

    private final String jwtSecret = "MiSuperSecreto";
    private final long jwtExpirationInMillis = 3600000; // 1 hora

    // Create JWT based on user data
    public String generateToken(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMillis);

        // Example: claims: subject, roles, email, etc.
        return Jwts.builder()
                .setSubject(userDetails.getUsername()) // Podría ser "email" o "userId"
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS256, jwtSecret)
                .compact();
    }

    // Validate JWT
    public boolean validateToken(String token) { 
    try { 
        Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token); 
        return true; 
    } catch (ExpiredJwtException e) { 
        System.out.println("El token ha expirado.");
    } catch (SignatureException e) { 
        System.out.println("Firma del token inválida.");
    } catch (MalformedJwtException e) { 
        System.out.println("Token mal formado.");
    }
    return false;
}

    // Get 'subject' (usually username o userId)
    public String getUsernameFromJWT(String token) {
        Claims claims = Jwts.parser().setSigningKey(jwtSecret)
                .parseClaimsJws(token).getBody();
        return claims.getSubject();
    }
}

JWT Authentication Filter

For each HTTP request, we need to intercept the Authorization header and, if a JWT token exists, validate it. This is done with a filter that extends OncePerRequestFilter:

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
                                    throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (tokenProvider.validateToken(token)) {
                String username = tokenProvider.getUsernameFromJWT(token);

                // Load more user details, e.g. roles. 
                // We could have them in the database or encoded in the token claims..
                // Here we take on an exemplary role:
                UserDetails userDetails = new User(
                        username,
                        "",
                        Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER”))
                );

                UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

Security Configuration

In SecurityConfig (either by extending WebSecurityConfigurerAdapter or using the newer bean-based approach), we register the JWT filter and define which endpoints should be secured or publicly accessible:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/auth/login", "/auth/register").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

Authentication Controller

Finally, we need a controller to handle the login process, where:

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenProvider tokenProvider;

    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
        // We authenticate with Spring's ‘AuthenticationManager’
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // Generamos el JWT
        String jwt = tokenProvider.generateToken(authentication);
        return ResponseEntity.ok(Collections.singletonMap("token", jwt));
    }
}

JWT: Pros and Cons

Pros:

Cons:

Hybrid Architecture Example (OAuth2 + JWT) in e-Commerce

In many cases, several patterns are combined. For example:

This provides the best of both worlds:

In an e-commerce scenario, we could:

This approach reduces barriers for new users (e.g., no need to create a new account—just use Google) and maintains efficient token validation.

Login flow using hybrid OAuth + JWT architecture.
Login flow using hybrid OAuth + JWT architecture.

Advanced Considerations and Best Practices

Handling Refresh Tokens

In both Token-based and JWT strategies, you can implement refresh token logic to avoid forcing users to re-login constantly. The idea is:

Token Revocation

Asymmetric vs Symmetric Signing

JWT can use either a shared secret (HMAC) or asymmetric key pairs (RSA/ECDSA).

Key Storage Security

You should never store the signing key in plain text within the code repository. Use a secret manager (Vault, AWS Secrets Manager, etc.) or environment variables. In production environments, key security is critical to prevent token forgery.

HTTPS / TLS

Always encrypt the communication channel. Use TLS (HTTPS) for both external interactions and internal microservice communications (if feasible). Without encryption, attackers could steal tokens (JWT or otherwise).

Logging and Auditing

In e-commerce, it's common to require audit logs to track sensitive actions (purchases, cancellations, product changes). Including user data (ID, roles) in logs improves traceability and compliance.

Integration with API Gateway

In many microservices architectures, an API Gateway (e.g., Spring Cloud Gateway or NGINX) is used. The Gateway can intercept requests and verify tokens before routing to internal microservices.

This simplifies security logic, as you don't need to add authentication filters in each microservice (though they may still have additional validation if needed).

Final Comparison Between Patterns

Feature Token-based Auth OAuth2 JWT
Ease of implementation Relatively simple Moderate to high (multiple flows) Moderate (requires signing and parsing tokens)
Typical scenario Internal apps, small/medium projects Login with external providers, delegation to a server Microservices with local validation, scalability
Need for central service Optional (if token is validated in store) Yes (Authorization Server) Only for issuance, validation can be decentralized
Revocation Easy if token is stored in DB/Redis Via the Authorization Server Requires blacklists or short expiration
Token size Usually small (UUID) Depends on provider (can be opaque token) Can grow with number of claims
Use of claims Optional (can store only an ID) Yes, in some flows (depends on ID Token) Strong, claims defined in payload

Conclusions and Recommendations

Always evaluate your workload, architecture, user experience, and system complexity before choosing a pattern. In many cases, a combination of approaches is the best solution.

In complex applications that handle transactions and personal data, it's crucial to carefully design the access and token management flow.

Security in a microservices architecture is a broad topic, covering authentication, authorization, data integrity, and protection against multiple attack vectors.

In the end, the best choice depends on your business context, tech stack, and scalability/security requirements. Many production systems combine, for example, OAuth2 with JWT issuance, and a central API gateway that intercepts and verifies requests to simplify the architecture for internal microservices.

Regardless of your approach, security must always be a priority from the early design stages, ensuring a systematic and robust strategy that protects the confidentiality, integrity, and availability of your services — and above all, your customers' data.

We're Not Done Yet

We hope you enjoyed this security section — there’s much more to explore. In the next post, we’ll cover fault tolerance patterns, which, as the name suggests, allow systems to remain functional even when some components fail or crash.

References and Recommended Reading

Tell us what you think.

Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.

Subscribe