Gateway: ES256 developer-token signer with expiry refresh #3

Closed
opened 2026-08-09 20:31:47 +00:00 by hermes · 1 comment
Owner

Context

Gateway service (see #18). The gateway signs the MusicKit developer JWT that
both the login page and Apple API calls need.

Task

app/apple_auth.py: a DevTokenSigner (or similar) that

  • reads the .p8 key (PEM EC P-256) and signs
    {"iss": team_id, "iat": now, "exp": now + 15777000} with ES256 via
    PyJWT, header {"kid": key_id} — jwt.encode(payload, key_pem, algorithm="ES256", headers={"kid": key_id})
  • caches the signed token in memory and regenerates when exp is less than
    30 days away (Apple caps lifetime at ~6 months)
  • raises a clear error if the key file is unreadable or the
    team/key ids are missing.

Never log the token. Thread-safety: the app is async (FastAPI); protect the
cache with a lock or make signing cheap enough to do per-call.

Acceptance criteria

  • Unit test with a generated test EC key: token decodes, iss/kid are
    correct, exp - iat == 15777000.
  • Cache test: within 30 days of expiry the same token is returned without
    re-signing; past the threshold a new token is minted.
## Context Gateway service (see #18). The gateway signs the MusicKit developer JWT that both the login page and Apple API calls need. ## Task `app/apple_auth.py`: a `DevTokenSigner` (or similar) that - reads the `.p8` key (PEM EC P-256) and signs `{"iss": team_id, "iat": now, "exp": now + 15777000}` with ES256 via PyJWT, header `{"kid": key_id}` — `jwt.encode(payload, key_pem, algorithm="ES256", headers={"kid": key_id})` - caches the signed token in memory and regenerates when `exp` is less than 30 days away (Apple caps lifetime at ~6 months) - raises a clear error if the key file is unreadable or the team/key ids are missing. Never log the token. Thread-safety: the app is async (FastAPI); protect the cache with a lock or make signing cheap enough to do per-call. ## Acceptance criteria - Unit test with a generated test EC key: token decodes, `iss`/`kid` are correct, `exp - iat == 15777000`. - Cache test: within 30 days of expiry the same token is returned without re-signing; past the threshold a new token is minted.
Author
Owner

Done in coop/apple-music-gateway (commit 37d3fa7): app/apple_auth.py — DevTokenSigner signs ES256 JWT (iss=team id, iat=now, exp=now+15777000, kid header) from the .p8 key; in-memory cache under a lock re-signs only when expiry is <30 days away; clear DevTokenError for missing ids / unreadable key / bad key material; token never logged, excluded from repr. 22 tests pass with a real generated P-256 key (verified decode incl. signature, iss/kid, exp-iat, cache/refresh boundaries, concurrency).

Done in `coop/apple-music-gateway` (commit `37d3fa7`): `app/apple_auth.py` — `DevTokenSigner` signs ES256 JWT (iss=team id, iat=now, exp=now+15777000, kid header) from the .p8 key; in-memory cache under a lock re-signs only when expiry is <30 days away; clear `DevTokenError` for missing ids / unreadable key / bad key material; token never logged, excluded from repr. 22 tests pass with a real generated P-256 key (verified decode incl. signature, iss/kid, exp-iat, cache/refresh boundaries, concurrency).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coop/beets-appleplaylists#3
No description provided.