Skip to content

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
Hold "Alt" / "Option" to enable pan & zoom

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:

Authorization: Bearer <access_token>

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
Hold "Alt" / "Option" to enable pan & zoom

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]
Hold "Alt" / "Option" to enable pan & zoom

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:

  1. GCP Service Account - Created in Google Cloud Console
  2. JSON Key File - Stored in AWS Secrets Manager
  3. AlanEmployee Entry - Database record with appropriate roles/permissions

Creating a Service Account

  1. Create the service account in GCP
  2. Generate a JSON key and store in Secrets Manager
  3. Create the AlanEmployee entry:
# 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