DevNotes
Support
Microservices Fundamentals · Chapter 09

Security in Microservices

Edge vs service-level security, OAuth2 and OpenID Connect roles, an authorization server such as Keycloak, services as JWT resource servers, token relay, service-to-service authentication with client credentials and mTLS, scopes and roles, and zero trust.

◷ 7 min read§ 10 sections? 6 interview questions{ } 3 code examples

#Introduction

In a monolith, a user logs in once and a session covers every feature. With dozens of services, each one must know who is calling and what they're allowed to do — without every service handling passwords. The standard answer is OAuth2 + OpenID Connect with JWT access tokens.

#Where Security Is Enforced

LayerResponsibility
API gatewayReject requests without a valid token; rate limiting; CORS; TLS termination
Each serviceValidate the token again and authorize every operation (roles, scopes, ownership)
NetworkTLS (ideally mTLS) between services; network policies limiting who can talk to whom

#OAuth2 and OpenID Connect

RoleExample
Resource ownerThe user
ClientThe web app, mobile app or another service
Authorization serverKeycloak, Okta, Auth0, Azure AD / Entra ID, Spring Authorization Server
Resource serverYour microservices, which accept access tokens
  • OAuth2 is about authorization — getting an access token to call APIs.
  • OpenID Connect (OIDC) adds authentication on top — an ID token describing the user, and a standard user-info endpoint.

The Login Flow (Authorization Code + PKCE)

User clicks "Log in" in the web or mobile app
App redirects to the authorization server's login page
User authenticates — password, MFA, social login
Authorization server redirects back with a one-time code
App exchanges the code (plus a PKCE verifier) for tokens
App calls APIs with Authorization: Bearer access-token

Your services never see the user's password — only signed tokens. Login, MFA, password resets and social login are handled once, by the authorization server.

#Tokens

TokenPurposeLifetime
Access token (usually a JWT)Sent to APIs; carries subject, scopes / roles, audience, expiryShort — minutes
ID token (JWT, OIDC)Tells the client who logged inShort
Refresh tokenGets new access tokens without logging in againLonger — hours or days

A typical access token payload:

JSON
{
  "iss": "https://auth.example.com/realms/shop",
  "sub": "8f14e45f-ceea-467a-9575-0b5b1c1e3f9a",
  "aud": "order-service",
  "scope": "orders:read orders:write",
  "realm_access": { "roles": ["CUSTOMER"] },
  "exp": 1760522430
}

Because the authorization server signs tokens with RS256 (private key) and publishes its public keys at a JWKS endpoint, every service can verify tokens locally, without calling the auth server on each request.

#Services as Resource Servers

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
YAML
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/shop   # discovers the JWKS keys automatically
          audiences: order-service                          # reject tokens meant for other services
Java
@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/orders/**").hasAuthority("SCOPE_orders:read")
                .requestMatchers("/api/orders/**").hasAuthority("SCOPE_orders:write")
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable());
        return http.build();
    }
}

Spring Security validates the signature, expiry, issuer and audience, and maps the scope claim to SCOPE_… authorities. (Roles in custom claims, like Keycloak's realm_access.roles, need a small JwtAuthenticationConverter.)

Authorization Inside the Service

Scopes say what kind of action the client may take; the service must still check which data:

Java
@PreAuthorize("hasAuthority('SCOPE_orders:read')")
@GetMapping("/api/orders/{id}")
public OrderView get(@PathVariable String id, @AuthenticationPrincipal Jwt jwt) {
    Order order = orderService.find(id);
    if (!order.customerId().equals(jwt.getSubject()) && !isSupport(jwt)) {
        throw new AccessDeniedException("Not your order");    // object-level authorization
    }
    return OrderView.from(order);
}

#Propagating the User's Identity

When the order service calls the payment service on behalf of the user, forward the user's token:

Java
@Bean
RestClient paymentClient(RestClient.Builder builder) {
    return builder
            .baseUrl("http://payment-service")
            .requestInterceptor((request, body, execution) -> {
                Authentication auth = SecurityContextHolder.getContext().getAuthentication();
                if (auth instanceof JwtAuthenticationToken jwt) {
                    request.getHeaders().setBearerAuth(jwt.getToken().getTokenValue());
                }
                return execution.execute(request, body);
            })
            .build();
}

At the gateway, Spring Cloud Gateway's TokenRelay filter does this when the gateway itself handles the OAuth2 login.

#Service-to-Service Authentication

For calls not on behalf of a user — a scheduled job, an event consumer calling another service — use the client credentials grant: the service authenticates as itself and gets its own token.

YAML
spring:
  security:
    oauth2:
      client:
        registration:
          inventory-client:
            provider: shop-auth
            client-id: order-service
            client-secret: ${ORDER_SERVICE_CLIENT_SECRET}
            authorization-grant-type: client_credentials
            scope: inventory:read
        provider:
          shop-auth:
            issuer-uri: https://auth.example.com/realms/shop

Spring Security's OAuth2 client support (for example OAuth2ClientHttpRequestInterceptor with RestClient) fetches, caches and refreshes the token automatically.

mTLS and Service Meshes

Mutual TLS makes both sides present certificates, so services prove their identity at the connection level and all traffic is encrypted. Doing it by hand is painful; service meshes (Istio, Linkerd) issue and rotate certificates automatically and enforce which services may call which.

MechanismProves
User access token (forwarded)Which user, which scopes
Client-credentials tokenWhich service is calling, with which scopes
mTLSWhich workload is on the other end of the connection

#Other Essentials

  • TLS everywhere — at the edge and between services.
  • Secrets in Vault or a cloud secret manager — never in Git or images.
  • Least privilege — narrow scopes per client; network policies per service.
  • Validate input in every service — the gateway can't know each service's rules.
  • Audit logging of security-relevant actions, without logging tokens.
  • Dependency and image scanning in CI.

#Interview Questions

Q1How do you secure microservices?

Use an authorization server with OAuth2/OIDC; clients obtain access tokens (JWTs); the gateway validates them at the edge and every service validates them again as a resource server and enforces authorization, including object-level checks. Service-to-service calls use forwarded user tokens or client-credentials tokens, over TLS or mTLS.

Q2What's the difference between OAuth2 and OpenID Connect?

OAuth2 is an authorization framework for obtaining access tokens to call APIs; OIDC is an identity layer on top that authenticates the user and provides an ID token.

Q3Why validate tokens in each service if the gateway already does?

Zero trust: internal traffic may bypass the gateway, and only the service knows its own authorization rules.

Q4How do services verify JWTs without calling the auth server?

Tokens are signed with the auth server's private key (RS256); services download its public keys from the JWKS endpoint (found via the issuer URI) and verify signatures locally.

Q5How do you authenticate service-to-service calls?

Forward the user's token for calls made on their behalf; use the OAuth2 client-credentials grant for the service's own calls; and add mTLS (often via a service mesh) for transport-level identity.

Q6What is broken object-level authorization?

Failing to check that the authenticated user is allowed to access the specific object requested — for example, any user reading any order by changing the ID.

#Key Takeaways

  • Centralise login in an authorization server (Keycloak, Okta, Entra ID); services never handle passwords.
  • Services are OAuth2 resource servers that validate JWTs locally via JWKS.
  • Validate at the gateway and in every service; check object-level permissions.
  • Forward user tokens; use client credentials for service identity; add mTLS.
  • TLS everywhere, least privilege, and secrets outside code.