Backend-to-Backend Authentication¶
This guide covers how backends authenticate with each other using GCP service accounts and OAuth.
Overview¶
Backend-to-backend communication uses service account OAuth - the same authentication mechanism used by humans, but with machine credentials instead of user credentials.
sequenceDiagram
participant SA as Service Account<br/>(Backend/Cron)
participant AWS as AWS Secrets Manager
participant GCP as Google OAuth
participant API as Alan Backend
Note over SA: Needs to authenticate
SA->>AWS: 1. Fetch JSON key file
AWS-->>SA: Service account credentials
SA->>SA: 2. Create & sign JWT assertion<br/>(using private key)
SA->>GCP: 3. Token request<br/>(signed JWT assertion)
GCP-->>SA: 4. Access token
SA->>API: 5. Request with Bearer token
API->>GCP: Validate token (cached 1h)
API-->>SA: 6. Authenticated response
How It Works¶
1. Service Account Credentials¶
Service accounts use a JSON key file stored in AWS Secrets Manager. The key contains:
- Service account email (e.g.,
my-service@project.iam.gserviceaccount.com) - Private key for signing JWT assertions
- Project and client metadata
2. OAuth Token Exchange¶
The service account signs a JWT assertion with its private key and exchanges it with Google for an access token:
from google.auth.transport.requests import Request
from google.oauth2 import service_account
credentials = service_account.Credentials.from_service_account_info(
service_account_credentials,
scopes=["https://www.googleapis.com/auth/userinfo.email"],
)
credentials.refresh(Request())
access_token = credentials.token
Source: shared/cli/helpers/auth.py:184-198
3. Request Authentication¶
The access token is sent as a Bearer token in the Authorization header:
4. Backend Validation¶
The receiving backend validates the token with Google and extracts the service account email:
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
credentials = Credentials(access_token)
user_info_service = build("oauth2", "v2", credentials=credentials)
user_info = user_info_service.userinfo().get().execute()
email = user_info["email"] # e.g., "my-service@project.iam.gserviceaccount.com"
Source: shared/iam/auth_context/providers/service_account.py:31-41
Backend-specific token handling
EU Tools uses ServiceAccountAuthContextProvider which validates the Google access token directly on each request (cached for 1 hour to avoid round-trips to Google). No Alan-minted token is issued.
Other backends (FR, ES, BE, CA APIs) have an additional step: the service account calls /oauth/service-authenticated to exchange the Google token for an Alan-minted JWT cookie, which is then used for subsequent requests.
This inconsistency will be streamlined in a future iteration.
flowchart LR
subgraph EU Tools
A1[Google Token] --> B1[Validate with Google<br/>cached 1h]
end
subgraph Other Backends
A2[Google Token] --> B2["oauth/service-authenticated"]
B2 --> C2[Alan JWT Cookie]
C2 --> D2[Subsequent requests]
end
ServiceAccountAuthContextProvider¶
The ServiceAccountAuthContextProvider handles authentication for incoming requests from service accounts.
Configuration¶
Register the provider in your app's authentication chain:
from shared.iam.auth_context.providers.service_account import ServiceAccountAuthContextProvider
api.register_default_auth_context_providers(
ZeroTrustAuthContextProvider(Alaner, ExternalUser),
ServiceAccountAuthContextProvider(ServiceAccount),
)
How It Works¶
flowchart TD
A[Incoming Request] --> B{Authorization header?}
B -->|No| C[Next Provider]
B -->|Yes| D[Extract access token]
D --> E[Validate with Google OAuth]
E --> F{Email ends with<br/>.gserviceaccount.com?}
F -->|No| G[403 Forbidden]
F -->|Yes| H[Look up ServiceAccount]
H --> I{Found?}
I -->|No| G
I -->|Yes| J[set_auth_context]
J --> K[Request Authenticated]
Implementation¶
class ServiceAccountAuthContextProvider(AuthContextProvider):
def __init__(self, *auth_principal_types: type[AuthPrincipal]):
self.auth_principal_types = auth_principal_types
def will_handle_request(self) -> bool:
return "authorization" in request.headers
@cached_for(hours=1)
def get_email_from_access_token(self, access_token: str) -> str:
# Validates token with Google and returns email
...
def set_auth_context_from_request(self) -> None:
access_token = get_access_token()
email = self.get_email_from_access_token(access_token)
if not email.endswith(".gserviceaccount.com"):
abort(403, message="Token not from service account")
service_account = self.get_auth_principal_from_email(email, *self.auth_principal_types)
if not service_account:
abort(403, message="Invalid email")
set_auth_context(
real_principal=service_account,
cloudflare_zerotrust_identity=get_zerotrust_identity(),
)
Source: shared/iam/auth_context/providers/service_account.py
Token Caching
Token validation results are cached for 1 hour to avoid excessive calls to Google's API.
Service Account Setup¶
Prerequisites¶
Each service account needs:
- GCP Service Account - Created in Google Cloud Console
- JSON Key File - Stored in AWS Secrets Manager
- AlanEmployee Entry - Database record with appropriate roles/permissions
Creating a Service Account¶
- Create the service account in GCP
- Generate a JSON key and store in Secrets Manager
- Create the
AlanEmployeeentry:
# Service accounts have AlanEmployee entries like any backoffice user
alan_employee = AlanEmployee(
alan_email="my-service@project.iam.gserviceaccount.com",
is_active=True,
# ... roles and permissions
)
User/Profile Requirement
Currently, service accounts require a User and Profile entry due to legacy constraints. This will be addressed in a future migration.
Best Practices¶
Do¶
- Use service accounts for all backend-to-backend communication
- Store credentials in AWS Secrets Manager, never in code
- Give service accounts minimal required permissions
- Use webhook authentication for external service callbacks
Don't¶
- Bypass authentication with
requires_auth=False - Share service account credentials between services
- Use certificate-based authentication (deprecated)
- Process webhooks without validating signatures