[▲ Vercel Community](/) · [Categories](/categories) · [Latest](/latest) · [Top](/top) · [Live](/live) [Help](/c/help/9) # Custom OAuth implementation fails in production with Fastify and Bun 34 views · 0 likes · 3 posts Pancakes Rock (@fgo37333-2673) · 2026-02-21 Hello everyone, Recently I decedided to try and roll my own auth for a project because I wanted to learn how to do such. I was toying with `OAuth` and `OIDC` and seen that my `OAuth` works entirely and completely fine locally, but breaks in prod. \## Behavior Expected And Received Below. Both are the same commit: Code: OAuth.ts \`\`\`typescript import { Axios } from "axios"; import { eq } from "drizzle-orm"; import { oauthTable } from "./db/schema"; import { createRemoteJWKSet, jwtVerify } from "jose"; import { drizzle } from 'drizzle-orm/neon-http'; import { neon } from "@neondatabase/serverless"; const dbURL = process.env.DATABASE_URL as string const neonSQL = neon(dbURL) const db = drizzle(neonSQL) export async function callback(provider: string): Promise<(() => OAuth) | undefined> { const providerInfo = (await db.select().from(oauthTable).where(eq(oauthTable.provider, provider)))\[0\] switch (provider) { case "discord": if (!providerInfo) { throw new Error("No Provider Info") } return () => { const secret = process.env.DISCORD_SECRET if (!secret) { console.log("No Discord Secret") process.emitWarning("No Discord Secret") process.exit(1) } return new OAuth(new Axios({ headers: { "User-Agent": "Pancakes Cue System/1" } }), providerInfo.authorizeUrl, providerInfo.clientId, secret, providerInfo.tokenUrl, providerInfo.userInfoUrl ?? "", providerInfo.redirectUri, (data) => { return { userId: data.id, email: data.email } }) } case "google": if (!providerInfo) { throw new Error("No Provider Info") } return () => { const secret = process.env.GOOGLE_SECRET if (!secret) { console.log("No GOOGLE Secret") process.emitWarning("No GOOGLE Secret") process.exit(1) } const auth = new OAuth(new Axios({ headers: { "User-Agent": "Pancakes Cue System/1" } }), providerInfo.authorizeUrl, providerInfo.clientId, secret, providerInfo.tokenUrl, providerInfo.userInfoUrl ?? "", providerInfo.redirectUri, async (data, axios) => { const jwt = await jwtVerify(data.id_token, createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs")), { issuer: 'https://accounts.google.com', audience: providerInfo.clientId }) if (!jwt.payload.sub || !jwt.payload.email) throw new Error("Invalid JWT Payload"); return { userId: jwt.payload.sub, email: String(jwt.payload.email) } }) auth.skipUserEndpoint = true return auth } case "auth0": if (!providerInfo) { throw new Error("No Provider Info") } return () => { const secret = process.env.AUTH0_SECRET if (!secret) { console.log("No AUTH0 Secret") process.emitWarning("No AUTH0 Secret") process.exit(1) } const auth = new OAuth(new Axios({ headers: { "User-Agent": "Pancakes Cue System/1" } }), providerInfo.authorizeUrl, providerInfo.clientId, secret, providerInfo.tokenUrl, providerInfo.userInfoUrl ?? "", providerInfo.redirectUri, async (data, axios) => { return { userId: data.sub, email: data.email } }) return auth } default: return undefined }; } type parser = (data: any, axios: Axios) => { userId: string; email: string } | Promise<{ userId: string; email: string }>; type OAuthUser = { userId: string, email: string } | { err: string, error?: any } export class OAuth { #parser: parser; readonly clientId: string; #clientSecret: string; #tokenUrl: string; #userInfoUrl: string; #redirectUri: string; #stateSecret: string = ""; #baseUrl: string; #axiosInstance: Axios; skipUserEndpoint: boolean = false; readonly requestId: string = crypto.randomUUID(); constructor( axios: Axios, authorizationUrl: string, clientId: string, clientSecret: string, tokenUrl: string, userInfoUrl: string, redirectUri: string, parser: parser, ) { this.#axiosInstance = axios; this.#parser = parser; this.clientId = clientId; this.#clientSecret = clientSecret; this.#tokenUrl = tokenUrl; this.#userInfoUrl = userInfoUrl; this.#redirectUri = redirectUri; this.#baseUrl = authorizationUrl; } private async generatePKCE(): Promise<{ verifier: string; challenge: string; }> { const verifier = crypto.randomUUID().replace(/-/g, "") + crypto.randomUUID().replace(/-/g, ""); const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest("SHA-256", data); const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/\\+/g, "-") .replace(/\\//g, "\_") .replace(/=+$/, ""); return { verifier, challenge }; } public verifyState(state: string): boolean { return state === this.#stateSecret } public async generateURL(scope: string | string\[\]): Promise<string> { this.#stateSecret = crypto.randomUUID(); const params = new URLSearchParams({ client_id: this.clientId, redirect_uri: this.#redirectUri, response_type: "code", state: this.#stateSecret, scope: Array.isArray(scope) ? scope.join(" ") : scope, }); return \`${this.#baseUrl}?${params}\`; } public async getUserInfo(code: string): Promise<OAuthUser> { const body = new URLSearchParams({ grant_type: "authorization_code", client_id: this.clientId, client_secret: this.#clientSecret, code, redirect_uri: this.#redirectUri, }); try { const tokenRes = await this.#axiosInstance.post(this.#tokenUrl, body.toString()); const token = JSON.parse(tokenRes.data); console.log("Token request body:", body.toString()); console.log("Token response:", token); if (!token.access_token) return { err: "Error Requesting Token", error: token }; if (!this.skipUserEndpoint) { const userRes = await this.#axiosInstance.get(this.#userInfoUrl, { headers: { Authorization: \`Bearer ${token.access_token}\`, "Content-Type": "application/x-www-form-urlencoded" }, }); return this.#parser(JSON.parse(userRes.data), this.#axiosInstance as any); } else { return this.#parser(token, this.#axiosInstance as any); } } catch (err: any) { return { err: "Auth Request Failed", error: err.response?.data || err.message }; } } public async saveState(redis: Bun.RedisClient) { try { await redis.set("STATE\_" + this.requestId, this.#stateSecret, "EX", 300) } catch (e) { console.error("Failed to save OAuth state to Redis:", e) throw e } } public async retrieveState(redis: Bun.RedisClient, id: string): Promise<boolean> { const state = await redis.get("STATE\_" + id) if (!state) { return false } this.#stateSecret = state return true } } \`\`\` Routes: \`\`\`typescript api.get("/oauth/:provider/callback", async (req, res) => { const { code, state } = req.query as { code: string; state: string } const { provider } = req.params as { provider: string } const { requestId } = req.cookies as { requestId: string } const providerData = (await db.select().from(oauthTable).where(eq(oauthTable.provider, provider)))\[0\] if (!providerData) { return res.code(400).send({ message: "Invalid Provider" }) } const oauthFunction = callback(providerData?.provider ?? "") if (oauthFunction === undefined) { return res.code(400).send({ message: "Invalid Provider" }) } try { const authFunction = (await oauthFunction) if (!authFunction) { return res.code(400).send({ message: "Invalid Provider" }) } const auth = authFunction() if (!state) { return res.send({ err: "No State" }).code(400) } const restored = await auth.retrieveState(redis, requestId) if (!restored) { console.log("Restored?: " + restored) console.log("Fetched Redis Response: " + await redis.get("STATE\_" + requestId)) console.log("State Provided: " + state) return res.code(400).send({ err: "Invalid OAuth Session" }) } if (!auth.verifyState(state)) { return res.code(400).send({ err: "State Mismatch" }) } const userInfo = await auth.getUserInfo(code) if ("err" in userInfo) { return res.send(userInfo).code(400) } let targetUser = (await db.select().from(userTable).where(sql\`${userTable.oauthId} @> ${JSON.stringify(\[{ providerId: providerData.id, userId: userInfo.userId }\])}::jsonb\`))\[0\] if (!targetUser) { const emailUser = (await db.select().from(userTable).where(eq(userTable.username, userInfo.email)))\[0\] if (emailUser) { const updatedOauthIds = \[...(emailUser.oauthId ?? \[\]), { providerId: providerData.id, userId: userInfo.userId }\]; await db.update(userTable) .set({ oauthId: updatedOauthIds }) .where(eq(userTable.id, emailUser.id)); targetUser = emailUser; } else { const newUserId = crypto.randomUUID(); await db.insert(userTable).values({ id: newUserId, username: userInfo.email, passwordHash: "OAUTH", oauthId: \[{ providerId: providerData.id, userId: userInfo.userId }\] }); targetUser = (await db.select().from(userTable).where(eq(userTable.id, newUserId)))\[0\]; } } if (!targetUser) return res.code(500).send({ message: "Failed to create user" }); // Fetch the user safely const getChannelNames = async (userId: string, table: any) => { const ids = (await db.select().from(table).where(eq(table.userId, userId)))\[0\]?.channelIds ?? \[\] const names: string\[\] = \[\] for (const id of ids) { const data = (await db.select().from(channelTable).where(eq(channelTable.id, id)))\[0\] if (data) names.push(data.name) } return names } const sendChannelNames = await getChannelNames(targetUser.id, userSend); const receiveChannelNames = await getChannelNames(targetUser.id, userRecieve); res.setCookie("token", await createJWT(targetUser.id, receiveChannelNames, sendChannelNames, "12h"), { path: "/", domain: domain, secure: isSecure, sameSite: "lax" }) } catch (e) { console.error(e) return res.code(500).send({ message: "Internal Server Error" }) } res.clearCookie("requestId", { path: "/" }) res.redirect("/cue") }) api.get("/oauth/:provider", async (req, res) => { const { provider } = req.params as { provider: string } const providerData = (await db.select().from(oauthTable).where(eq(oauthTable.provider, provider)))\[0\] if (!providerData) { return res.code(400).send({ message: "Invalid Provider" }) } const axios = new Axios() const auth = new OAuth(axios, providerData.authorizeUrl, providerData.clientId, "", providerData.tokenUrl, "", providerData.redirectUri, () => { return { userId: "", email: "" } }) const uri = await auth.generateURL(providerData?.scopes) await auth.saveState(redis) res.clearCookie("requestId", { path: "/" }) res.setCookie("requestId", auth.requestId, { path: "/", domain: domain, secure: isSecure, sameSite: "lax", }) return res.status(302).redirect(uri) }) \`\`\` I know my implementation is bad. But if you could help that’d be stellar! And In Vercel. I can see 2 requests, the one that gets sent to the client is the Invalid one, the other is proper and valid. Swarnava Sengupta (@swarnava) · 2026-02-21 Can you redeploy and try again? We recently fixed a similar bug. Pancakes Rock (@fgo37333-2673) · 2026-02-21 Just redeployed and now it’s fixed! Thanks for the help!