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)
↓
actor-bff → Forwards user JWT to actor-service
↓
actor-service → Validates user JWT (knows it's from a user)
What the receiving service knows:
- ✅ The token is valid (signed by Cognito)
- ✅ Which user is making the request
- ❌ Which service is making the call (could be actor-bff, could be an attacker with a stolen user token)
- ❌ Whether this is a legitimate service call or a direct user call
The Problem: Missing Service Identity
Scenario 1: Compromised User Token
Attack:
- Attacker steals a user's JWT token (XSS, man-in-the-middle, etc.)
- Attacker directly calls
document-servicewith the stolen user token document-servicevalidates the token → ✅ Valid user tokendocument-serviceprocesses the request → ❌ But it doesn't know this isn't coming fromactor-bff
Current system: Can't distinguish between:
- Legitimate:
actor-bffforwarding user X's token →document-service - Attack: Attacker directly calling
document-servicewith user X's token
Scenario 2: Background Jobs / Scheduled Tasks
Problem:
- You want a service to run a scheduled task (e.g., "clean up old resumes every night")
- The task isn't tied to any specific user
- Current system: Can't make service calls without a user token
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:
- You want
parse-serviceto only accept calls fromdocument-service - Currently: Any service (or attacker) with a valid user token can call it
- You can't say "only document-service is allowed to call parse-service"
Example:
document-service → parse-service ✅ (should work)
actor-bff → 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:
- Each service has a service account in Cognito (separate from user accounts)
- Services authenticate with Cognito using their service credentials
- Services receive service JWTs (different from user JWTs)
- 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
-
Service Identity Verification:
- Receiving service knows which service is calling
- Can implement service-level authorization
- Can audit which services are making calls
-
Background Jobs:
- Services can make calls without user context
- Scheduled tasks can authenticate as the service
- System-level operations don't need user tokens
-
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
-
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:
- Order service forwards user's JWT to inventory service
- Inventory service doesn't know if call is from order-service or an attacker
- Can't implement "only order-service can reserve inventory"
With service accounts:
- Order service authenticates as
order-serviceaccount - Gets service JWT with
service_id: "order-service" - Inventory service validates service JWT
- Inventory service checks: "Is caller order-service?" → ✅ Authorize
Example 2: Scheduled Data Sync
Scenario: Analytics service needs to sync data every hour
Without service accounts:
- Can't make service calls without a user token
- Would need a "system user" account (bad practice)
- User token could expire, breaking scheduled jobs
With service accounts:
- Analytics service authenticates as
analytics-service - Gets long-lived service JWT (or refreshes automatically)
- Can make calls 24/7 without user context
- Scheduled jobs work reliably
Example 3: Microservices Authorization
Scenario: Only specific services should access sensitive endpoints
Without service accounts:
- All services with valid user tokens can call any endpoint
- Can't restrict
admin-serviceendpoints to onlyadmin-service - Security relies on network isolation (not good enough)
With service accounts:
admin-serviceendpoints check: "Is caller admin-service?"- Other services can't call admin endpoints even with valid user tokens
- Fine-grained service-level authorization
Implementation details
Service Accounts
Service accounts are created in Cognito using the seed script (scripts/aws/sandbox-cognito-seed.sh):
- Username format:
service-{service-name}(e.g.,service-document-service) - Custom attribute:
custom:service_id={service-name} - Credentials stored in AWS Parameter Store and
.envrc
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:
ServiceAuthenticationProvider- Authenticate servicesServiceTokenProvider- Get and cache service JWTs
Infrastructure:
CognitoServiceAuthenticationProvider- Authenticates services with CognitoCachingServiceTokenProvider- Caches and refreshes service JWTs automatically
Infrastructure:
ServiceTokenClientRequestFilter- Automatically injects service JWTs into outgoing REST client callsServiceTokenAuthorizationInterceptor- Enforces@AllowedServicesrestrictions@AllowedServices- Annotation to restrict endpoints to specific services
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-serviceThese 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:
- Service-to-service authentication ✅
- Service-level authorization ✅
- Automatic token injection ✅
- Token caching and refresh ✅
Potential future enhancements:
- Mutual TLS (mTLS) for additional transport security
- Service mesh integration (Istio, Linkerd)
- Certificate-based service authentication
- Network policy enforcement
Updated 10 days ago