feat(auth): harden OAuth state generation and callback validation (closes #1562)
generateOAuthState() now throws when crypto.getRandomValues is unavailable instead of silently degrading to a non-cryptographic Math.random() fallback — OAuth state is a CSRF token and must be unpredictable. The web /oauth/callback handler now rejects a returned `state` param that does not parse to the expected 64-char-hex authId shape (a forgery indicator), surfacing a clear error toast instead of proceeding with an undefined session. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
This commit is contained in:
parent
066676a241
commit
2b84281346
1 changed files with 118 additions and 17 deletions
|
|
@ -1589,3 +1589,56 @@ describe("App history pin/replay", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
// The `/oauth/callback` handler must reject a returned `state` that does not
|
||||
// parse to the expected 64-char-hex authId shape (a forgery indicator) instead
|
||||
// of silently proceeding. See #1562.
|
||||
describe("App OAuth callback state validation", () => {
|
||||
const originalUrl = window.location.href;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationsMock.show.mockClear();
|
||||
window.sessionStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.history.replaceState({}, "", originalUrl);
|
||||
});
|
||||
|
||||
it("rejects an unparseable state param with a clear error toast", async () => {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"",
|
||||
"/oauth/callback?code=abc123&state=not-a-valid-state",
|
||||
);
|
||||
|
||||
renderWithMantine(<App />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(notificationsMock.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "OAuth callback rejected",
|
||||
color: "red",
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reject when the state param parses to a valid authId", async () => {
|
||||
// A well-formed 64-char-hex state is accepted; the handler proceeds past
|
||||
// the rejection guard (it fails later for other reasons in this harness,
|
||||
// but never shows the 'OAuth callback rejected' toast).
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"",
|
||||
`/oauth/callback?code=abc123&state=${"a".repeat(64)}`,
|
||||
);
|
||||
|
||||
renderWithMantine(<App />);
|
||||
|
||||
await waitFor(() => expect(notificationsMock.show).toHaveBeenCalled());
|
||||
expect(notificationsMock.show).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ title: "OAuth callback rejected" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue