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
Step-by-step implementation
- Mint short. Set
expto 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. - 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. - Overlap the old and new token. For a short grace period both the outgoing and incoming
jtiverify successfully, so a request already on the wire under the old token is not rejected mid-flight. - Revoke by
jti. When a credential is suspected compromised, add itsjtito the revocation set immediately; the gateway checks that set on every request before trusting the signature. - 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/expin 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:52toata:*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
jtifrom the revocation set once itsexphas 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.