Riki
Riki4mo ago

[React-Native] Custom Auth - JWT refresh issue

Hello, forwarding the comments on this thread to this new one. Problem: When the app is in background/inactive, the JWT can expire during this period. When the app is put in foreground, it seems that the convex queries are rerun before a new JWT is fetched. A symptom we see is that the ctx.auth.getUserIdentity() function returns null on Convex backend.
12 Replies
Riki
RikiOP4mo ago
@sshader I have the exact same problem as @adam , except I use another custom auth (firebase auth in my case).
sshader
sshader4mo ago
To make sure I understand the bug report: * This is a bug in an app using React Native + custom auth (https://docs.convex.dev/auth/advanced/custom-auth) Steps: * Open the app, sign in. Observe that we can load the current user and show it in the app as expected. * Background the app for a while. * Reopen the app - expected: app is still logged in and we still see data about the user I'm not totally clear on the behavior y'all are observing, so going to state two guesses and please tell me which one / correct me: * actual: the app is in the "unauthenticated" state -- UI under the <Authenticated> component does not render, UI under the <Unauthenticated> does. The UI never switches back to authenticated without restarting the app or re-logging in. * actual: the app tries to render UI under the <Authenticated> component, but any queries it requests have auth.getUserIdentity() populated as null. If this is the case, I'm curious if these queries eventually return a non-null UserIdentity (indicating a potential race), or if this state persists (indicating a bug in our state machine). Also want to confirm that this is happening on the latest version of Convex (in particular 1.14.4 and up). I'll prioritize reproducing this myself and digging into what might be happening -- thanks for all the patience so far here.
Custom Auth Integration | Convex Developer Hub
Note: This is an advanced feature! We recommend sticking with the
Riki
RikiOP4mo ago
Hey @sshader sorry for the late reply I have been semi-off recently. I don't use the <Authenticated/> component at all as I don't think it fits well for React-Native and React-Navigation. I don't think it's relevant I open that topic in this thread, but let me know. Instead, I use the convex hook. For the sake of simplicity, this should be the minimal repro
export const App = () => {
const {isLoading} = useConvexAuth();

if (isLoading) {
return (
<View
style={{
flex: 1,
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
}}>
<ActivityIndicator />
</View>
);
} else {
return (<HomeFeed/>)
// renders the app that only consist of an home feed. That inner component calls `getHomeFeed` written below
}
}
export const App = () => {
const {isLoading} = useConvexAuth();

if (isLoading) {
return (
<View
style={{
flex: 1,
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
}}>
<ActivityIndicator />
</View>
);
} else {
return (<HomeFeed/>)
// renders the app that only consist of an home feed. That inner component calls `getHomeFeed` written below
}
}
On the convex back-end side this is more or less what I have:
export const getHomeFeed = query({
handler: async (ctx) => {
const userIdentity = await ctx.auth.getUserIdentity();
if (userIdentity == null) {
throw new ConvexError(ApiError.USER_NOT_AUTHENTICATED);
}
// returns the home feed
}
})
export const getHomeFeed = query({
handler: async (ctx) => {
const userIdentity = await ctx.auth.getUserIdentity();
if (userIdentity == null) {
throw new ConvexError(ApiError.USER_NOT_AUTHENTICATED);
}
// returns the home feed
}
})
The behavior that I am observing is: when the app comes back from background/inactive state and the JWT has expired meanwhile, it seems the HomeFeed component is still mounted, so the query is still running an throw an error. Then on the UI side I need to handle it and display an ErrorBoundary screen to our users which is "suboptimal". So it looks like your last scenario is correct. I am not sure what difference you are asking at the end of your quote What I would expect is that the isLoading status should be true meanwhile the JWT has expired and is being refresh. For the record, this is my auth related hook
import auth from '@react-native-firebase/auth';
import {useCallback, useEffect, useMemo, useState} from 'react';

export function useAuthFromFirebase() {
const [isAuthenticated, setIsAuthenticated] = useState<undefined | boolean>(
undefined,
);

useEffect(() => {
const subscription = auth().onAuthStateChanged(user => {
if (user) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
});
return subscription;
}, []);

const fetchAccessToken = useCallback(
async ({forceRefreshToken}: {forceRefreshToken: boolean}) => {
// Here you can do whatever transformation to get the ID Token
// or null
// Make sure to fetch a new token when `forceRefreshToken` is true
const idToken = await auth().currentUser?.getIdToken(forceRefreshToken);
return idToken ?? null;
},
// If `getToken` isn't correctly memoized
// remove it from this dependency array
[],
);
return useMemo(
() => ({
// Whether the auth provider is in a loading state
isLoading: isAuthenticated == null ? true : false,
// Whether the auth provider has the user signed in
isAuthenticated: isAuthenticated ?? false,
// The async function to fetch the ID token
fetchAccessToken,
}),
[isAuthenticated, fetchAccessToken],
);
}
import auth from '@react-native-firebase/auth';
import {useCallback, useEffect, useMemo, useState} from 'react';

export function useAuthFromFirebase() {
const [isAuthenticated, setIsAuthenticated] = useState<undefined | boolean>(
undefined,
);

useEffect(() => {
const subscription = auth().onAuthStateChanged(user => {
if (user) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
});
return subscription;
}, []);

const fetchAccessToken = useCallback(
async ({forceRefreshToken}: {forceRefreshToken: boolean}) => {
// Here you can do whatever transformation to get the ID Token
// or null
// Make sure to fetch a new token when `forceRefreshToken` is true
const idToken = await auth().currentUser?.getIdToken(forceRefreshToken);
return idToken ?? null;
},
// If `getToken` isn't correctly memoized
// remove it from this dependency array
[],
);
return useMemo(
() => ({
// Whether the auth provider is in a loading state
isLoading: isAuthenticated == null ? true : false,
// Whether the auth provider has the user signed in
isAuthenticated: isAuthenticated ?? false,
// The async function to fetch the ID token
fetchAccessToken,
}),
[isAuthenticated, fetchAccessToken],
);
}
Also want to confirm that this is happening on the latest version of Convex (in particular 1.14.4 and up).
I was on 1.14.3. Let me update convex and keep you updated on this thread if the problem stills exists 👍
kingyml
kingyml4mo ago
Hi so I have a similar issue. I am noticing it occurs specifically when I try to call const identity = await ctx.auth.getUserIdentity(); inside of a query like @Riki on their getHomeFeed function. Using const identity = await ctx.auth.getUserIdentity(); in a mutation seems to work fine. I haven't tried it in an action yet. I am running convex version 1.12.1 I am using clerk and clerk is logged in fine and is sending the user Id however convex seems to not agree that the user is signed in but this is only in queries, works fine in mutations. Sorry I think I just didn't read the docs, I had to wrap my component with <Authenticated> that seemed to work
Riki
RikiOP4mo ago
@sshader yes the problem still exists in 1.16.0 (client and server). I have just received an "expired_token" error appearing on my React-Native application when puting the app in foreground for the first time after the night.
ballingt
ballingt4mo ago
thanks @Riki , sshader has some work in progress on this and is out today, I got to see some of it and wow, auth keeps getting more interesting
adam
adam3mo ago
After updating convex from 1.15.0 to 1.16.3 I have started to get console error of "Auth token is not a valid JWT, cannot refetch the token" raised on app startup. The issue doesn't happen on 1.15.0, so I've rolled back to this version for now. I'm using "expo": "49.0.20" convex-js code that is being triggered is in src/browser/sync/authentication_manager.ts - scheduleTokenRefetch(token: string) function.
sshader
sshader3mo ago
(I'm going to start a separate thread about the last thing because I want to ask some follow ups, but think it's unrelated to earlier stuff in this thread)
Riki
RikiOP3mo ago
No description
Riki
RikiOP3mo ago
@sshader I think you improved the logs of this auth error which is nice. I can still confirm the issue occurs (it serms it occurs less frequently though than ~1 month ago)
adam
adam3mo ago
Could you please add a link to the new thread?

Did you find this page helpful?