sneakyf1shy
sneakyf1shy4mo ago

No auth provider found

since around yesterday, my already working auth setup suddenly broke and now I can’t use auth at all. The setup is custom JWT using better auth JWKS
Uncaught Error: No auth provider found matching the given token
at async <anonymous> (../../convex/chat_http_action.ts:60:13)
at async invokeFunction (../../node_modules/convex/src/server/impl/registration_impl.ts:80:11)
at async invokeHttpAction (../../node_modules/convex/src/server/impl/registration_impl.ts:442:0)
at async <anonymous> (../../node_modules/convex/src/server/router.ts:322:16)
Uncaught Error: No auth provider found matching the given token
at async <anonymous> (../../convex/chat_http_action.ts:60:13)
at async invokeFunction (../../node_modules/convex/src/server/impl/registration_impl.ts:80:11)
at async invokeHttpAction (../../node_modules/convex/src/server/impl/registration_impl.ts:442:0)
at async <anonymous> (../../node_modules/convex/src/server/router.ts:322:16)
seems like there are no more requests to the JWKS endpoint at all
20 Replies
Convex Bot
Convex Bot4mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
sneakyf1shy
sneakyf1shyOP4mo ago
No description
sneakyf1shy
sneakyf1shyOP4mo ago
No description
erquhart
erquhart4mo ago
Are you using @erquhart/convex-better-auth?
sneakyf1shy
sneakyf1shyOP4mo ago
I got it working by changing from localhost to a dev proxy But no, I'm not using convex-better-auth But either way the auth did suddenly stop working which is weird I still feel there might be some bug in convex or something, but at least everything's working for now... Better auth is hosted on a seperate SQL DB, hence using it through custom JWT provider
erquhart
erquhart4mo ago
ah, gotcha If you're interested, the Convex integration lets you run everything through your Convex deployment
dent_arthur_42
dent_arthur_423mo ago
Can you please elaborate? Facing the exact same issue
sneakyf1shy
sneakyf1shyOP3mo ago
eventually realised that using cloud dev means convex can't talk to your jwks endpoint on localhost so i setup a cloudflare tunnel on a public fixed domain, e.g local-dev-tunnel.mydomain.dev -> my local machine and then convex cloud was able to talk to my JWKS on local dev
dent_arthur_42
dent_arthur_423mo ago
This is what my auth.config.js looks like -
const config = {
providers: [
{
type: "customJwt",
applicationID: "http://localhost:3000",
issuer: "http://localhost:3000",
jwks: "http://localhost:3000/api/auth/jwks",
algorithm: "RS256",
},
],
};

export default config;
const config = {
providers: [
{
type: "customJwt",
applicationID: "http://localhost:3000",
issuer: "http://localhost:3000",
jwks: "http://localhost:3000/api/auth/jwks",
algorithm: "RS256",
},
],
};

export default config;
sneakyf1shy
sneakyf1shyOP3mo ago
are you using convex local dev or cloud dev?
dent_arthur_42
dent_arthur_423mo ago
Convex cloud dev
sneakyf1shy
sneakyf1shyOP3mo ago
Then yeah, the cloud dev server can't talk to your machine on localhost aka the remote server tries to contact http://localhost:3000/api/auth/jwks but it won't be able to
dent_arthur_42
dent_arthur_423mo ago
Looks like I'll have to replace all of these. Should I also replace in better auth client?
sneakyf1shy
sneakyf1shyOP3mo ago
export default {
providers: [
{
type: "customJwt",
applicationID: "uncovr-nextjs",
issuer: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
jwks: process.env.NEXT_PUBLIC_BETTER_AUTH_URL + "/api/auth/jwks",
algorithm: "RS256",
},
],
};
export default {
providers: [
{
type: "customJwt",
applicationID: "uncovr-nextjs",
issuer: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
jwks: process.env.NEXT_PUBLIC_BETTER_AUTH_URL + "/api/auth/jwks",
algorithm: "RS256",
},
],
};
this is the way i'm doing it
dent_arthur_42
dent_arthur_423mo ago
import { createAuthClient } from "better-auth/react";
import { anonymousClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */
baseURL: "http://localhost:3000",
plugins: [anonymousClient()],
});
import { createAuthClient } from "better-auth/react";
import { anonymousClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */
baseURL: "http://localhost:3000",
plugins: [anonymousClient()],
});
sneakyf1shy
sneakyf1shyOP3mo ago
and NEXT_PUBLIC_BETTER_AUTH_URL=https://local-dev-tunnel.<redacted>.dev i would change the baseURL and issuer and jwks to use a env variable since it will also be easier to deploy when you get to production
dent_arthur_42
dent_arthur_423mo ago
Can you please also share the hook you are passing to ConvexProviderWithAuth
sneakyf1shy
sneakyf1shyOP3mo ago
// this is only really so i can call fetch to a httpAction using the same token somewhere else in my app
const InternalAuthContext = createContext<{
lastToken: RefObject<string | null>;
}>({
lastToken: { current: null },
});
export const useInternalAuth = () => useContext(InternalAuthContext);

export default function Provider(props: { children: React.ReactNode }) {
const client = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const lastToken = useRef<string | null>(null);

return (
<InternalAuthContext.Provider value={{ lastToken }}>
// somewhere else in the app you can call useConvexAuth
// to get the isLoading and isAuthenticated state of the sync websocket
<ConvexProviderWithAuth client={client} useAuth={useBetterAuth}>
<ConvexQueryCacheProvider
maxIdleEntries={50}
expiration={1000 * 60}
>
{props.children}
</ConvexQueryCacheProvider>
</ConvexProviderWithAuth>
</InternalAuthContext.Provider>
);
}

function useBetterAuth() {
const { isPending, data: session } = useSessionQuery();
const { lastToken } = useInternalAuth();
const fetchAccessToken = useFunction(
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
// slightly messy caching in cookies, but does work pretty well
if (!forceRefreshToken) {
const cookie = document.cookie
.split("; ")
.find((row) => row.startsWith("internal-auth.jwt="));
console.debug(
`[FetchAccessToken] Cookie ${cookie?.split("=")[1]?.slice(0, 16) + "***" + cookie?.split("=")[1]?.slice(-16)}`
);
if (cookie) return cookie.split("=")[1];
}

const token = (await new Promise<string | null>((resolve, reject) => {
client
.getSession({
query: {
disableCookieCache: forceRefreshToken,
},
fetchOptions: {
onResponse: (ctx) => {
resolve(ctx.response.headers.get("set-auth-jwt"));
},
onError: (ctx) => {
reject(ctx.error);
},
},
})
.catch((err) => {
reject(err);
});
})) satisfies string | null;

if (token) {
const in_6_hours = new Date(Date.now() + 6 * 60 * 60 * 1000);
document.cookie = `internal-auth.jwt=${token}; path=/; expires=${in_6_hours.toUTCString()}`;
lastToken.current = token;
}
console.debug(
`[FetchAccessToken] Token ${token?.slice(0, 16) + "***" + token?.slice(-16)}`
);
return token;
}
);

const isAuthenticated = !!session?.user?.id;
console.log(
`[convex] isAuthenticated: ${isAuthenticated}, isPending: ${isPending}`
);

return useMemo(
() => ({
isLoading: isPending,
isAuthenticated,
fetchAccessToken,
}),
[isPending, session, fetchAccessToken]
);
}
// this is only really so i can call fetch to a httpAction using the same token somewhere else in my app
const InternalAuthContext = createContext<{
lastToken: RefObject<string | null>;
}>({
lastToken: { current: null },
});
export const useInternalAuth = () => useContext(InternalAuthContext);

export default function Provider(props: { children: React.ReactNode }) {
const client = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const lastToken = useRef<string | null>(null);

return (
<InternalAuthContext.Provider value={{ lastToken }}>
// somewhere else in the app you can call useConvexAuth
// to get the isLoading and isAuthenticated state of the sync websocket
<ConvexProviderWithAuth client={client} useAuth={useBetterAuth}>
<ConvexQueryCacheProvider
maxIdleEntries={50}
expiration={1000 * 60}
>
{props.children}
</ConvexQueryCacheProvider>
</ConvexProviderWithAuth>
</InternalAuthContext.Provider>
);
}

function useBetterAuth() {
const { isPending, data: session } = useSessionQuery();
const { lastToken } = useInternalAuth();
const fetchAccessToken = useFunction(
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
// slightly messy caching in cookies, but does work pretty well
if (!forceRefreshToken) {
const cookie = document.cookie
.split("; ")
.find((row) => row.startsWith("internal-auth.jwt="));
console.debug(
`[FetchAccessToken] Cookie ${cookie?.split("=")[1]?.slice(0, 16) + "***" + cookie?.split("=")[1]?.slice(-16)}`
);
if (cookie) return cookie.split("=")[1];
}

const token = (await new Promise<string | null>((resolve, reject) => {
client
.getSession({
query: {
disableCookieCache: forceRefreshToken,
},
fetchOptions: {
onResponse: (ctx) => {
resolve(ctx.response.headers.get("set-auth-jwt"));
},
onError: (ctx) => {
reject(ctx.error);
},
},
})
.catch((err) => {
reject(err);
});
})) satisfies string | null;

if (token) {
const in_6_hours = new Date(Date.now() + 6 * 60 * 60 * 1000);
document.cookie = `internal-auth.jwt=${token}; path=/; expires=${in_6_hours.toUTCString()}`;
lastToken.current = token;
}
console.debug(
`[FetchAccessToken] Token ${token?.slice(0, 16) + "***" + token?.slice(-16)}`
);
return token;
}
);

const isAuthenticated = !!session?.user?.id;
console.log(
`[convex] isAuthenticated: ${isAuthenticated}, isPending: ${isPending}`
);

return useMemo(
() => ({
isLoading: isPending,
isAuthenticated,
fetchAccessToken,
}),
[isPending, session, fetchAccessToken]
);
}
it probably isn't the best solution (i'm no self-proclaimed react expert) , and you can probably drop the useInternalAuth context unless you need to similarly use httpActions
dent_arthur_42
dent_arthur_423mo ago
Thank you so much! I'll post my own setup once I get it working.
sneakyf1shy
sneakyf1shyOP3mo ago
no problem! should work well with any client side react app (i'm using react-router in declarative mode) if you are using something like nextjs, you can also optionally add a initial token from SSR

Did you find this page helpful?