Video upload from the Bluesky client failed for every account on this
server. The client reported "The video could not be uploaded to your
hosting provider" and the video service's job record said
pds_upload_failure at 70% -- after transcoding had already succeeded.
The flow has three legs and only the last was broken. The client asks
its own PDS for a service-auth token (getServiceAuth, aud = that
PDS, lxm = com.atproto.repo.uploadBlob), hands it to video.bsky.app,
and the video service presents it back to deposit the transcoded blob.
uploadBlob accepted only the two HS256 shapes this server issues, so
the asymmetrically-signed token was refused at the algorithm check -- a
credential this server had minted itself thirteen seconds earlier,
addressed to itself, scoped to exactly the method being called.
The courier is not the principal, which is what keeps the fix small.
iss is the account, the signature is the account's own #atproto key,
aud is this server. Nothing here decides whether the video service is
trustworthy and there is no allow-list to keep: whoever carries a token
the account asked for may deposit a blob into that account's own
repository, which is what the account authorized by asking.
Four checks, each load-bearing. aud must name this server, or a token
minted for another service is replayable here by whoever received it.
lxm must name this method, because lxm is the grant. The signature
must verify against the key the issuer publishes at #atproto. And the
issuer must be an account this server hosts -- aud only says a token
was addressed here, and without that check a valid token from a DID
hosted elsewhere would open an actor store for an account we do not
have. The blob lands in iss's repository and nowhere else; service
auth carries no sub, so reading one would address the empty string.
Claim checking is space::service_auth::verify_service_auth, already
used by inbound migration and admin-by-DID. Scoped to uploadBlob
alone: importRepo and listMissingBlobs share the session guard and
are deliberately not widened.
Also: a refused access token now says what it was. These failures
produced no log line at all, so an operator had a failing integration, a
healthy server, and nothing connecting the two -- which is most of why
this report needed a job-status lookup and log archaeology to explain.
The debug record names the token's alg, typ, iss, aud, lxm,
iat, exp and jti, never the token, and every value is unverified
by construction. It is off by default because a lapsed token is ordinary
traffic: RUST_LOG=info,atproto_pds::http::auth=debug. The refusal for
an asymmetric token also stops reporting "unsupported alg ES256K", which
read as this server lacking ES256K support rather than as the token
being the wrong kind.
Verified end to end on a live deployment: one uploadBlob of 3,780,988 bytes accepted, and the resulting app.bsky.embed.video post written.