From dc708b9811db53531af9e23f7911b0b602804a89 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Thu, 25 Dec 2025 16:55:21 +0400 Subject: [PATCH 1/4] Added Discord OAuth identity verification --- .env.example | 3 + src/libs/abstraction/index.ts | 104 +++++++++++++ src/libs/identity/oauth/discord.ts | 240 +++++++++++++++++++++++++++++ src/libs/network/manageNodeCall.ts | 27 ++++ 4 files changed, 374 insertions(+) create mode 100644 src/libs/identity/oauth/discord.ts diff --git a/.env.example b/.env.example index 730ff244..b0722e47 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,6 @@ DISCORD_BOT_TOKEN= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= + +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= diff --git a/src/libs/abstraction/index.ts b/src/libs/abstraction/index.ts index 6ede01ae..c8f08059 100644 --- a/src/libs/abstraction/index.ts +++ b/src/libs/abstraction/index.ts @@ -8,6 +8,7 @@ import log from "src/utilities/logger" import { TelegramSignedAttestation } from "@kynesyslabs/demosdk/abstraction" import { getSharedState } from "@/utilities/sharedState" import { SignedGitHubOAuthAttestation } from "../identity/oauth/github" +import { SignedDiscordOAuthAttestation } from "../identity/oauth/discord" /** * Verifies telegram dual signature attestation (user + bot signatures) @@ -281,6 +282,109 @@ export async function verifyWeb2Proof( } } + // Handle Discord OAuth-based proofs with signed attestation + // Check if the proof is a JSON-stringified SignedDiscordOAuthAttestation + if (payload.context === "discord" && typeof payload.proof === "string" && payload.proof.startsWith("{")) { + try { + const signedAttestation: SignedDiscordOAuthAttestation = JSON.parse(payload.proof) + + // Validate attestation structure + if ( + !signedAttestation?.attestation || + !signedAttestation?.signature || + !signedAttestation?.signatureType + ) { + return { + success: false, + message: "Invalid Discord OAuth attestation structure", + } + } + + const { attestation, signature, signatureType } = signedAttestation + + // Verify attestation data matches payload + if (attestation.provider !== "discord") { + return { + success: false, + message: "Invalid provider in attestation", + } + } + + if (attestation.userId !== payload.userId) { + return { + success: false, + message: `User ID mismatch: expected ${payload.userId}, got ${attestation.userId}`, + } + } + + if (attestation.username !== payload.username) { + return { + success: false, + message: `Username mismatch: expected ${payload.username}, got ${attestation.username}`, + } + } + + // Check attestation is not too old (5 minutes) + const maxAge = 5 * 60 * 1000 + if (Date.now() - attestation.timestamp > maxAge) { + return { + success: false, + message: "Discord OAuth attestation has expired", + } + } + + // Verify the signature + const attestationString = JSON.stringify(attestation) + const hash = Hashing.sha256(attestationString) + + const nodePublicKeyHex = attestation.nodePublicKey.replace("0x", "") + const publicKeyBytes = hexToUint8Array(nodePublicKeyHex) + const signatureBytes = hexToUint8Array(signature) + + const isValid = await ucrypto.verify({ + algorithm: signatureType as "ed25519" | "ml-dsa" | "falcon", + message: new TextEncoder().encode(hash), + signature: signatureBytes, + publicKey: publicKeyBytes, + }) + + if (!isValid) { + return { + success: false, + message: "Invalid Discord OAuth attestation signature", + } + } + + // Check that the signing node is authorized (exists in genesis identities) + const nodeAddress = attestation.nodePublicKey.replace("0x", "") + const ownPublicKey = getSharedState.publicKeyHex?.replace("0x", "") + const isOwnNode = nodeAddress === ownPublicKey + + const nodeAuthorized = isOwnNode || await checkBotAuthorization(nodeAddress) + if (!nodeAuthorized) { + return { + success: false, + message: "Unauthorized node - not found in genesis addresses", + } + } + + log.info( + `Discord OAuth attestation verified: userId=${payload.userId}, username=${payload.username}`, + ) + + return { + success: true, + message: "Verified Discord OAuth attestation", + } + } catch (error) { + log.error(`Discord OAuth attestation verification error: ${error}`) + return { + success: false, + message: `Discord OAuth attestation verification failed: ${error instanceof Error ? error.message : String(error)}`, + } + } + } + switch (payload.context) { case "twitter": parser = TwitterProofParser diff --git a/src/libs/identity/oauth/discord.ts b/src/libs/identity/oauth/discord.ts new file mode 100644 index 00000000..3a9bd482 --- /dev/null +++ b/src/libs/identity/oauth/discord.ts @@ -0,0 +1,240 @@ +import log from "src/utilities/logger" +import { Hashing, ucrypto, uint8ArrayToHex, hexToUint8Array } from "@kynesyslabs/demosdk/encryption" +import { getSharedState } from "src/utilities/sharedState" + +interface DiscordTokenResponse { + access_token: string + token_type: string + expires_in: number + refresh_token: string + scope: string + error?: string + error_description?: string +} + +interface DiscordUser { + id: string + username: string + discriminator: string + global_name?: string + avatar?: string + email?: string +} + +export interface DiscordOAuthAttestation { + provider: "discord" + userId: string + username: string + timestamp: number + nodePublicKey: string +} + +export interface SignedDiscordOAuthAttestation { + attestation: DiscordOAuthAttestation + signature: string + signatureType: string +} + +export interface DiscordOAuthResult { + success: boolean + userId?: string + username?: string + signedAttestation?: SignedDiscordOAuthAttestation + error?: string +} + +/** + * Sign the OAuth attestation with the node's private key + */ +async function signAttestation(attestation: DiscordOAuthAttestation): Promise { + const attestationString = JSON.stringify(attestation) + const hash = Hashing.sha256(attestationString) + + const signature = await ucrypto.sign( + getSharedState.signingAlgorithm, + new TextEncoder().encode(hash), + ) + + return { + attestation, + signature: uint8ArrayToHex(signature.signature), + signatureType: getSharedState.signingAlgorithm, + } +} + +/** + * Exchange Discord OAuth authorization code for access token and fetch user info + * Returns a signed attestation that can be verified by other nodes + */ +export async function exchangeDiscordCode(code: string, redirectUri: string): Promise { + const clientId = process.env.DISCORD_CLIENT_ID + const clientSecret = process.env.DISCORD_CLIENT_SECRET + + if (!clientId || !clientSecret) { + log.error("[Discord OAuth] Missing DISCORD_CLIENT_ID or DISCORD_CLIENT_SECRET") + + return { + success: false, + error: "Discord OAuth not configured on server", + } + } + + try { + // Step 1: Exchange code for access token + const tokenController = new AbortController() + const tokenTimeoutId = setTimeout(() => tokenController.abort(), 10000) // 10-second timeout + + const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + grant_type: "authorization_code", + code: code, + redirect_uri: redirectUri, + }), + signal: tokenController.signal, + }) + clearTimeout(tokenTimeoutId) + + const tokenData: DiscordTokenResponse = await tokenResponse.json() + + if (tokenData.error) { + log.error(`[Discord OAuth] Token exchange failed: ${tokenData.error_description || tokenData.error}`) + return { + success: false, + error: tokenData.error_description || tokenData.error, + } + } + + if (!tokenData.access_token) { + log.error("[Discord OAuth] No access token in response") + return { + success: false, + error: "Failed to obtain access token", + } + } + + // Step 2: Fetch user info using access token + const userController = new AbortController() + const userTimeoutId = setTimeout(() => userController.abort(), 10000) // 10-second timeout + + const userResponse = await fetch("https://discord.com/api/users/@me", { + headers: { + "Authorization": `Bearer ${tokenData.access_token}`, + }, + signal: userController.signal, + }) + clearTimeout(userTimeoutId) + + if (!userResponse.ok) { + log.error(`[Discord OAuth] Failed to fetch user info: ${userResponse.status}`) + return { + success: false, + error: "Failed to fetch Discord user info", + } + } + + const userData: DiscordUser = await userResponse.json() + + // Discord usernames: use global_name if available, otherwise username#discriminator or just username + const displayUsername = userData.global_name || + (userData.discriminator !== "0" ? `${userData.username}#${userData.discriminator}` : userData.username) + + log.info(`[Discord OAuth] Successfully authenticated user: ${displayUsername} (ID: ${userData.id})`) + + // Step 3: Create and sign attestation + const nodePublicKey = getSharedState.publicKeyHex + if (!nodePublicKey) { + log.error("[Discord OAuth] Node public key not available") + return { + success: false, + error: "Node identity not initialized", + } + } + + // Ensure nodePublicKey has 0x prefix (publicKeyHex doesn't include it) + const normalizedPublicKey = nodePublicKey.startsWith("0x") ? nodePublicKey : "0x" + nodePublicKey + + const attestation: DiscordOAuthAttestation = { + provider: "discord", + userId: userData.id, + username: displayUsername, + timestamp: Date.now(), + nodePublicKey: normalizedPublicKey, + } + + const signedAttestation = await signAttestation(attestation) + + return { + success: true, + userId: userData.id, + username: displayUsername, + signedAttestation, + } + } catch (error) { + log.error(`[Discord OAuth] Error: ${error}`) + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error during OAuth", + } + } +} + +/** + * Verify a signed Discord OAuth attestation + */ +export async function verifyDiscordOAuthAttestation( + signedAttestation: SignedDiscordOAuthAttestation, + expectedUserId: string, + expectedUsername: string, +): Promise<{ valid: boolean; error?: string }> { + try { + const { attestation, signature, signatureType } = signedAttestation + + // Verify attestation data matches expected values + if (attestation.provider !== "discord") { + return { valid: false, error: "Invalid provider in attestation" } + } + + if (attestation.userId !== expectedUserId) { + return { valid: false, error: "User ID mismatch in attestation" } + } + + if (attestation.username !== expectedUsername) { + return { valid: false, error: "Username mismatch in attestation" } + } + + // Check attestation is not too old (e.g., 5 minutes) + const maxAge = 5 * 60 * 1000 // 5 minutes in milliseconds + if (Date.now() - attestation.timestamp > maxAge) { + return { valid: false, error: "Attestation has expired" } + } + + // Verify the signature using the node's public key from the attestation + const attestationString = JSON.stringify(attestation) + const hash = Hashing.sha256(attestationString) + + const isValid = await ucrypto.verify({ + algorithm: signatureType as "ed25519" | "ml-dsa" | "falcon", + message: new TextEncoder().encode(hash), + signature: hexToUint8Array(signature), + publicKey: hexToUint8Array(attestation.nodePublicKey.replace("0x", "")), + }) + + if (!isValid) { + return { valid: false, error: "Invalid attestation signature" } + } + + return { valid: true } + } catch (error) { + log.error(`[Discord OAuth] Attestation verification error: ${error}`) + return { + valid: false, + error: error instanceof Error ? error.message : "Verification error", + } + } +} diff --git a/src/libs/network/manageNodeCall.ts b/src/libs/network/manageNodeCall.ts index 8b03b7cb..b9fd9185 100644 --- a/src/libs/network/manageNodeCall.ts +++ b/src/libs/network/manageNodeCall.ts @@ -26,6 +26,7 @@ import ensureGCRForUser from "../blockchain/gcr/gcr_routines/ensureGCRForUser" import { Discord, DiscordMessage } from "../identity/tools/discord" import { UDIdentityManager } from "../blockchain/gcr/gcr_routines/udIdentityManager" import { exchangeGitHubCode } from "../identity/oauth/github" +import { exchangeDiscordCode } from "../identity/oauth/discord" export interface NodeCall { message: string @@ -373,6 +374,32 @@ export async function manageNodeCall(content: NodeCall): Promise { break } + case "exchangeDiscordOAuthCode": { + if (!data.code) { + response.result = 400 + response.response = { + success: false, + error: "No authorization code provided", + } + break + } + + if (!data.redirectUri) { + response.result = 400 + response.response = { + success: false, + error: "No redirect URI provided", + } + break + } + + const discordOauthResult = await exchangeDiscordCode(data.code, data.redirectUri) + + response.result = discordOauthResult.success ? 200 : 400 + response.response = discordOauthResult + break + } + // INFO: Tests if twitter account is a bot // case "checkIsBot": { // if (!data.username || !data.userId) { From 8780bbfd8cb8b133dffffddc4112ff829dce2fb6 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Fri, 26 Dec 2025 15:13:02 +0400 Subject: [PATCH 2/4] Fixed qodo comments --- src/libs/abstraction/index.ts | 374 +++++++++++++---------------- src/libs/identity/oauth/discord.ts | 10 +- src/libs/identity/oauth/github.ts | 10 +- 3 files changed, 179 insertions(+), 215 deletions(-) diff --git a/src/libs/abstraction/index.ts b/src/libs/abstraction/index.ts index c8f08059..63fae12c 100644 --- a/src/libs/abstraction/index.ts +++ b/src/libs/abstraction/index.ts @@ -7,8 +7,27 @@ import { Twitter } from "../identity/tools/twitter" import log from "src/utilities/logger" import { TelegramSignedAttestation } from "@kynesyslabs/demosdk/abstraction" import { getSharedState } from "@/utilities/sharedState" -import { SignedGitHubOAuthAttestation } from "../identity/oauth/github" -import { SignedDiscordOAuthAttestation } from "../identity/oauth/discord" + +// Generic OAuth attestation type that works for any provider +interface SignedOAuthAttestation { + attestation: { + provider: string + userId: string + username: string + timestamp: number + nodePublicKey: string + } + signature: string + signatureType: string +} + +function canonicalJSON(obj: Record): string { + const sortedObj: Record = {} + Object.keys(obj).sort().forEach(key => { + sortedObj[key] = obj[key] + }) + return JSON.stringify(sortedObj) +} /** * Verifies telegram dual signature attestation (user + bot signatures) @@ -35,6 +54,133 @@ async function checkBotAuthorization(botAddress: string): Promise { return false } +/** + * Generic OAuth attestation verification for any provider (GitHub, Discord, etc.) + * + * @param payload - The web2 identity payload containing proof and user info + * @param provider - The OAuth provider name (e.g., "github", "discord") + * @returns Verification result with success status and message + */ +async function verifySignedOAuthAttestation( + payload: Web2CoreTargetIdentityPayload, + provider: string, +): Promise<{ success: boolean; message: string }> { + try { + let signedAttestation: SignedOAuthAttestation + + // Parse the proof - it could be a string or already an object + if (typeof payload.proof === "string") { + signedAttestation = JSON.parse(payload.proof) + } else { + signedAttestation = payload.proof as unknown as SignedOAuthAttestation + } + + // Validate attestation structure + if ( + !signedAttestation?.attestation || + !signedAttestation?.signature || + !signedAttestation?.signatureType + ) { + return { + success: false, + message: `Invalid ${provider} OAuth attestation structure`, + } + } + + const { attestation, signature, signatureType } = signedAttestation + + // Verify attestation data matches payload + if (attestation.provider !== provider) { + return { + success: false, + message: `Invalid provider in attestation: expected ${provider}, got ${attestation.provider}`, + } + } + + if (attestation.userId !== payload.userId) { + return { + success: false, + message: `User ID mismatch: expected ${payload.userId}, got ${attestation.userId}`, + } + } + + if (attestation.username !== payload.username) { + return { + success: false, + message: `Username mismatch: expected ${payload.username}, got ${attestation.username}`, + } + } + + // Check attestation is not too old (5 minutes) + const maxAge = 5 * 60 * 1000 + if (Date.now() - attestation.timestamp > maxAge) { + return { + success: false, + message: `${provider} OAuth attestation has expired`, + } + } + + // Validate signature algorithm + const allowedAlgorithms = ["ed25519", "ml-dsa", "falcon"] as const + if (!allowedAlgorithms.includes(signatureType as typeof allowedAlgorithms[number])) { + return { + success: false, + message: `Unsupported signature algorithm: ${signatureType}`, + } + } + + // Verify the signature using canonical JSON for deterministic hashing + const attestationString = canonicalJSON(attestation as unknown as Record) + const hash = Hashing.sha256(attestationString) + + const nodePublicKeyHex = attestation.nodePublicKey.replace("0x", "") + const publicKeyBytes = hexToUint8Array(nodePublicKeyHex) + const signatureBytes = hexToUint8Array(signature) + + const isValid = await ucrypto.verify({ + algorithm: signatureType as typeof allowedAlgorithms[number], + message: new TextEncoder().encode(hash), + signature: signatureBytes, + publicKey: publicKeyBytes, + }) + + if (!isValid) { + return { + success: false, + message: `Invalid ${provider} OAuth attestation signature`, + } + } + + // Check that the signing node is authorized (exists in genesis identities) + const nodeAddress = attestation.nodePublicKey.replace("0x", "") + const ownPublicKey = getSharedState.publicKeyHex?.replace("0x", "") + const isOwnNode = nodeAddress === ownPublicKey + + const nodeAuthorized = isOwnNode || await checkBotAuthorization(nodeAddress) + if (!nodeAuthorized) { + return { + success: false, + message: "Unauthorized node - not found in genesis addresses", + } + } + + log.info( + `${provider} OAuth attestation verified: userId=${payload.userId}, username=${payload.username}`, + ) + + return { + success: true, + message: `Verified ${provider} OAuth attestation`, + } + } catch (error) { + log.error(`${provider} OAuth attestation verification error: ${error}`) + return { + success: false, + message: `${provider} OAuth attestation verification failed: ${error instanceof Error ? error.message : String(error)}`, + } + } +} + async function verifyTelegramProof( payload: Web2CoreTargetIdentityPayload, sender: string, @@ -172,217 +318,19 @@ export async function verifyWeb2Proof( | typeof TwitterProofParser | typeof DiscordProofParser - // Handle OAuth-based proofs with signed attestation - // The proof should be a SignedGitHubOAuthAttestation object (stringified) - if (payload.context === "github") { - try { - let signedAttestation: SignedGitHubOAuthAttestation - - // Parse the proof - it could be a string or already an object - if (typeof payload.proof === "string") { - signedAttestation = JSON.parse(payload.proof) - } else { - signedAttestation = payload.proof as unknown as SignedGitHubOAuthAttestation - } - - // Validate attestation structure - if ( - !signedAttestation?.attestation || - !signedAttestation?.signature || - !signedAttestation?.signatureType - ) { - return { - success: false, - message: "Invalid GitHub OAuth attestation structure", - } - } - - const { attestation, signature, signatureType } = signedAttestation - - // Verify attestation data matches payload - if (attestation.provider !== "github") { - return { - success: false, - message: "Invalid provider in attestation", - } - } - - if (attestation.userId !== payload.userId) { - return { - success: false, - message: `User ID mismatch: expected ${payload.userId}, got ${attestation.userId}`, - } - } - - if (attestation.username !== payload.username) { - return { - success: false, - message: `Username mismatch: expected ${payload.username}, got ${attestation.username}`, - } - } - - // Check attestation is not too old (5 minutes) - const maxAge = 5 * 60 * 1000 - if (Date.now() - attestation.timestamp > maxAge) { - return { - success: false, - message: "GitHub OAuth attestation has expired", - } - } - - // Verify the signature - const attestationString = JSON.stringify(attestation) - const hash = Hashing.sha256(attestationString) - - const nodePublicKeyHex = attestation.nodePublicKey.replace("0x", "") - const publicKeyBytes = hexToUint8Array(nodePublicKeyHex) - const signatureBytes = hexToUint8Array(signature) - - const isValid = await ucrypto.verify({ - algorithm: signatureType as "ed25519" | "ml-dsa" | "falcon", - message: new TextEncoder().encode(hash), - signature: signatureBytes, - publicKey: publicKeyBytes, - }) - - if (!isValid) { - return { - success: false, - message: "Invalid GitHub OAuth attestation signature", - } - } - - // Check that the signing node is authorized (exists in genesis identities) - const nodeAddress = attestation.nodePublicKey.replace("0x", "") - const ownPublicKey = getSharedState.publicKeyHex?.replace("0x", "") - const isOwnNode = nodeAddress === ownPublicKey - - const nodeAuthorized = isOwnNode || await checkBotAuthorization(nodeAddress) - if (!nodeAuthorized) { - return { - success: false, - message: "Unauthorized node - not found in genesis addresses", - } - } - - log.info( - `GitHub OAuth attestation verified: userId=${payload.userId}, username=${payload.username}`, - ) - - return { - success: true, - message: "Verified GitHub OAuth attestation", - } - } catch (error) { - log.error(`GitHub OAuth attestation verification error: ${error}`) - return { - success: false, - message: `GitHub OAuth attestation verification failed: ${error instanceof Error ? error.message : String(error)}`, - } - } - } - - // Handle Discord OAuth-based proofs with signed attestation - // Check if the proof is a JSON-stringified SignedDiscordOAuthAttestation - if (payload.context === "discord" && typeof payload.proof === "string" && payload.proof.startsWith("{")) { - try { - const signedAttestation: SignedDiscordOAuthAttestation = JSON.parse(payload.proof) - - // Validate attestation structure - if ( - !signedAttestation?.attestation || - !signedAttestation?.signature || - !signedAttestation?.signatureType - ) { - return { - success: false, - message: "Invalid Discord OAuth attestation structure", - } - } - - const { attestation, signature, signatureType } = signedAttestation - - // Verify attestation data matches payload - if (attestation.provider !== "discord") { - return { - success: false, - message: "Invalid provider in attestation", - } - } - - if (attestation.userId !== payload.userId) { - return { - success: false, - message: `User ID mismatch: expected ${payload.userId}, got ${attestation.userId}`, - } - } - - if (attestation.username !== payload.username) { - return { - success: false, - message: `Username mismatch: expected ${payload.username}, got ${attestation.username}`, - } - } - - // Check attestation is not too old (5 minutes) - const maxAge = 5 * 60 * 1000 - if (Date.now() - attestation.timestamp > maxAge) { - return { - success: false, - message: "Discord OAuth attestation has expired", - } - } - - // Verify the signature - const attestationString = JSON.stringify(attestation) - const hash = Hashing.sha256(attestationString) - - const nodePublicKeyHex = attestation.nodePublicKey.replace("0x", "") - const publicKeyBytes = hexToUint8Array(nodePublicKeyHex) - const signatureBytes = hexToUint8Array(signature) - - const isValid = await ucrypto.verify({ - algorithm: signatureType as "ed25519" | "ml-dsa" | "falcon", - message: new TextEncoder().encode(hash), - signature: signatureBytes, - publicKey: publicKeyBytes, - }) - - if (!isValid) { - return { - success: false, - message: "Invalid Discord OAuth attestation signature", - } - } - - // Check that the signing node is authorized (exists in genesis identities) - const nodeAddress = attestation.nodePublicKey.replace("0x", "") - const ownPublicKey = getSharedState.publicKeyHex?.replace("0x", "") - const isOwnNode = nodeAddress === ownPublicKey - - const nodeAuthorized = isOwnNode || await checkBotAuthorization(nodeAddress) - if (!nodeAuthorized) { - return { - success: false, - message: "Unauthorized node - not found in genesis addresses", - } - } - - log.info( - `Discord OAuth attestation verified: userId=${payload.userId}, username=${payload.username}`, - ) - - return { - success: true, - message: "Verified Discord OAuth attestation", - } - } catch (error) { - log.error(`Discord OAuth attestation verification error: ${error}`) - return { - success: false, - message: `Discord OAuth attestation verification failed: ${error instanceof Error ? error.message : String(error)}`, - } - } + // Handle OAuth-based proofs with signed attestation (GitHub, Discord, etc.) + // Check if proof is a JSON object (OAuth attestation) vs URL string (legacy proof) + // OAuth proof can be: 1) a string starting with "{", 2) an object with attestation property + const oauthProviders = ["github", "discord"] + const isStringProof = typeof payload.proof === "string" + const isOAuthStringProof = isStringProof && (payload.proof as string).trim().startsWith("{") + const isOAuthObjectProof = !isStringProof && + typeof payload.proof === "object" && + payload.proof !== null && + "attestation" in payload.proof + + if (oauthProviders.includes(payload.context) && (isOAuthStringProof || isOAuthObjectProof || payload.context === "github")) { + return await verifySignedOAuthAttestation(payload, payload.context) } switch (payload.context) { diff --git a/src/libs/identity/oauth/discord.ts b/src/libs/identity/oauth/discord.ts index 3a9bd482..a111dea4 100644 --- a/src/libs/identity/oauth/discord.ts +++ b/src/libs/identity/oauth/discord.ts @@ -43,11 +43,19 @@ export interface DiscordOAuthResult { error?: string } +function canonicalJSON(obj: Record): string { + const sortedObj: Record = {} + Object.keys(obj).sort().forEach(key => { + sortedObj[key] = obj[key] + }) + return JSON.stringify(sortedObj) +} + /** * Sign the OAuth attestation with the node's private key */ async function signAttestation(attestation: DiscordOAuthAttestation): Promise { - const attestationString = JSON.stringify(attestation) + const attestationString = canonicalJSON(attestation as unknown as Record) const hash = Hashing.sha256(attestationString) const signature = await ucrypto.sign( diff --git a/src/libs/identity/oauth/github.ts b/src/libs/identity/oauth/github.ts index 0bd371cd..6e210aef 100644 --- a/src/libs/identity/oauth/github.ts +++ b/src/libs/identity/oauth/github.ts @@ -40,11 +40,19 @@ export interface GitHubOAuthResult { error?: string } +function canonicalJSON(obj: Record): string { + const sortedObj: Record = {} + Object.keys(obj).sort().forEach(key => { + sortedObj[key] = obj[key] + }) + return JSON.stringify(sortedObj) +} + /** * Sign the OAuth attestation with the node's private key */ async function signAttestation(attestation: GitHubOAuthAttestation): Promise { - const attestationString = JSON.stringify(attestation) + const attestationString = canonicalJSON(attestation as unknown as Record) const hash = Hashing.sha256(attestationString) const signature = await ucrypto.sign( From 9e2693afd6a93765c7552430c5f2f4550171acf2 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Fri, 26 Dec 2025 15:27:28 +0400 Subject: [PATCH 3/4] Fixed qodo comment --- src/libs/abstraction/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/abstraction/index.ts b/src/libs/abstraction/index.ts index 63fae12c..79ed134f 100644 --- a/src/libs/abstraction/index.ts +++ b/src/libs/abstraction/index.ts @@ -329,7 +329,7 @@ export async function verifyWeb2Proof( payload.proof !== null && "attestation" in payload.proof - if (oauthProviders.includes(payload.context) && (isOAuthStringProof || isOAuthObjectProof || payload.context === "github")) { + if (oauthProviders.includes(payload.context) && (isOAuthStringProof || isOAuthObjectProof)) { return await verifySignedOAuthAttestation(payload, payload.context) } From f891eeb60582bbcebab94e0f4c61f70ba79055d3 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Fri, 26 Dec 2025 20:53:46 +0100 Subject: [PATCH 4/4] Use localeCompare for reliable alphabetical sorting in canonicalJSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes S2871: Array.prototype.sort() should use a compare function for consistent string sorting across JavaScript engines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/libs/abstraction/index.ts | 2 +- src/libs/identity/oauth/discord.ts | 2 +- src/libs/identity/oauth/github.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/abstraction/index.ts b/src/libs/abstraction/index.ts index 79ed134f..8ef3bfe2 100644 --- a/src/libs/abstraction/index.ts +++ b/src/libs/abstraction/index.ts @@ -23,7 +23,7 @@ interface SignedOAuthAttestation { function canonicalJSON(obj: Record): string { const sortedObj: Record = {} - Object.keys(obj).sort().forEach(key => { + Object.keys(obj).sort((a, b) => a.localeCompare(b)).forEach(key => { sortedObj[key] = obj[key] }) return JSON.stringify(sortedObj) diff --git a/src/libs/identity/oauth/discord.ts b/src/libs/identity/oauth/discord.ts index a111dea4..f488d001 100644 --- a/src/libs/identity/oauth/discord.ts +++ b/src/libs/identity/oauth/discord.ts @@ -45,7 +45,7 @@ export interface DiscordOAuthResult { function canonicalJSON(obj: Record): string { const sortedObj: Record = {} - Object.keys(obj).sort().forEach(key => { + Object.keys(obj).sort((a, b) => a.localeCompare(b)).forEach(key => { sortedObj[key] = obj[key] }) return JSON.stringify(sortedObj) diff --git a/src/libs/identity/oauth/github.ts b/src/libs/identity/oauth/github.ts index 6e210aef..7487e3ea 100644 --- a/src/libs/identity/oauth/github.ts +++ b/src/libs/identity/oauth/github.ts @@ -42,7 +42,7 @@ export interface GitHubOAuthResult { function canonicalJSON(obj: Record): string { const sortedObj: Record = {} - Object.keys(obj).sort().forEach(key => { + Object.keys(obj).sort((a, b) => a.localeCompare(b)).forEach(key => { sortedObj[key] = obj[key] }) return JSON.stringify(sortedObj)