# Service-to-Service Authentication Patterns

## Token Relay (Propagate User Token)

Concept: Pass the original user's access token through the entire call chain.

Use Case: When downstream services need to know WHO the end-user is and make authorization decisions based on that

{% @mermaid/diagram content="sequenceDiagram
participant Client
participant Gateway as API Gateway
participant VideoService as Video Service
participant UserService as User Service
participant SocialService as Social Service
participant Keycloak

```
Client->>Gateway: POST /api/videos/upload<br/>Authorization: Bearer USER_TOKEN_ABC

Gateway->>Gateway: Validate USER_TOKEN_ABC

Gateway->>VideoService: POST /videos/upload<br/>Authorization: Bearer USER_TOKEN_ABC<br/>X-User-ID: user123<br/>X-Forwarded-For: client-ip

Note over VideoService: Process video upload

VideoService->>VideoService: Extract user_id from token<br/>user_id = "user123"

rect rgb(255, 240, 240)
    Note over VideoService,UserService: Need to get user profile info
    VideoService->>UserService: GET /internal/users/user123<br/>Authorization: Bearer USER_TOKEN_ABC<br/>X-Service-Name: video-service
    
    UserService->>UserService: Validate JWT signature<br/>Check if token has scope "profile:read"<br/>Check if user_id matches token
    
    UserService->>UserService: Authorization check:<br/>Can this user access their own profile? ✓
    
    UserService->>VideoService: 200 OK<br/>{<br/>  "id": "user123",<br/>  "username": "john_doe",<br/>  "tier": "premium"<br/>}
end

rect rgb(240, 255, 240)
    Note over VideoService,SocialService: Need to get follower count
    VideoService->>SocialService: GET /internal/users/user123/followers/count<br/>Authorization: Bearer USER_TOKEN_ABC
    
    SocialService->>SocialService: Validate JWT<br/>Check token has scope "social:read"
    
    SocialService->>VideoService: 200 OK<br/>{"count": 15000}
end

VideoService->>Gateway: 201 Created<br/>{video_id: "vid789"}
Gateway->>Client: 201 Created" %}
```

✅ PROS:

* Maintains user context across all services
* Simple to implement
* Each service can check user permissions
* Audit trail shows real user
* No additional tokens needed

❌ CONS:

* User token might expire mid-request chain
* All services must validate same token (overhead)
* Tight coupling to user token format
* No differentiation between user and service calls
* Token might have unnecessary scopes for internal calls

🎯 BEST FOR:

* Simple architectures
* When you need user context everywhere
* Synchronous request chains

## Service Account Token (Client Credentials)

Use Case: When service needs to act on its own behalf, not on behalf of a user.

{% @mermaid/diagram content="sequenceDiagram
participant Client
participant Gateway as API Gateway
participant VideoService as Video Service
participant AnalyticsService as Analytics Service
participant Keycloak
participant Cache as Redis Cache

```
Client->>Gateway: POST /api/videos/upload<br/>Authorization: Bearer USER_TOKEN

Gateway->>VideoService: POST /videos/upload<br/>Authorization: Bearer USER_TOKEN<br/>X-User-ID: user123

Note over VideoService: Process video, then log analytics

rect rgb(240, 240, 255)
    Note over VideoService,Keycloak: Get Service-to-Service Token
    
    VideoService->>Cache: Check cached service token
    
    alt Token not in cache or expired
        VideoService->>Keycloak: POST /token<br/>grant_type=client_credentials<br/>&client_id=video-service<br/>&client_secret=SERVICE_SECRET<br/>&scope=analytics:write
        
        Keycloak->>Keycloak: Authenticate service account
        
        Keycloak->>VideoService: 200 OK<br/>{<br/>  "access_token": "SERVICE_TOKEN_XYZ",<br/>  "token_type": "Bearer",<br/>  "expires_in": 3600,<br/>  "scope": "analytics:write"<br/>}
        
        VideoService->>Cache: Cache token (TTL: 50 min)
    else Token in cache
        Cache->>VideoService: Return cached SERVICE_TOKEN_XYZ
    end
end

rect rgb(240, 255, 240)
    Note over VideoService,AnalyticsService: Call with Service Token
    
    VideoService->>AnalyticsService: POST /internal/events<br/>Authorization: Bearer SERVICE_TOKEN_XYZ<br/>X-Original-User-ID: user123<br/>{<br/>  "event": "video_uploaded",<br/>  "user_id": "user123",<br/>  "video_id": "vid789"<br/>}
    
    AnalyticsService->>AnalyticsService: Validate SERVICE_TOKEN_XYZ<br/>Check scope: analytics:write ✓<br/>Check client_id: video-service ✓
    
    AnalyticsService->>AnalyticsService: Trust X-Original-User-ID header<br/>(from authenticated service)
    
    AnalyticsService->>VideoService: 201 Created
end

VideoService->>Gateway: 201 Created
Gateway->>Client: 201 Created
```

" %}

Pros:

✅ Service acts with its own identity\
✅ Narrowly scoped permissions\
✅ Longer-lived tokens (can cache)\
✅ Better security isolation\
Cons:

❌ Loses original user context (must pass separately)\
❌ Need to manage service credentials\
❌ Two tokens to validate

## Mutual TLS (mTLS)

**Use Case**: High-security internal communication without token overhead.

{% @mermaid/diagram content="sequenceDiagram
participant VideoService as Video Service<br/>(Client Cert)
participant UserService as User Service<br/>(Server Cert)
participant CA as Certificate Authority

```
Note over VideoService,UserService: Initial TLS Handshake

VideoService->>UserService: 1. ClientHello<br/>(TLS 1.3, cipher suites)

UserService->>VideoService: 2. ServerHello<br/>+ Server Certificate<br/>+ Request Client Certificate

VideoService->>VideoService: 3. Validate Server Certificate<br/>- Check CN=user-service.internal<br/>- Verify signature by CA<br/>- Check expiration

VideoService->>UserService: 4. Client Certificate<br/>(CN=video-service.internal)

UserService->>UserService: 5. Validate Client Certificate<br/>- Check CN=video-service.internal<br/>- Verify signature by CA<br/>- Check against allowed services

UserService->>VideoService: 6. TLS Handshake Complete<br/>(Encrypted channel established)

rect rgb(240, 255, 240)
    Note over VideoService,UserService: Authenticated & Encrypted Communication
    
    VideoService->>UserService: GET /internal/users/user123<br/>X-Original-User-ID: user123<br/>(over mTLS)
    
    UserService->>UserService: Already authenticated via mTLS<br/>Client is: video-service ✓
    
    UserService->>VideoService: 200 OK (encrypted)
end" %}
```

{% @mermaid/diagram content="graph TB
subgraph "Certificate Authority"
CA\[Internal CA]
Root\[Root Certificate]
CA --> Root
end

```
subgraph "Service Certificates"
    VS_Cert[video-service.internal<br/>Client + Server Cert]
    US_Cert[user-service.internal<br/>Client + Server Cert]
    SS_Cert[social-service.internal<br/>Client + Server Cert]
    
    CA -->|Signs| VS_Cert
    CA -->|Signs| US_Cert
    CA -->|Signs| SS_Cert
end

subgraph "Runtime"
    VS[Video Service] -->|Presents| VS_Cert
    US[User Service] -->|Presents| US_Cert
    SS[Social Service] -->|Presents| SS_Cert
    
    VS -.->|mTLS| US
    VS -.->|mTLS| SS
    US -.->|mTLS| SS
end

style CA fill:#ffcccc
style VS_Cert fill:#ccffcc
style US_Cert fill:#ccffcc
style SS_Cert fill:#ccffcc
```

" %}

Pros:

✅ No token management overhead\
✅ Network-level authentication\
✅ High performance (TLS termination)\
✅ Works well with service mesh\
Cons:

❌ Complex certificate management\
❌ Certificate rotation challenges\
❌ No user context (must pass separately)\
❌ All-or-nothing (service either trusted or not)

## Detailed Video Upload Flow with Async Components

{% @mermaid/diagram content="sequenceDiagram
participant Client
participant Gateway as API Gateway
participant VideoService as Video Service
participant UserService as User Service
participant SocialService as Social Service
participant Kafka as Kafka Message Queue
participant NotificationWorker as Notification Worker
participant AnalyticsWorker as Analytics Worker
participant TranscodeWorker as Transcode Worker
participant NotificationService as Notification Service
participant AnalyticsService as Analytics Service
participant TranscodeService as Transcode Service
participant Keycloak
participant Redis as Redis Cache
participant S3 as S3 Storage
participant DB as PostgreSQL

```
Note over Client,DB: PHASE 1: SYNCHRONOUS REQUEST HANDLING

Client->>Gateway: POST /api/videos/upload<br/>Authorization: Bearer USER_TOKEN_ABC<br/>Content-Type: multipart/form-data<br/>(video file + metadata)

Gateway->>Gateway: 1. Validate USER_TOKEN_ABC signature<br/>2. Check expiration<br/>3. Extract user_id: "user123"

rect rgb(255, 240, 240)
    Note over Gateway,VideoService: mTLS Handshake
    Gateway->>VideoService: TLS ClientHello<br/>(Gateway presents client cert)
    VideoService->>Gateway: TLS ServerHello<br/>(VideoService presents server cert)
    Note over Gateway,VideoService: Mutual certificate validation ✓<br/>Encrypted channel established
end

Gateway->>VideoService: POST /internal/videos/upload<br/>Authorization: Bearer USER_TOKEN_ABC<br/>X-User-ID: user123<br/>X-Request-ID: req-uuid-001<br/>X-Client-IP: 192.168.1.100<br/>(over encrypted mTLS channel)

VideoService->>VideoService: Extract and re-validate USER_TOKEN_ABC<br/>(defense in depth)

rect rgb(240, 255, 240)
    Note over VideoService,UserService: Check User Profile (Synchronous)
    
    VideoService->>Redis: GET user:user123:profile
    
    alt Cache miss
        rect rgb(255, 240, 240)
            Note over VideoService,UserService: mTLS Handshake
            VideoService->>UserService: TLS handshake
        end
        
        VideoService->>UserService: GET /internal/users/user123/profile<br/>Authorization: Bearer USER_TOKEN_ABC<br/>X-Request-ID: req-uuid-001<br/>X-Calling-Service: video-service<br/>(over mTLS)
        
        UserService->>UserService: 1. Validate mTLS cert (CN=video-service) ✓<br/>2. Validate USER_TOKEN signature ✓<br/>3. Check token.user_id == path.user_id ✓<br/>4. Check token.scope contains "profile:read" ✓
        
        UserService->>DB: SELECT * FROM users WHERE id='user123'
        DB->>UserService: User data
        
        UserService->>VideoService: 200 OK<br/>{<br/>  "id": "user123",<br/>  "username": "john_doe",<br/>  "tier": "premium",<br/>  "storage_used": "45GB",<br/>  "storage_limit": "100GB",<br/>  "status": "active"<br/>}
        
        VideoService->>Redis: SET user:user123:profile (TTL: 5min)
    else Cache hit
        Redis->>VideoService: Cached user profile
    end
end

VideoService->>VideoService: Business logic checks:<br/>1. storage_used + video_size < storage_limit ✓<br/>2. user.status == "active" ✓<br/>3. user.tier allows this resolution ✓

rect rgb(240, 240, 255)
    Note over VideoService,SocialService: Check User Not Banned (Synchronous)
    
    rect rgb(255, 240, 240)
        Note over VideoService,SocialService: mTLS Handshake
        VideoService->>SocialService: TLS handshake
    end
    
    VideoService->>SocialService: GET /internal/users/user123/moderation-status<br/>Authorization: Bearer USER_TOKEN_ABC<br/>X-Request-ID: req-uuid-001<br/>(over mTLS)
    
    SocialService->>SocialService: Validate mTLS + Token
    SocialService->>DB: SELECT banned FROM user_moderation WHERE user_id='user123'
    DB->>SocialService: banned = false
    
    SocialService->>VideoService: 200 OK {"banned": false, "strikes": 0}
end

VideoService->>VideoService: Generate video_id: "vid-789"<br/>Generate upload URL

VideoService->>S3: PUT /videos/raw/vid-789.mp4<br/>(upload raw video file)
S3->>VideoService: Upload complete

VideoService->>DB: INSERT INTO videos<br/>(id, user_id, status, raw_path)<br/>VALUES ('vid-789', 'user123', 'processing', 's3://raw/vid-789.mp4')
DB->>VideoService: Insert OK

Note over VideoService,Kafka: PHASE 2: ASYNC MESSAGE PUBLISHING

rect rgb(255, 255, 240)
    Note over VideoService,Kafka: Publish Events (Fire-and-Forget)
    
    VideoService->>VideoService: Get or request SERVICE_TOKEN from cache
    
    alt Service token not in cache
        VideoService->>Keycloak: POST /realms/tiktok/protocol/openid-connect/token<br/>grant_type=client_credentials<br/>client_id=video-service<br/>client_secret=SECRET_XYZ<br/>scope=events:publish
        
        Keycloak->>Keycloak: Authenticate video-service<br/>Check client credentials
        
        Keycloak->>VideoService: 200 OK<br/>{<br/>  "access_token": "SERVICE_TOKEN_XYZ",<br/>  "token_type": "Bearer",<br/>  "expires_in": 3600,<br/>  "scope": "events:publish"<br/>}
        
        VideoService->>Redis: SET service:video-service:token<br/>VALUE: SERVICE_TOKEN_XYZ<br/>TTL: 3500 (expires before token)
    else Service token in cache
        Redis->>VideoService: SERVICE_TOKEN_XYZ
    end
    
    par Publish to Multiple Topics
        VideoService->>Kafka: Publish to topic: video.uploaded<br/>{<br/>  "event_id": "evt-001",<br/>  "event_type": "video.uploaded",<br/>  "video_id": "vid-789",<br/>  "user_id": "user123",<br/>  "timestamp": "2024-01-15T10:30:00Z",<br/>  "metadata": {<br/>    "duration": 45,<br/>    "size_bytes": 52428800,<br/>    "format": "mp4"<br/>  },<br/>  "auth": {<br/>    "service_token": "SERVICE_TOKEN_XYZ",<br/>    "original_user_id": "user123",<br/>    "request_id": "req-uuid-001"<br/>  }<br/>}
        
        VideoService->>Kafka: Publish to topic: video.transcode.requested<br/>{<br/>  "event_id": "evt-002",<br/>  "video_id": "vid-789",<br/>  "user_id": "user123",<br/>  "source_path": "s3://raw/vid-789.mp4",<br/>  "resolutions": ["1080p", "720p", "480p"],<br/>  "auth": {<br/>    "service_token": "SERVICE_TOKEN_XYZ",<br/>    "original_user_id": "user123"<br/>  }<br/>}
        
        VideoService->>Kafka: Publish to topic: analytics.events<br/>{<br/>  "event_id": "evt-003",<br/>  "event_type": "video_upload",<br/>  "user_id": "user123",<br/>  "video_id": "vid-789",<br/>  "timestamp": "2024-01-15T10:30:00Z",<br/>  "properties": {<br/>    "duration": 45,<br/>    "file_size": 52428800<br/>  },<br/>  "auth": {<br/>    "service_token": "SERVICE_TOKEN_XYZ"<br/>  }<br/>}
    end
    
    Kafka->>VideoService: All messages acknowledged
end

VideoService->>Gateway: 201 Created<br/>{<br/>  "video_id": "vid-789",<br/>  "status": "processing",<br/>  "message": "Video uploaded successfully"<br/>}

Gateway->>Client: 201 Created

Note over Kafka,DB: PHASE 3: ASYNC MESSAGE CONSUMPTION & PROCESSING

rect rgb(240, 255, 255)
    Note over TranscodeWorker,TranscodeService: Transcode Processing
    
    TranscodeWorker->>Kafka: Poll topic: video.transcode.requested<br/>(consumer group: transcode-workers)
    Kafka->>TranscodeWorker: Message: evt-002
    
    TranscodeWorker->>TranscodeWorker: Extract SERVICE_TOKEN from message
    
    TranscodeWorker->>TranscodeWorker: Validate SERVICE_TOKEN:<br/>1. Check signature<br/>2. Check expiration<br/>3. Verify issuer
    
    alt Token expired (edge case - message delayed)
        TranscodeWorker->>Keycloak: POST /token<br/>grant_type=client_credentials<br/>client_id=transcode-worker
        Keycloak->>TranscodeWorker: New SERVICE_TOKEN
    end
    
    rect rgb(255, 240, 240)
        Note over TranscodeWorker,TranscodeService: mTLS Handshake
        TranscodeWorker->>TranscodeService: TLS handshake
    end
    
    TranscodeWorker->>TranscodeService: POST /internal/transcode/jobs<br/>Authorization: Bearer SERVICE_TOKEN_XYZ<br/>X-Original-User: user123<br/>X-Request-ID: req-uuid-001<br/>X-Consumer-Group: transcode-workers<br/>{<br/>  "video_id": "vid-789",<br/>  "source": "s3://raw/vid-789.mp4",<br/>  "outputs": ["1080p", "720p", "480p"]<br/><br/>(over mTLS)
    
    TranscodeService->>TranscodeService: Validate:<br/>1. mTLS cert (CN=transcode-worker) ✓<br/>2. SERVICE_TOKEN signature ✓<br/>3. SERVICE_TOKEN scope: "transcode:write" ✓
    
    TranscodeService->>S3: GET s3://raw/vid-789.mp4
    S3->>TranscodeService: Raw video file
    
    TranscodeService->>TranscodeService: Transcode to multiple resolutions<br/>(CPU-intensive work)
    
    TranscodeService->>S3: PUT s3://videos/vid-789/1080p.mp4
    TranscodeService->>S3: PUT s3://videos/vid-789/720p.mp4
    TranscodeService->>S3: PUT s3://videos/vid-789/480p.mp4
    
    TranscodeService->>DB: UPDATE videos<br/>SET status='ready',<br/>    paths='{...}'<br/>WHERE id='vid-789'
    
    TranscodeService->>Kafka: Publish to topic: video.transcode.completed<br/>{<br/>  "video_id": "vid-789",<br/>  "status": "completed",<br/>  "auth": {"service_token": "..."}  <br/>}
    
    TranscodeService->>TranscodeWorker: 200 OK
    TranscodeWorker->>Kafka: Commit offset (message processed)
end

rect rgb(255, 240, 255)
    Note over NotificationWorker,NotificationService: Notification Processing
    
    NotificationWorker->>Kafka: Poll topic: video.uploaded<br/>(consumer group: notification-workers)
    Kafka->>NotificationWorker: Message: evt-001
    
    NotificationWorker->>NotificationWorker: Validate SERVICE_TOKEN from message
    
    NotificationWorker->>NotificationWorker: Extract user_id: "user123"<br/>Need to get user's followers
    
    alt Need fresh service token
        NotificationWorker->>Keycloak: POST /token<br/>grant_type=client_credentials<br/>client_id=notification-worker<br/>scope=social:read notifications:send
        Keycloak->>NotificationWorker: NEW_SERVICE_TOKEN_AAA
    end
    
    rect rgb(255, 240, 240)
        Note over NotificationWorker,SocialService: mTLS to Social Service
        NotificationWorker->>SocialService: TLS handshake
    end
    
    NotificationWorker->>SocialService: GET /internal/users/user123/followers<br/>Authorization: Bearer NEW_SERVICE_TOKEN_AAA<br/>X-Original-User: user123<br/>X-Purpose: notification<br/>(over mTLS)
    
    SocialService->>SocialService: Validate:<br/>1. mTLS cert (CN=notification-worker) ✓<br/>2. SERVICE_TOKEN scope: "social:read" ✓
    
    Note over SocialService: CANNOT use original USER_TOKEN here!<br/>User token only authorizes access to user123's data,<br/>NOT to read followers list in background job.<br/>Service token authorizes the WORKER to read this data.
    
    SocialService->>DB: SELECT follower_id FROM followers<br/>WHERE following_id='user123'<br/>AND notifications_enabled=true
    DB->>SocialService: [user456, user789, user101, ...]
    
    SocialService->>NotificationWorker: 200 OK<br/>{<br/>  "followers": ["user456", "user789", "user101"],<br/>  "count": 15000<br/>}
    
    rect rgb(255, 240, 240)
        Note over NotificationWorker,NotificationService: mTLS to Notification Service
        NotificationWorker->>NotificationService: TLS handshake
    end
    
    NotificationWorker->>NotificationService: POST /internal/notifications/bulk<br/>Authorization: Bearer NEW_SERVICE_TOKEN_AAA<br/>{<br/>  "type": "new_video",<br/>  "from_user": "user123",<br/>  "to_users": ["user456", "user789", ...],<br/>  "data": {<br/>    "video_id": "vid-789",<br/>    "message": "john_doe posted a new video"<br/>  }<br/>}<br/>(over mTLS)
    
    NotificationService->>NotificationService: Validate mTLS + SERVICE_TOKEN<br/>scope: "notifications:send" ✓
    
    NotificationService->>NotificationService: Queue push notifications<br/>to 15,000 users
    
    NotificationService->>NotificationWorker: 202 Accepted
    NotificationWorker->>Kafka: Commit offset
end

rect rgb(240, 255, 240)
    Note over AnalyticsWorker,AnalyticsService: Analytics Processing
    
    AnalyticsWorker->>Kafka: Poll topic: analytics.events<br/>(consumer group: analytics-workers)
    Kafka->>AnalyticsWorker: Message: evt-003
    
    AnalyticsWorker->>AnalyticsWorker: Validate SERVICE_TOKEN
    
    alt Batch multiple events
        AnalyticsWorker->>Kafka: Poll more messages (batch of 100)
        Kafka->>AnalyticsWorker: 99 more events
    end
    
    AnalyticsWorker->>AnalyticsWorker: Get/refresh service token
    
    rect rgb(255, 240, 240)
        Note over AnalyticsWorker,AnalyticsService: mTLS Handshake
        AnalyticsWorker->>AnalyticsService: TLS handshake
    end
    
    AnalyticsWorker->>AnalyticsService: POST /internal/events/batch<br/>Authorization: Bearer SERVICE_TOKEN_BBB<br/>{<br/>  "events": [<br/>    {<br/>      "type": "video_upload",<br/>      "user_id": "user123",<br/>      "video_id": "vid-789",<br/>      "timestamp": "...",<br/>      "properties": {...}<br/>    },<br/>    ... (99 more events)<br/>  ]<br/>}<br/>(over mTLS)
    
    AnalyticsService->>AnalyticsService: Validate mTLS + SERVICE_TOKEN<br/>scope: "analytics:write" ✓
    
    AnalyticsService->>DB: BATCH INSERT INTO events_log<br/>(100 events)
    
    AnalyticsService->>AnalyticsWorker: 201 Created
    AnalyticsWorker->>Kafka: Commit offset (batch)
end" %}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://amartyushov.gitbook.io/tech/protocols/oauth-2.0/service-to-service-authentication-patterns.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
