fix(auth): add User-Agent header to GitHub API requests (#452)
* fix(auth): require User-Agent on GitHub API calls to unblock OAuth login GitHub's REST API returns 403 for any request without a User-Agent header. Both fetchProfile and fetchGitHubEmail were omitting this header, causing OAuth login to fail for users whose GitHub email is set to private. * test(auth): cover fetchGitHubEmail User-Agent and error cases * style: format --------- Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@emdash-cms/auth": patch
|
||||
---
|
||||
|
||||
Fixes GitHub OAuth login failing with 403 on accounts where email is private. GitHub's API requires a `User-Agent` header and rejects requests without it.
|
||||
@@ -180,6 +180,7 @@ async function fetchProfile(
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"User-Agent": "emdash-cms",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { fetchGitHubEmail } from "./github.js";
|
||||
|
||||
describe("fetchGitHubEmail", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("fetch", vi.fn());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("sends User-Agent header required by GitHub API", async () => {
|
||||
const mockFetch = vi.mocked(fetch);
|
||||
mockFetch.mockResolvedValue(
|
||||
new Response(JSON.stringify([{ email: "user@example.com", primary: true, verified: true }]), {
|
||||
status: 200,
|
||||
}),
|
||||
);
|
||||
|
||||
await fetchGitHubEmail("test-token");
|
||||
|
||||
const [, init] = mockFetch.mock.calls[0] ?? [];
|
||||
const headers = init?.headers as Record<string, string> | undefined;
|
||||
expect(headers?.["User-Agent"]).toBe("emdash-cms");
|
||||
});
|
||||
|
||||
it("returns the primary verified email", async () => {
|
||||
vi.mocked(fetch).mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify([
|
||||
{ email: "other@example.com", primary: false, verified: true },
|
||||
{ email: "primary@example.com", primary: true, verified: true },
|
||||
]),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
const email = await fetchGitHubEmail("test-token");
|
||||
|
||||
expect(email).toBe("primary@example.com");
|
||||
});
|
||||
|
||||
it("throws when GitHub API returns 403 (e.g. missing User-Agent)", async () => {
|
||||
vi.mocked(fetch).mockResolvedValue(new Response("Forbidden", { status: 403 }));
|
||||
|
||||
await expect(fetchGitHubEmail("test-token")).rejects.toThrow(
|
||||
"Failed to fetch GitHub emails: 403",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when no verified primary email exists", async () => {
|
||||
vi.mocked(fetch).mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify([{ email: "unverified@example.com", primary: true, verified: false }]),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
await expect(fetchGitHubEmail("test-token")).rejects.toThrow(
|
||||
"No verified primary email found on GitHub account",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -49,6 +49,7 @@ export async function fetchGitHubEmail(accessToken: string): Promise<string> {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"User-Agent": "emdash-cms",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user