Security in Microservices

Authentication, Authorization & Trust

The Security Challenge

In a Monolith:

  • User logs in → Session created → Cookie stored
  • Every request carries session cookie
  • Server validates session in memory or DB

In Microservices:

  • User logs in → Where? Gateway? Auth Service?
  • 15 services → Do they all check the session DB?
  • Service-to-service calls → How do they authenticate?

Session-based auth doesn't scale in distributed systems.

Token-Based Authentication

Solution: Use tokens instead of sessions.

Flow:

  1. User logs in → Auth service returns token
  2. Client includes token in every request
  3. Each service validates token independently (no DB lookup)

Key Benefit: Stateless authentication.

JWT (JSON Web Token)

Structure:

HEADER.PAYLOAD.SIGNATURE

Example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJkb2N0b3ItNDU2IiwibmFtZSI6IkRyLiBTbWl0aCIsImV4cCI6MTYxNjIzOTAyMn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decoded Payload:

{
  "sub": "doctor-456",
  "name": "Dr. Smith",
  "role": "DOCTOR",
  "exp": 1735689600
}

JWT Components

Header:

{
  "alg": "RS256",  // Signature algorithm
  "typ": "JWT"
}

Payload (Claims):

{
  "sub": "user-id",      // Subject (user identifier)
  "iss": "auth-service",  // Issuer
  "exp": 1735689600,     // Expiration timestamp
  "iat": 1735686000,     // Issued at
  "role": "DOCTOR"       // Custom claim
}

Signature:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

JWT Validation

Each service validates the token:

  1. Decode header and payload
  2. Verify signature (using public key or secret)
  3. Check expiration (exp > now)
  4. Extract user info and roles

No database lookup needed!

OAuth 2.0 & OpenID Connect

Instead of building auth yourself:

  • Password storage (bcrypt, argon2)
  • Password reset flows
  • Multi-factor authentication
  • Social login (Google, GitHub)

Use an Identity Provider (IdP) like Keycloak.

OAuth 2.0 Flow (Authorization Code)

┌──────────┐                                   ┌──────────┐
│  Client  │                                   │   IdP    │
│  (Web)   │                                   │(Keycloak)│
└────┬─────┘                                   └─────┬────┘
     │                                               │
     │ 1. Redirect to /authorize                    │
     │─────────────────────────────────────────────>│
     │                                               │
     │ 2. User logs in, approves                    │
     │                                               │
     │ 3. Redirect back with authorization code     │
     │<─────────────────────────────────────────────│
     │                                               │
     │ 4. Exchange code for token                   │
     │─────────────────────────────────────────────>│
     │                                               │
     │ 5. Access token + Refresh token              │
     │<─────────────────────────────────────────────│
     │                                               │
┌────▼─────┐                                   ┌─────┴────┐
│  Client  │                                   │   IdP    │
│uses token│                                   │          │
└──────────┘                                   └──────────┘

Exercise 1: Integrate Keycloak

Enterprise-Grade Identity Management

Objective: Protect microservices with Keycloak authentication.

Exercise 1: Step 1 - Start Keycloak

Add to docker-compose.yml:

  keycloak:
    image: quay.io/keycloak/keycloak:23.0
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_HTTP_PORT: 8090
    ports:
      - "8090:8090"
    command: start-dev --http-port=8090
docker-compose up -d keycloak

# Open Keycloak Admin
open http://localhost:8090

Exercise 1: Step 2 - Configure Realm

In Keycloak UI:

  1. Create Realm: practicemanager
  2. Create Client:
    • Client ID: practicemanager-api
    • Client Protocol: openid-connect
    • Access Type: bearer-only
  3. Create Roles: DOCTOR, NURSE, ADMIN
  4. Create Users:
    • Username: drsmith
    • Assign role: DOCTOR

Exercise 1: Step 3 - Configure Quarkus OIDC

Update practicemanager/pom.xml:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-oidc</artifactId>
</dependency>

Update application.properties:

# Remove old JWT config, add OIDC
quarkus.oidc.auth-server-url=http://localhost:8090/realms/practicemanager
quarkus.oidc.client-id=practicemanager-api
quarkus.oidc.credentials.secret=<client-secret>

# Enable token verification
quarkus.oidc.token.issuer=http://localhost:8090/realms/practicemanager

Exercise 1: Step 4 - Test with Keycloak

# Get token from Keycloak
TOKEN=$(curl -X POST \
  http://localhost:8090/realms/practicemanager/protocol/openid-connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=practicemanager-api" \
  -d "username=drsmith" \
  -d "password=password" \
  | jq -r '.access_token')

# Use token
curl http://localhost:8080/api/patients/P001 \
  -H "Authorization: Bearer $TOKEN"

Service-to-Service Authentication

Problem:

  • Consultations service calls practicemanager
  • How does practicemanager know it's a legitimate service?

Solutions:

  1. Shared Secret (API Key)
  2. JWT with Service Identity
  3. Mutual TLS (mTLS)

Exercise 2: Service-to-Service JWT

Machine-to-Machine Authentication

Objective: Consultations service authenticates when calling practicemanager.

Exercise 2: Step 1 - Create Service Account in Keycloak

In Keycloak:

  1. Create Client: consultations-service
  2. Set Access Type: confidential
  3. Enable: Service Accounts Enabled
  4. Assign Service Account Role: SERVICE

Exercise 2: Step 2 - Get Service Token

Update consultationsRestPatientClient.java:

import org.eclipse.microprofile.config.inject.ConfigProperty;

@ApplicationScoped
public class RestPatientClient implements PatientClient {

    @ConfigProperty(name = "keycloak.token-url")
    String tokenUrl;

    @ConfigProperty(name = "keycloak.client-id")
    String clientId;

    @ConfigProperty(name = "keycloak.client-secret")
    String clientSecret;

    private String getServiceToken() {
        // Get token using client credentials grant
        Response response = ClientBuilder.newClient()
            .target(tokenUrl)
            .request()
            .post(Entity.form(
                new Form()
                    .param("grant_type", "client_credentials")
                    .param("client_id", clientId)
                    .param("client_secret", clientSecret)
            ));

        JsonObject json = response.readEntity(JsonObject.class);
        return json.getString("access_token");
    }

    @Override
    public Patient getPatientById(String id) {
        String token = getServiceToken();

        return ClientBuilder.newClient()
            .target("http://practicemanager:8080/api/patients/" + id)
            .request()
            .header("Authorization", "Bearer " + token)
            .get(Patient.class);
    }
}

Exercise 2: Step 3 - Verify Service Identity

Update PatientResource.java:

@GET
@Path("/{id}")
@RolesAllowed({"DOCTOR", "NURSE", "SERVICE"})
public Response getPatient(@PathParam("id") String id) {
    if ("SERVICE".equals(jwt.getGroups().iterator().next())) {
        LOG.info("Request from another service");
    }
    return Response.ok(patientService.getPatient(id)).build();
}

Mutual TLS (mTLS)

Problem: How do you trust that a service is who it claims to be?

Solution: Both client and server verify each other's certificates.

Regular TLS: Server proves identity to client
mTLS: Both prove identity to each other

Use Case: Service mesh (Istio, Linkerd) automatically enables mTLS.

Exercise 3: API Key Authentication

Simple Alternative for Internal Services

Objective: Use API keys for service-to-service auth (dev/testing).

Exercise 3: Step 1 - Generate API Key

# Generate random API key
API_KEY=$(openssl rand -hex 32)
echo $API_KEY

Store in consultations/application.properties:

api.key.practicemanager=a1b2c3d4e5f6...

Exercise 3: Step 2 - Send API Key in Header

Update RestPatientClient.java:

@ConfigProperty(name = "api.key.practicemanager")
String apiKey;

@Override
public Patient getPatientById(String id) {
    return ClientBuilder.newClient()
        .target("http://practicemanager:8080/api/patients/" + id)
        .request()
        .header("X-API-Key", apiKey)
        .get(Patient.class);
}

Exercise 3: Step 3 - Validate API Key

Create ApiKeyFilter.java in practicemanager:

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;

@Provider
public class ApiKeyFilter implements ContainerRequestFilter {

    @ConfigProperty(name = "api.keys.allowed")
    List<String> allowedKeys;

    @Override
    public void filter(ContainerRequestContext requestContext) {
        String apiKey = requestContext.getHeaderString("X-API-Key");

        if (apiKey != null && allowedKeys.contains(apiKey)) {
            // Valid API key - allow request
            return;
        }

        // Check for JWT if no API key
        String authHeader = requestContext.getHeaderString("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            // Let JWT filter handle it
            return;
        }

        // No valid auth
        requestContext.abortWith(
            Response.status(401).entity("Unauthorized").build()
        );
    }
}

Security Best Practices

  1. Never Store Secrets in Code
    Use environment variables or secret managers (AWS Secrets Manager, HashiCorp Vault)

  2. Use HTTPS Everywhere
    Even internal service-to-service communication

  3. Rotate Secrets Regularly
    API keys, JWT signing keys, database passwords

  4. Principle of Least Privilege
    Grant minimum permissions needed

  5. Audit Logs
    Log all authentication/authorization events

Exercise 4: Secrets Management

Using Environment Variables Securely

Objective: Never commit secrets to Git.

Exercise 4: Step 1 - Use .env Files

Create practicemanager/.env:

JWT_PRIVATE_KEY_PATH=/secrets/privateKey.pem
KEYCLOAK_CLIENT_SECRET=super-secret-value
DATABASE_PASSWORD=db-password-123
API_KEY=a1b2c3d4e5f6...

Add to .gitignore:

.env
*.pem

Exercise 4: Step 2 - Load in Docker

Update docker-compose.yml:

  practicemanager:
    build: ./practicemanager
    env_file:
      - ./practicemanager/.env
    environment:
      QUARKUS_OIDC_CREDENTIALS_SECRET: ${KEYCLOAK_CLIENT_SECRET}
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}

Exercise 4: Step 3 - Kubernetes Secrets

# Create secret from .env file
kubectl create secret generic practicemanager-secrets \
  --from-env-file=practicemanager/.env

# Or from literals
kubectl create secret generic db-secret \
  --from-literal=password=super-secret

Rate Limiting & Throttling

Prevent abuse and DDoS:

API Gateway Level:

  • 100 requests/minute per IP
  • 1000 requests/hour per user

Service Level:

  • 10 patient registrations/minute
  • 50 document uploads/hour

Implementation: Already covered in API Gateway module (APISIX limit-count plugin).

Input Validation

Never trust user input!

import jakarta.validation.constraints.*;

public class RegisterPatientRequest {
    @NotBlank(message = "First name is required")
    @Size(min = 2, max = 50)
    private String firstName;

    @NotBlank
    @Pattern(regexp = "\\d{3}-\\d{2}-\\d{4}",
             message = "SSN must be format XXX-XX-XXXX")
    private String ssn;

    @Email
    private String email;

    @Past(message = "Date of birth must be in the past")
    private LocalDate dateOfBirth;
}

SQL Injection Prevention

Bad (Vulnerable):

String query = "SELECT * FROM patients WHERE ssn = '" + ssn + "'";
// Attacker sends: ' OR '1'='1

Good (Parameterized):

String query = "SELECT * FROM patients WHERE ssn = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, ssn);

Quarkus Panache handles this automatically!

OWASP Top 10 for Microservices

  1. Broken Authentication → Use OAuth2/OIDC
  2. Broken Authorization → Role-based access control
  3. Injection (SQL, NoSQL) → Parameterized queries
  4. Sensitive Data Exposure → Encrypt at rest and in transit
  5. XML External Entities (XXE) → Disable XML external entities
  6. Broken Access Control → Validate user permissions
  7. Security Misconfiguration → Harden defaults
  8. Cross-Site Scripting (XSS) → Sanitize outputs
  9. Using Components with Vulnerabilities → Update dependencies
  10. Insufficient Logging → Log auth events, anomalies

Exercise 5: Dependency Scanning

Detect Vulnerable Libraries

Objective: Scan for known vulnerabilities in dependencies.

Exercise 5: Using OWASP Dependency Check

Add to pom.xml:

<plugin>
  <groupId>org.owasp</groupId>
  <artifactId>dependency-check-maven</artifactId>
  <version>9.0.7</version>
  <executions>
    <execution>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Run:

./mvnw dependency-check:check

# View report
open target/dependency-check-report.html

Exercise 5: Using Snyk

# Install Snyk CLI
npm install -g snyk

# Authenticate
snyk auth

# Test for vulnerabilities
snyk test

# Monitor project
snyk monitor

Integrate into CI/CD pipeline to block deployments with critical vulnerabilities.

Compliance & Data Privacy

GDPR (Europe):

  • Right to be forgotten (delete patient data)
  • Data portability (export patient records)
  • Consent management

HIPAA (USA - Healthcare):

  • Encrypt PHI (Protected Health Information)
  • Audit access logs
  • Business Associate Agreements (BAAs)

Implementation:

  • Encrypt SSN, medical records at rest
  • Log all access to patient data
  • Implement data retention policies

Exercise 7: Data Encryption

Encrypt Sensitive Fields

Objective: Encrypt SSN in database.

Exercise 7: Implementation

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

@ApplicationScoped
public class EncryptionService {

    @ConfigProperty(name = "encryption.key")
    String encryptionKey;

    public String encrypt(String data) {
        SecretKeySpec key = new SecretKeySpec(
            encryptionKey.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] encrypted = cipher.doFinal(data.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public String decrypt(String encrypted) {
        SecretKeySpec key = new SecretKeySpec(
            encryptionKey.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] decoded = Base64.getDecoder().decode(encrypted);
        byte[] decrypted = cipher.doFinal(decoded);
        return new String(decrypted);
    }
}

Exercise 7: Use in Repository

@Inject
EncryptionService encryption;

@Override
public Patient save(Patient patient) {
    // Encrypt SSN before saving
    patient.setSsn(encryption.encrypt(patient.getSsn()));
    patients.put(patient.getId(), patient);
    return patient;
}

@Override
public Optional<Patient> findBySsn(String ssn) {
    String encryptedSsn = encryption.encrypt(ssn);
    return patients.values().stream()
        .filter(p -> p.getSsn().equals(encryptedSsn))
        .findFirst()
        .map(p -> {
            // Decrypt for display
            p.setSsn(encryption.decrypt(p.getSsn()));
            return p;
        });
}

Security Checklist

  • [ ] All endpoints require authentication (except health checks)
  • [ ] Use HTTPS in production
  • [ ] Secrets stored in environment variables or secret managers
  • [ ] Input validation on all endpoints
  • [ ] Rate limiting enabled
  • [ ] Dependency scanning in CI/CD
  • [ ] Sensitive data encrypted at rest
  • [ ] Audit logs for all auth events
  • [ ] CORS configured properly
  • [ ] Security headers set (CSP, HSTS, X-Frame-Options)

Summary

  • JWT: Stateless authentication for microservices
  • OAuth2/OIDC: Delegate auth to identity providers (Keycloak)
  • Service-to-Service: API keys, JWT, or mTLS
  • Secrets Management: Never hardcode, use env vars
  • Input Validation: Prevent injection attacks
  • Encryption: Protect sensitive data at rest
  • Compliance: GDPR, HIPAA requirements

Security is not optional - build it in from day one.

Resources

You now have a production-ready microservices architecture!