Require CoinPay userinfo identity (#21)

This commit is contained in:
phucnguyen1707 2026-06-14 13:01:15 +07:00 committed by GitHub
parent 63ccad6d2a
commit 14fe9d608f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 36 additions and 1 deletions

View file

@ -392,6 +392,34 @@ describe("CoinPay OAuth", () => {
user: { provider: "coinpay", sub: "merchant-123", email: "merchant@example.com", name: "Merchant User" } user: { provider: "coinpay", sub: "merchant-123", email: "merchant@example.com", name: "Merchant User" }
}); });
}); });
it("does not create a session when userinfo omits the subject", async () => {
process.env.COINPAY_OAUTH_ISSUER = "https://coinpayportal.example";
process.env.COINPAY_OAUTH_CLIENT_ID = "cp_test_client";
process.env.COINPAY_OAUTH_CLIENT_SECRET = "cps_test_secret";
process.env.COINPAY_OAUTH_REDIRECT_URI = "https://logicsrc.com/api/oauth/coinpay/callback";
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
const url = typeof input === "string" ? input : input.toString();
if (url.includes("/api/oauth/token")) {
return jsonResponse({ access_token: "access_token_123", token_type: "Bearer", scope: "openid profile email" });
}
if (url.includes("/api/oauth/userinfo")) {
return jsonResponse({ email: "merchant@example.com", name: "Merchant User" });
}
throw new Error(`unexpected fetch ${url}`);
});
const callback = await oauthCallback(
new NextRequest("http://localhost/api/oauth/coinpay/callback?code=auth_code_123&state=state_123", {
headers: { cookie: "logicsrc_coinpay_oauth_state=state_123" }
})
);
expect(callback.status).toBe(302);
expect(callback.headers.get("location")).toBe("/?coinpay_oauth=error&error=userinfo_failed");
expect(callback.headers.getSetCookie().some((cookie) => cookie.startsWith("logicsrc_coinpay_session="))).toBe(false);
});
}); });
describe("session signing", () => { describe("session signing", () => {

View file

@ -74,10 +74,17 @@ export async function GET(request: NextRequest) {
}); });
const userText = await userResponse.text(); const userText = await userResponse.text();
const userInfo = userResponse.ok ? parseJson(userText) : {}; const userInfo = userResponse.ok ? parseJson(userText) : {};
if (!userResponse.ok || typeof userInfo.sub !== "string" || userInfo.sub.trim() === "") {
console.error("[coinpay-oauth] userinfo failed", {
status: userResponse.status,
error: userResponse.ok ? "missing_sub" : userText.slice(0, 160)
});
return redirectWithOAuthStatus("error", "userinfo_failed");
}
const session = signSession({ const session = signSession({
provider: "coinpay", provider: "coinpay",
sub: typeof userInfo.sub === "string" ? userInfo.sub : null, sub: userInfo.sub,
email: typeof userInfo.email === "string" ? userInfo.email : null, email: typeof userInfo.email === "string" ? userInfo.email : null,
name: typeof userInfo.name === "string" ? userInfo.name : null, name: typeof userInfo.name === "string" ? userInfo.name : null,
scope: typeof tokenPayload.scope === "string" ? tokenPayload.scope : config.scopes, scope: typeof tokenPayload.scope === "string" ? tokenPayload.scope : config.scopes,