Skip to content

Commit cbd945e

Browse files
authored
[codex] Fix Neon malformed Basic auth validation (#1381)
## What changed This fixes Sentry issue [STACK-BACKEND-1A3](https://stackframe-pw.sentry.io/issues/7436639623/?project=4507442898272256&query=is%3Aunresolved&referrer=issue-stream&seerDrawer=true). A request with this malformed header: ```http Authorization: Basic ``` used to crash the Neon auth validator with a `StackAssertionError`, which turned a bad client request into a 500. The fix makes `neonAuthorizationHeaderSchema` only validate Neon client credentials after the Basic auth header successfully decodes. If decoding fails, the Neon-specific validator returns `true` and lets `basicAuthorizationHeaderSchema` produce the intended 400 schema error: `Authorization header must be in the format "Basic <base64>"`. ## Reviewer walkthrough There are two checks chained together: 1. `basicAuthorizationHeaderSchema` checks that the header is structurally valid Basic auth. 2. `neonAuthorizationHeaderSchema` checks that the decoded `client_id:client_secret` matches a configured Neon client. Yup may still run the second check after the first one has failed, because route validation collects errors with `abortEarly: false`. The old code assumed the first check had already passed and called `throwErr(...)` when decoding returned `null`. This PR changes that path to return `true`, because the format error is already owned by the first check. ## Tests - `pnpm -C packages/stack-shared exec vitest run --maxWorkers=1 --minWorkers=1 src/schema-fields.ts` - `pnpm -C apps/e2e exec vitest run --maxWorkers=1 --minWorkers=1 tests/backend/endpoints/api/v1/integrations/neon/projects/transfer.test.ts -t "malformed"` - `pnpm -C packages/stack-shared lint` - `pnpm -C packages/stack-shared typecheck` - `pnpm -C apps/e2e lint` - `pnpm -C apps/e2e typecheck` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Enhanced authorization header validation in API endpoints with improved error handling, ensuring malformed credentials return clear, specific validation error messages. * **Tests** * Added comprehensive end-to-end test coverage for API request validation, including edge cases for authorization headers. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent a132dd2 commit cbd945e

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/transfer.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,39 @@ it("should fail if the neon client details are missing", async ({ expect }) => {
215215
`);
216216
});
217217

218+
it("should fail if the neon client authorization header is malformed", async ({ expect }) => {
219+
// This project ID is arbitrary because malformed Basic auth is rejected before any project lookup runs.
220+
const projectId = "73782539-cf39-486b-a9f8-9b2893f79ef2";
221+
const response = await niceBackendFetch(urlString`/api/v1/integrations/neon/projects/transfer?project_id=${projectId}`, {
222+
method: "GET",
223+
headers: {
224+
"Authorization": "Basic",
225+
},
226+
});
227+
expect(response).toMatchInlineSnapshot(`
228+
NiceResponse {
229+
"status": 400,
230+
"body": {
231+
"code": "SCHEMA_ERROR",
232+
"details": {
233+
"message": deindent\`
234+
Request validation failed on GET /api/v1/integrations/neon/projects/transfer:
235+
- Authorization header must be in the format "Basic <base64>"
236+
\`,
237+
},
238+
"error": deindent\`
239+
Request validation failed on GET /api/v1/integrations/neon/projects/transfer:
240+
- Authorization header must be in the format "Basic <base64>"
241+
\`,
242+
},
243+
"headers": Headers {
244+
"x-stack-known-error": "SCHEMA_ERROR",
245+
<some fields may have been hidden>,
246+
},
247+
}
248+
`);
249+
});
250+
218251
it("should fail to transfer project if the user is not signed in", async ({ expect }) => {
219252
const provisioned = await provisionProject();
220253
const projectId = provisioned.body.project_id;
@@ -305,4 +338,3 @@ it("should fail the check if project was already transferred", async ({ expect }
305338
}
306339
`);
307340
});
308-

packages/stack-shared/src/schema-fields.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { KnownErrors } from "./known-errors";
33
import { isBase64 } from "./utils/bytes";
44
import { SUPPORTED_CURRENCIES, type Currency, type MoneyAmount } from "./utils/currency-constants";
55
import type { DayInterval, Interval } from "./utils/dates";
6-
import { StackAssertionError, throwErr } from "./utils/errors";
6+
import { StackAssertionError } from "./utils/errors";
77
import { decodeBasicAuthorizationHeader } from "./utils/http";
88
import { allProviders } from "./utils/oauth";
99
import { deepPlainClone, omit, typedFromEntries } from "./utils/objects";
@@ -878,12 +878,17 @@ export const basicAuthorizationHeaderSchema = yupString().test('is-basic-authori
878878
// Neon integration
879879
export const neonAuthorizationHeaderSchema = basicAuthorizationHeaderSchema.test('is-authorization-header', 'Invalid client_id:client_secret values; did you use the correct values for the integration?', (value) => {
880880
if (!value) return true;
881-
const [clientId, clientSecret] = decodeBasicAuthorizationHeader(value) ?? throwErr(`Authz header invalid? This should've been validated by basicAuthorizationHeaderSchema: ${value}`);
881+
const decoded = decodeBasicAuthorizationHeader(value);
882+
if (decoded === null) return true;
883+
const [clientId, clientSecret] = decoded;
882884
for (const neonClientConfig of JSON.parse(process.env.STACK_INTEGRATION_CLIENTS_CONFIG || '[]')) {
883885
if (clientId === neonClientConfig.client_id && clientSecret === neonClientConfig.client_secret) return true;
884886
}
885887
return false;
886888
});
889+
import.meta.vitest?.test("neonAuthorizationHeaderSchema handles malformed Basic auth as a validation error", async ({ expect }) => {
890+
await expect(neonAuthorizationHeaderSchema.validate("Basic", { abortEarly: false })).rejects.toThrow('Authorization header must be in the format "Basic <base64>"');
891+
});
887892

888893
// Utils
889894
export function yupDefinedWhen<S extends yup.AnyObject>(

0 commit comments

Comments
 (0)