uma
uma2mo ago

callback, 3rd party authentication, convexAuth

I am running into a problem with Figma Oauth 2.0 callback which returns with ?code=&state= appended to the return URL. Now, the ?code part of it is getting consumed by ConvexAuth. I'd love some help on getting around it. I edited my middleware.ts to do the following:
export default convexAuthNextjsMiddleware((request) => {
const url = new URL(request.url)
console.log('Middleware full URL:', url.href)
// Intercept the Figma callback route to rewrite "code" as "figmaCode"
if (url.pathname.startsWith('/settings/profile/figma-auth-callback') && url.searchParams.has('code')) {
const code = url.searchParams.get('code')!
url.searchParams.delete('code')
url.searchParams.set('figmaCode', code)
// Redirect to the same URL with the renamed query parameter.
return NextResponse.redirect(url)
}
if (!isPublicPage(request as NextRequest) && !isAuthenticatedNextjs()) {
return nextjsMiddlewareRedirect(request as NextRequest, '/auth')
}
})
export default convexAuthNextjsMiddleware((request) => {
const url = new URL(request.url)
console.log('Middleware full URL:', url.href)
// Intercept the Figma callback route to rewrite "code" as "figmaCode"
if (url.pathname.startsWith('/settings/profile/figma-auth-callback') && url.searchParams.has('code')) {
const code = url.searchParams.get('code')!
url.searchParams.delete('code')
url.searchParams.set('figmaCode', code)
// Redirect to the same URL with the renamed query parameter.
return NextResponse.redirect(url)
}
if (!isPublicPage(request as NextRequest) && !isAuthenticatedNextjs()) {
return nextjsMiddlewareRedirect(request as NextRequest, '/auth')
}
})
But that doesn't fix it either. I couldn't use this workaround because I am not using tanstack server: https://github.com/get-convex/convex-auth/issues/145 The work around would be to break out a chunk of code out from ConvexAuthNextjsServerProvider and that is painful. Integration with a third party is common, so what could I be missing? This has to be solved elegantly. A movement on this thread: https://github.com/get-convex/convex-auth/issues/145 as suggested by @ballingt and @sshader would work.
GitHub
Convex Auth consumes pages that have "code" as search param · Issu...
Came across this curious case recently: A user is created and authenticated through Convex Auth, no problem, no sweat. As a business rule, that user needs to be able to connect a Mailchimp account ...
33 Replies
erquhart
erquhart2mo ago
@uma I'm going to see about providing some kind of escape hatch for this, will update. Proposal: Handle custom OAuth flows If you have custom OAuth flows that use the code query parameter (like Figma OAuth), you can prevent Convex Auth from handling those codes by providing a shouldHandleCode callback:
ts filename="middleware.ts"
export default convexAuthNextjsMiddleware(
(request, { convexAuth }) => {
// ...
},
{
shouldHandleCode: (request) => {
// Don't handle code parameter for Figma OAuth callback
if (
request.nextUrl.pathname.startsWith(
"/settings/profile/figma-auth-callback",
)
) {
return false;
}
// Handle all other code parameters
return true;
},
},
);
ts filename="middleware.ts"
export default convexAuthNextjsMiddleware(
(request, { convexAuth }) => {
// ...
},
{
shouldHandleCode: (request) => {
// Don't handle code parameter for Figma OAuth callback
if (
request.nextUrl.pathname.startsWith(
"/settings/profile/figma-auth-callback",
)
) {
return false;
}
// Handle all other code parameters
return true;
},
},
);
The callback receives the request object and should return true if Convex Auth should handle the code parameter, or false if it should be left alone for your custom OAuth flow to handle.
uma
umaOP2mo ago
I can use this, yes!!!
erquhart
erquhart2mo ago
I'm going to publish a fork with this included, can you test? cool, that'll be quicker than setting up a repro. just a minute Alright you can do:
npm i @convex-dev/auth@npm:@erquhart/convex-auth@0.0.82-beta.1
npm i @convex-dev/auth@npm:@erquhart/convex-auth@0.0.82-beta.1
That will install the fork under the alias of the original package name so you don't have to update your imports
uma
umaOP2mo ago
Never done this: installing fork under same alias, so this feels cool. Will report back. Broke something..fixing. Issues are on my end. I am on it.
Sheng
Sheng2mo ago
How about this? It will check the pathname first, if matched then no need to go through the convex auth middleware.
export const middleware = async (req: NextRequest, event: NextFetchEvent) => {
if (req.nextUrl.pathname.startsWith("/settings/profile/figma-auth-callback")) {
// Do something here if needed
return NextResponse.next()
}

return convexAuthNextjsMiddleware(
async (request, { convexAuth }) => {
// ...
},
)(req, event)
}
export const middleware = async (req: NextRequest, event: NextFetchEvent) => {
if (req.nextUrl.pathname.startsWith("/settings/profile/figma-auth-callback")) {
// Do something here if needed
return NextResponse.next()
}

return convexAuthNextjsMiddleware(
async (request, { convexAuth }) => {
// ...
},
)(req, event)
}
uma
umaOP2mo ago
This works - I had some outdated packages, installed them and now I receive the callback 'code' and my middleware.ts code that is replacing code with figmaCode as query param - that's successful too! Whoa! Super happy. Didn't try your suggestion, Sheng, but that should work too - saying this from reading the code only.
Sheng
Sheng2mo ago
Yes, basically is just run some code before reaching the convex auth middleware
uma
umaOP2mo ago
@erquhart i also get some warnings because of the new code. Is there a github thread where I should be tabulating them?
erquhart
erquhart2mo ago
Here is fine for now
uma
umaOP2mo ago
I will send some reports tonight - have to prepare for a demo going to take place at Convex this evening! 😎 Hi Shawn, here are some warnings/errors I see in my terminal:
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
⨯ unhandledRejection: Error: Server Error: could not find api.auth.isAuthenticated. convex-auth 0.0.76 introduced a new export in convex/auth.ts. Add `isAuthenticated` to the list of functions returned from convexAuth(). See convex-auth changelog for more https://github.com/get-convex/convex-auth/blob/main/CHANGELOG.md
at isAuthenticated (webpack-internal:///(middleware)/./node_modules/.pnpm/@erquhart+convex-auth@0.0.82-beta.1_@auth+core@0.37.4_convex@1.17.0_react-dom@18.3.1_react@18_wxvxbogbwybgxysh7jrykisbni/node_modules/@erquhart/convex-auth/dist/nextjs/server/index.js:182:19)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
⨯ unhandledRejection: Error: Server Error: could not find api.auth.isAuthenticated. convex-auth 0.0.76 introduced a new export in convex/auth.ts. Add `isAuthenticated` to the list of functions returned from convexAuth(). See convex-auth changelog for more https://github.com/get-convex/convex-auth/blob/main/CHANGELOG.md
at isAuthenticated (webpack-internal:///(middleware)/./node_modules/.pnpm/@erquhart+convex-auth@0.0.82-beta.1_@auth+core@0.37.4_convex@1.17.0_react-dom@18.3.1_react@18_wxvxbogbwybgxysh7jrykisbni/node_modules/@erquhart/convex-auth/dist/nextjs/server/index.js:182:19)
Sheng
Sheng2mo ago
Does your convex/auth.ts file have the isAuthenticated exported like this?
import { convexAuth } from "@convex-dev/auth/server";

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [],
});
import { convexAuth } from "@convex-dev/auth/server";

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [],
});
uma
umaOP2mo ago
No. We do this: export const { auth, signIn, signOut, store } = convexAuth({..
Sheng
Sheng2mo ago
Yea, probably the issue is due to missing isAuthenticated from your export there
uma
umaOP2mo ago
@Sheng that has fixed all of my warnings so far. Going to test out more stuff tomorrow. Just updating.
Sronds
Sronds2w ago
@erquhart left a comment on the PR you made because i can't access the shouldHandleCode prop from <ConvexAuthProvider />
erquhart
erquhart2w ago
Good catch, fixing
Sronds
Sronds2w ago
@erquhart if i'm using the <ConvexAuthProvider shouldhandlecode={...} /> where should i implement my own custom OAuth flow ? is it supposed to be like this?
<ConvexAuthProvider
client={convex}
shouldHandleCode={() => {
if (location.pathname.startsWith("/auth")) {
// run some code ?
}
return true;
}}
<ConvexAuthProvider
client={convex}
shouldHandleCode={() => {
if (location.pathname.startsWith("/auth")) {
// run some code ?
}
return true;
}}
my specific use case is dealing with tiktok oauth flow and running into the same "code" issue as mentioned by OP
erquhart
erquhart2w ago
All this does is stop Convex Auth from processing and removing the code param from the url, how you use the code isn't directly facilitated.
Sronds
Sronds2w ago
hmm does that mean i should override and create a custom /api/auth/callback/tiktok http endpoint? apologies if i'm asking silly questions but i've been stuck on this all day lol it seems like all the processing of the tokens and auth flows is abstracted from us through the various libraries (@convex-dev and/or @auth/core) so it's kind of awkward knowing when or how to add custom logic
Sronds
Sronds2w ago
i've been getting this error specifically '"response" body "access_token" property must be a string
No description
erquhart
erquhart2w ago
If you're just trying to do oauth with Tiktok I'd try using the Auth.js provider. General setup should be the same, might have some oddities to work out. Docs mention using any of the providers, but they're just not all known to work or supported: https://labs.convex.dev/auth/config/oauth#providers
OAuth - Convex Auth
Authentication library for your Convex backend
erquhart
erquhart2w ago
I believe the OP was trying to allow their app users to connect their account from Figma, vs just using Figma to log in the way you're using Tiktok. For login the provider is at least your best starting point.
erquhart
erquhart2w ago
Auth.js | Tiktok
Authentication for the Web
Sronds
Sronds2w ago
I am using the Auth JS provider actually But I’m just using it cause I thought it would be easy to implement, my actual goal is just to get the user to verify they own the TikTok account (was planning to do Instagram & YouTube later too the same way) I already have the user logging in through Gmail this was just me trying to get them to “connect”/“link” their account so that I could verify that they indeed owned the account I wonder if the issue stems from already being logged in and trying to sign in again with TikTok? it's not i'm just doing
const { signIn } = useAuthActions();
...
<Button
onClick={async () => {
await signIn("tiktok")
}}
const { signIn } = useAuthActions();
...
<Button
onClick={async () => {
await signIn("tiktok")
}}
and my auth.ts file looks like this
import Google from "@auth/core/providers/google";
import { ResendOTP } from "./otp/ResendOTP";
import TikTok from "@auth/core/providers/tiktok";
import { convexAuth } from "@convex-dev/auth/server";
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
ResendOTP,
Google,
TikTok({
clientId: process.env.AUTH_TIKTOK_ID,
clientSecret: process.env.AUTH_TIKTOK_SECRET,
}),
],
});
import Google from "@auth/core/providers/google";
import { ResendOTP } from "./otp/ResendOTP";
import TikTok from "@auth/core/providers/tiktok";
import { convexAuth } from "@convex-dev/auth/server";
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
ResendOTP,
Google,
TikTok({
clientId: process.env.AUTH_TIKTOK_ID,
clientSecret: process.env.AUTH_TIKTOK_SECRET,
}),
],
});
the reason i think this error is related to op's message is because i get this as the response after going through the process of confirming the login on tiktok. https://bright-shark-383.convex.site/api/auth/callback/tiktok?code=6Be6A-lAdB9_CRbJ5HvibBKpM44QmFKWLY6cAmmb9hLj1jviZCMzK4iAsy8yVNLQCXXidcRutCH-wS6p2PHynC18hH7dwkfKjx4lPcwL7ndRcg9kba2-nuTes-gvE0zloF0ZvLzOB4bYbBKOkaXujrotbLW-qqZR688A8aYVYEayg0zqcSG7iZUALAss6lmQ2JsS5QkYfWVbZ2WdqFmsyML5F4MHVofQT_5KSFvf04w%2A2%215730.e1&scopes=user.info.basic%2Cuser.info.profile but then for some reason it fails and returns the error '"response" body "access_token" property must be a string and the full flow never completes and i thought it could be related to the code param @erquhart
erquhart
erquhart2w ago
What does it do if you return false from shouldHandleCode?
Sronds
Sronds2w ago
your pr hasn't been merged in yet but i tried patching it myself but didn't get much luck with getting a different error message although it could've been because i didn't patch it properly imma try again real quick yeah still getting the same error even when returning false
Sronds
Sronds2w ago
this is my patch file
Sronds
Sronds2w ago
i've traced it to this error:
'OAuth Token Response Body:' '{"error":"invalid_request","error_description":"The request parameters are malformed.","log_id":"202505081903505897F995CEADA71635F2"}'
'OAuth Token Response Body:' '{"error":"invalid_request","error_description":"The request parameters are malformed.","log_id":"202505081903505897F995CEADA71635F2"}'
erquhart
erquhart2w ago
GitHub
Tiktok error: "The request parameters are malformed." · nextauthjs...
Environment Local Reproduction URL https://github.com/eidriahn/next-auth-example Describe the issue When trying to login with the TikTok provider, during the code/token exchange an error is thrown ...
Sronds
Sronds2w ago
that. worked. tysm @erquhart, i was going mad ❤️ ❤️ ❤️ if anyone in the future is struggling with this, the fix is to go to node_modules/@convex-dev/auth/dist/server/oauth/callback.js and update
let codeGrantResponse = await o.authorizationCodeGrantRequest(as, client, clientAuth, codeGrantParams, redirect_uri, codeVerifier ?? "decoy", {
// TODO: move away from allowing insecure HTTP requests
[o.allowInsecureRequests]: true,
[o.customFetch]: (...args) => {
if (!provider.checks.includes("pkce")) {
args[1].body.delete("code_verifier");
}
return fetchOpt(provider)[o.customFetch](...args);
},
});
let codeGrantResponse = await o.authorizationCodeGrantRequest(as, client, clientAuth, codeGrantParams, redirect_uri, codeVerifier ?? "decoy", {
// TODO: move away from allowing insecure HTTP requests
[o.allowInsecureRequests]: true,
[o.customFetch]: (...args) => {
if (!provider.checks.includes("pkce")) {
args[1].body.delete("code_verifier");
}
return fetchOpt(provider)[o.customFetch](...args);
},
});
to
let codeGrantResponse = await o.authorizationCodeGrantRequest(as, client, clientAuth, codeGrantParams, redirect_uri, codeVerifier ?? "decoy", {
// TODO: move away from allowing insecure HTTP requests
[o.allowInsecureRequests]: true,
[o.customFetch]: (...args) => {
if (!provider.checks.includes("pkce")) {
args[1].body.delete("code_verifier");
}
return fetchOpt(provider)[o.customFetch](...args);
},
additionalParameters: {
client_key: options.provider.clientId,
client_secret: options.provider.clientSecret ?? ""
}
});
let codeGrantResponse = await o.authorizationCodeGrantRequest(as, client, clientAuth, codeGrantParams, redirect_uri, codeVerifier ?? "decoy", {
// TODO: move away from allowing insecure HTTP requests
[o.allowInsecureRequests]: true,
[o.customFetch]: (...args) => {
if (!provider.checks.includes("pkce")) {
args[1].body.delete("code_verifier");
}
return fetchOpt(provider)[o.customFetch](...args);
},
additionalParameters: {
client_key: options.provider.clientId,
client_secret: options.provider.clientSecret ?? ""
}
});
erquhart
erquhart2w ago
Awesome, glad you're up and running! And thanks for sharing the fix!
Sronds
Sronds2w ago
okay just a quick follow up, this flow creates a new user to the database with tiktok as the provider but that's not necessarily what i'm trying to do. My goal is to 'link' the tiktok account to the existing user that had already signed in similar to what OP was trying to do i believe. How different do you reckon the flow should be compared to what we have right now? @erquhart I get the following log after signing in
'defaultCreateOrUpdateUser args:' {
existingAccountId: undefined,
existingSessionId: 'jh7261gqn4nherc0vy4r24nmqx7fh8qt',
...
'defaultCreateOrUpdateUser args:' {
existingAccountId: undefined,
existingSessionId: 'jh7261gqn4nherc0vy4r24nmqx7fh8qt',
...
is there perhaps a way to connect to use that existingSessionId to link the accounts? I managed to solve it by patching the users.js convex implementation.
if (config.callbacks?.createOrUpdateUser !== undefined) {
logWithLevel(LOG_LEVELS.DEBUG, "Using custom createOrUpdateUser callback");
const res = await config.callbacks.createOrUpdateUser(ctx, {
existingUserId,
--> existingSessionId,
...args,
});
---> if (res) {
---> return res;
---> }
}
if (config.callbacks?.createOrUpdateUser !== undefined) {
logWithLevel(LOG_LEVELS.DEBUG, "Using custom createOrUpdateUser callback");
const res = await config.callbacks.createOrUpdateUser(ctx, {
existingUserId,
--> existingSessionId,
...args,
});
---> if (res) {
---> return res;
---> }
}
I now pass the existingSessionId in the callback async createOrUpdateUser(ctx, args) which i then use to check if there's an existing session and use that to link the accounts together. If there is no existing session and the provider is not one of my linking ones, then i just return undefined in the callback so that the defaultCreateOrUpdateUser function continues the logic. My end result looks something like this
async createOrUpdateUser(ctx, args) {
if (args.existingSessionId && args.provider.id === "tiktok") {
const session: Doc<"authSessions"> | undefined | null =
await ctx.db.get(args.existingSessionId);
if (session) {
const user: Doc<"users"> | undefined | null = await ctx.db.get(
session.userId,
);
if (user) {
const creator = await ctx.db
.query("creators")
// @ts-ignore
.withIndex("by_user", (q) => q.eq("userId", user._id))
.first();
if (creator) {
await ctx.db.patch(creator._id, {
tiktokHandle: args.profile.email,
});
return user._id;
} else {
await ctx.db.insert("creators", {
userId: user._id,
tiktokHandle: args.profile.email,
});
return user._id as any;
}
}
}
} else {
// returning undefined will trigger the default implementation
return undefined;
}
}
async createOrUpdateUser(ctx, args) {
if (args.existingSessionId && args.provider.id === "tiktok") {
const session: Doc<"authSessions"> | undefined | null =
await ctx.db.get(args.existingSessionId);
if (session) {
const user: Doc<"users"> | undefined | null = await ctx.db.get(
session.userId,
);
if (user) {
const creator = await ctx.db
.query("creators")
// @ts-ignore
.withIndex("by_user", (q) => q.eq("userId", user._id))
.first();
if (creator) {
await ctx.db.patch(creator._id, {
tiktokHandle: args.profile.email,
});
return user._id;
} else {
await ctx.db.insert("creators", {
userId: user._id,
tiktokHandle: args.profile.email,
});
return user._id as any;
}
}
}
} else {
// returning undefined will trigger the default implementation
return undefined;
}
}
erquhart
erquhart2w ago
Yep that makes sense @Sronds that pr is released, 0.0.84

Did you find this page helpful?