Skip to content

Commit 15e4ad5

Browse files
Retry getServiceAuth once on DPoP nonce mismatch and bump oauth-client-browser to 0.3.42 so the OAuth client's stored nonce primes successfully on the first request to a previously-unseen PDS origin
1 parent 9afe464 commit 15e4ad5

3 files changed

Lines changed: 119 additions & 16 deletions

File tree

pnpm-lock.yaml

Lines changed: 75 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/lib/auth/service-auth.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,15 @@ export async function getServiceAuthToken(agent: Agent, lxm?: string): Promise<s
115115
return cached.token;
116116
}
117117

118-
// Request new service auth token from PDS
119-
const response = await agent.com.atproto.server.getServiceAuth({
120-
aud: CHIVE_SERVICE_DID,
121-
lxm,
122-
});
118+
// Request new service auth token from PDS.
119+
// The first call against a given PDS can fail with `use_dpop_nonce` when the
120+
// OAuth client's stored nonce is empty or stale for that origin. The
121+
// @atproto/oauth-client-browser fetch handler is supposed to auto-retry once,
122+
// but in practice the retry can leak through as a thrown error -- the
123+
// response *does* return a fresh `DPoP-Nonce`, so a second call succeeds.
124+
// Wrapping in a single explicit retry guarantees the user-visible request
125+
// succeeds when the only obstacle is the nonce-priming step.
126+
const response = await callGetServiceAuthWithNonceRetry(agent, lxm);
123127

124128
const { token } = response.data as ServiceAuthResponse;
125129

@@ -133,6 +137,40 @@ export async function getServiceAuthToken(agent: Agent, lxm?: string): Promise<s
133137
return token;
134138
}
135139

140+
async function callGetServiceAuthWithNonceRetry(
141+
agent: Agent,
142+
lxm?: string
143+
): Promise<{ data: ServiceAuthResponse }> {
144+
try {
145+
return await agent.com.atproto.server.getServiceAuth({
146+
aud: CHIVE_SERVICE_DID,
147+
lxm,
148+
});
149+
} catch (error) {
150+
if (isNonceMismatchError(error)) {
151+
serviceAuthLogger.debug('getServiceAuth hit DPoP nonce mismatch; retrying once', { lxm });
152+
return await agent.com.atproto.server.getServiceAuth({
153+
aud: CHIVE_SERVICE_DID,
154+
lxm,
155+
});
156+
}
157+
throw error;
158+
}
159+
}
160+
161+
function isNonceMismatchError(error: unknown): boolean {
162+
if (!error || typeof error !== 'object') return false;
163+
const message = (error as { message?: string }).message ?? '';
164+
const data = (error as { data?: unknown }).data;
165+
const dataError =
166+
data && typeof data === 'object' ? (data as { error?: string }).error : undefined;
167+
return (
168+
dataError === 'use_dpop_nonce' ||
169+
/use_dpop_nonce/i.test(message) ||
170+
/dpop.*nonce.*mismatch/i.test(message)
171+
);
172+
}
173+
136174
/**
137175
* Clear all cached service auth tokens.
138176
*

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"@atproto/api": "^0.18.3",
2323
"@atproto/common-web": "^0.4.9",
2424
"@atproto/lexicon": "^0.5.2",
25-
"@atproto/oauth-client-browser": "^0.3.37",
25+
"@atproto/oauth-client-browser": "^0.3.42",
2626
"@dnd-kit/core": "^6.3.1",
2727
"@dnd-kit/sortable": "^10.0.0",
2828
"@dnd-kit/utilities": "^3.2.2",

0 commit comments

Comments
 (0)