fix(oauth): accept client_secret_basic on /oauth/token (RFC 6749 §2.3.1)
All checks were successful
Deploy to Production / deploy (push) Successful in 1m21s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m21s
Sovereign-audit Phase 3 caught the next layer of the same bug: form-urlencoded parsing now works, but the AS metadata advertises both `client_secret_basic` and `client_secret_post` while the handler only read credentials from the body. Claude Desktop (and most OAuth SDKs) prefer Basic auth, so every token exchange landed at "401 invalid_client" — visible in prod logs as POST /oauth/token from 160.79.106.37 returning 401 in <4ms (failing the missing-secret check). Parse Authorization: Basic header, decode base64, percent-decode each side (RFC 6749 §2.3.1 mandates pct-encoding of user/pass before the base64 step), and treat the resulting credentials as if they came from the body. Header takes precedence when both are present.
This commit is contained in:
parent
44cebc9fd8
commit
b421457010
@ -279,6 +279,33 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
|
||||
const parsed = Body.safeParse(body);
|
||||
if (!parsed.success) return reply.code(400).send({ error: 'invalid_request' });
|
||||
|
||||
// RFC 6749 §2.3.1: confidential clients MAY authenticate via HTTP Basic
|
||||
// (preferred) OR via client_id+client_secret in the request body. Our AS
|
||||
// metadata advertises both `client_secret_basic` and `client_secret_post`,
|
||||
// and Claude Desktop / most SDKs default to Basic. Without parsing the
|
||||
// header here every Basic-style POST hit the "missing client_secret"
|
||||
// branch and returned 401 invalid_client right after DCR succeeded —
|
||||
// exactly matching the production log signature.
|
||||
//
|
||||
// Header credentials take precedence when present; body credentials are
|
||||
// only used when the header is absent. Either form must reach the same
|
||||
// validation paths below.
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith('Basic ')) {
|
||||
try {
|
||||
const decoded = Buffer.from(authHeader.slice(6).trim(), 'base64').toString('utf8');
|
||||
const sep = decoded.indexOf(':');
|
||||
if (sep > 0) {
|
||||
const headerClientId = decodeURIComponent(decoded.slice(0, sep));
|
||||
const headerSecret = decodeURIComponent(decoded.slice(sep + 1));
|
||||
if (headerClientId) parsed.data.client_id = headerClientId;
|
||||
if (headerSecret) parsed.data.client_secret = headerSecret;
|
||||
}
|
||||
} catch {
|
||||
return reply.code(401).send({ error: 'invalid_client' });
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.data.grant_type === 'authorization_code') {
|
||||
const { code, code_verifier, client_id, client_secret, redirect_uri, resource } = parsed.data;
|
||||
if (!code || !code_verifier || !client_id || !redirect_uri || !resource) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user