forge-docs

Service authentication

User login, Cognito form auth, and LinkedIn: USER_AUTHENTICATION.md.

Current architecture (user token forwarding)

How it works now:

Frontend User → Gets JWT token (user identity)
     ↓
backend-actor → Forwards user JWT to actor-service
     ↓
actor-service → Validates user JWT (knows it's from a user)

What the receiving service knows:

The Problem: Missing Service Identity

Scenario 1: Compromised User Token

Attack:

  1. Attacker steals a user’s JWT token (XSS, man-in-the-middle, etc.)
  2. Attacker directly calls document-service with the stolen user token
  3. document-service validates the token → ✅ Valid user token
  4. document-service processes the request → ❌ But it doesn’t know this isn’t coming from backend-actor

Current system: Can’t distinguish between:

Scenario 2: Background Jobs / Scheduled Tasks

Problem:

Example:

// This won't work - no user token available
@Scheduled(every = "1 day")
void cleanupOldResumes() {
    documentService.deleteOldResumes(); // ❌ Needs user JWT, but no user!
}

Scenario 3: Service-Level Authorization

Problem:

Example:

document-service → parse-service ✅ (should work)
backend-actor → parse-service ❌ (should be blocked, but currently can't)
attacker → parse-service ❌ (should be blocked, but currently can't distinguish)

Service-to-Service Authentication Solution

How It Works

Service Accounts:

  1. Each service has a service account in Cognito (separate from user accounts)
  2. Services authenticate with Cognito using their service credentials
  3. Services receive service JWTs (different from user JWTs)
  4. Service JWTs contain service identity claims (e.g., service_id: "document-service")

Service Calls:

document-service → Authenticates with Cognito → Gets service JWT
     ↓
document-service → Calls parse-service with service JWT
     ↓
parse-service → Validates service JWT → Knows it's from document-service
     ↓
parse-service → Can check: "Is the caller document-service?" → ✅ Authorize

What Service Accounts Enable

  1. Service Identity Verification:
    • Receiving service knows which service is calling
    • Can implement service-level authorization
    • Can audit which services are making calls
  2. Background Jobs:
    • Services can make calls without user context
    • Scheduled tasks can authenticate as the service
    • System-level operations don’t need user tokens
  3. Security Isolation:
    • Even if a user token is compromised, attacker can’t impersonate services
    • Services have separate credentials from users
    • Can revoke service credentials independently
  4. Service Mesh / Zero Trust:
    • Every service call is authenticated
    • No “trusted internal network” assumptions
    • Services verify each other’s identity

Real-World Examples

Example 1: E-commerce Platform

Scenario: Order service needs to call inventory service

Without service accounts:

With service accounts:

Example 2: Scheduled Data Sync

Scenario: Analytics service needs to sync data every hour

Without service accounts:

With service accounts:

Example 3: Microservices Authorization

Scenario: Only specific services should access sensitive endpoints

Without service accounts:

With service accounts:

Implementation details

Service Accounts

Service accounts are created in Cognito using the seed script (scripts/aws/sandbox-cognito-seed.sh):

Service Authentication Flow

1. Service starts up
   ↓
2. CachingServiceTokenProvider initializes (if credentials configured)
   ↓
3. Service makes REST client call
   ↓
4. `UserTokenClientRequestFilter` runs → forwards user token if present
   ↓
5. `ServiceTokenClientRequestFilter` runs → adds service token if no user token
   ↓
6. Receiving service receives request with service JWT
   ↓
7. TokenAuthenticationFilter validates token → detects custom:service_id claim
   ↓
8. Stores authenticatedServiceId in request context
   ↓
9. ServiceTokenAuthorizationInterceptor checks @AllowedServices annotation
   ↓
10. Request proceeds if service is authorized ✅

Components

Domain Interfaces:

Infrastructure:

Infrastructure:

Configuration

Services need the following configuration to enable service-to-service authentication:

cognito.service-account.username=service-document-service
cognito.service-account.password=<password-from-parameter-store>
quarkus.application.name=document-service

These are automatically set by the Cognito seed script in AWS Parameter Store and .envrc.

Zero-Trust Architecture

This implementation provides the foundation for zero-trust architecture:

Every service call is authenticated - Services must have valid JWTs ✅ Service identity verification - Receiving services know which service is calling ✅ Service-level authorization - Fine-grained control over which services can access endpoints ✅ No trusted network assumptions - Services verify each other’s identity regardless of network location ✅ Credential isolation - Service credentials are separate from user credentials ✅ Automatic token management - Tokens are cached and refreshed automatically

What’s in place:

Potential future enhancements: