Token Rotation for MRO Data APIs

Machine clients that push maintenance records into an MRO data plane authenticate on every request, so a leaked bearer token is a leaked airworthiness ledger. This page shows how to issue short-lived access tokens, rotate them without dropping in-flight uploads, and revoke a compromised credential in seconds. It sits inside the Secure API Gateway Architecture as the credential-lifecycle control that every other gateway policy depends on, and it maps directly to the recordkeeping-access obligations of FAA 14 CFR § 145.219.

Prerequisites & inputs

Before rotation can be automated, three inputs must exist. First, each machine client — a line-station scanner, a CAMO batch job, an OEM data feed for aircraft N512DL — holds a long-lived registered client credential (a client ID plus a secret or, preferably, a private key) that is never sent on data requests. Second, the gateway exposes a token endpoint that exchanges that credential for a short-lived signed access token (a JWT with a 300-second exp). Third, an authorization store keeps a revocation set keyed by token identifier (jti) so that a still-valid signature can be denied before its natural expiry.

The token itself carries only claims the gateway needs to make an admission decision: sub (the client), scope (for example logbook:write ata:52), jti, iat, and exp. It never carries maintenance data. Persisting the secret material belongs to the encrypted store described in Securing Aviation Maintenance Databases; this stage assumes that store is already sealed and only reads from it.

Rotation lifecycle

Access-token rotationClientauthShort-livedtokenRotate /revokeGatewayverify

Step-by-step implementation

  1. Mint short. Set exp to 300 seconds and never longer than 900. A short window means a stolen token is worthless within minutes and shrinks the revocation set you must carry.
  2. Rotate ahead of expiry. The client refreshes when 80 percent of the lifetime has elapsed, not when a request fails with 401. Reactive rotation on failure loses the request that triggered it; proactive rotation keeps the upload stream unbroken.
  3. Overlap the old and new token. For a short grace period both the outgoing and incoming jti verify successfully, so a request already on the wire under the old token is not rejected mid-flight.
  4. Revoke by jti. When a credential is suspected compromised, add its jti to the revocation set immediately; the gateway checks that set on every request before trusting the signature.
  5. Log every transition. Each mint, rotate, and revoke event is written to the audit trail with a UTC timestamp so a reviewer can reconstruct exactly which credential signed record WO-52-0091.

The following module verifies a token at the gateway edge. It enforces expiry, scope, and the revocation set, and it emits a structured audit line for each decision.

from __future__ import annotations

import hashlib
import logging
from datetime import datetime, timezone
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator

logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
logger = logging.getLogger("mro.gateway.token")


class AccessToken(BaseModel):
    """Decoded gateway access token. Carries no maintenance data.

    Access to maintenance records is restricted per FAA 14 CFR § 145.219,
    which requires the repair station to retain and control such records.
    """

    model_config = ConfigDict(strict=True, extra="forbid")

    jti: str = Field(min_length=8, max_length=64)
    sub: str = Field(pattern=r"^client-[a-z0-9\-]{4,40}$")
    scope: str = Field(min_length=1)
    iat: datetime
    exp: datetime

    @field_validator("iat", "exp", mode="before")
    @classmethod
    def to_utc(cls, v: object) -> datetime:
        if isinstance(v, str):
            return datetime.fromisoformat(v.replace("Z", "+00:00")).astimezone(timezone.utc)
        if isinstance(v, datetime):
            return v.astimezone(timezone.utc)
        raise ValueError("timestamp must be ISO 8601 or datetime")

    def has_scope(self, required: str) -> bool:
        return required in self.scope.split()


class TokenVerifier:
    """Edge verifier: expiry, scope, and revocation-set enforcement."""

    def __init__(self, revoked_jti: set[str], leeway_seconds: int = 5) -> None:
        self._revoked = revoked_jti
        self._leeway = leeway_seconds

    def verify(self, raw_claims: dict, required_scope: str, registration: str) -> bool:
        try:
            token = AccessToken.model_validate(raw_claims)
        except ValidationError as exc:
            logger.error("MALFORMED reg=%s error=%s", registration, exc)
            return False

        now = datetime.now(timezone.utc)
        fingerprint = hashlib.sha256(token.jti.encode("utf-8")).hexdigest()[:12]

        if token.jti in self._revoked:
            logger.warning("REVOKED reg=%s jti_fp=%s", registration, fingerprint)
            return False
        if (now - token.exp).total_seconds() > self._leeway:
            logger.info("EXPIRED reg=%s jti_fp=%s", registration, fingerprint)
            return False
        if not token.has_scope(required_scope):
            logger.warning("SCOPE_DENIED reg=%s need=%s", registration, required_scope)
            return False

        logger.info("ADMITTED reg=%s jti_fp=%s scope=%s", registration, fingerprint, required_scope)
        return True

Worked example

A CAMO batch job authenticates as client-camo-fleet and receives a token with jti t-9f3ac21b, scope logbook:write ata:52, iat 2026-07-15T08:00:00Z, and exp 2026-07-15T08:05:00Z. At 08:04:00Z — 80 percent through the lifetime — it rotates and receives t-1c77de40 with the same scope. A record for part AS8830-14 on N512DL that was already streaming under t-9f3ac21b completes during the five-second overlap and is accepted.

At 08:07:00Z the security team detects the batch host was misconfigured and adds t-9f3ac21b to the revocation set. The next request presenting that jti logs REVOKED reg=N512DL jti_fp=... and returns 403, even though a naive expiry check alone would already have rejected it — the revocation path exists precisely for the case where the token has not yet expired. Passing the fresh token t-1c77de40 with the same claims logs ADMITTED and the door-52 removal record flows through.

Edge cases & compliance gotchas

  • Clock skew across line stations. A scanner whose clock drifts even 30 seconds can present a token the gateway thinks expired. Anchor every iat/exp in ISO 8601 UTC, verify with a small fixed leeway, and never trust the client’s wall clock for the admission decision.
  • Scope creep on rotation. A refreshed token must inherit no more scope than the credential was granted. A rotation endpoint that silently widens scope from ata:52 to ata:* is an authorization escalation; validate the requested scope against the registered grant on every mint.
  • Revocation-set growth. Because tokens are short-lived you can evict a jti from the revocation set once its exp has safely passed. Skipping that eviction lets the set grow without bound and slows the per-request check on the hot path.
  • Rotation must not weaken transport. A token proves what the client may do, not who holds the channel. Pair rotation with the certificate binding in Mutual TLS for Aviation Maintenance Data APIs so that a stolen token replayed from a foreign host still fails the handshake before the verifier ever runs.

Short lifetimes, proactive overlapping rotation, and an authoritative revocation set turn the access token from a standing liability into a disposable, auditable credential. Combined with the encrypted credential store and mutual-TLS channel, token rotation gives an MRO gateway a defensible answer to the question every auditor eventually asks: exactly which client signed this maintenance record, and how quickly could you have shut it off.