Skip to content

Commit 3f20e88

Browse files
authored
Merge pull request #18 from OriginTrail/fix/oauth-stale-code
Fixed a bug with Oauth flow with a stale login code
2 parents 43a7b0f + d757c2d commit 3f20e88

File tree

2 files changed

+106
-14
lines changed

2 files changed

+106
-14
lines changed

apps/agent/src/app/(protected)/_layout.tsx

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,96 @@
1-
import { useCallback, useEffect, useState } from "react";
1+
import { useCallback, useEffect, useRef, useState } from "react";
22
import { router, Slot, useGlobalSearchParams, usePathname } from "expo-router";
33
import * as SplashScreen from "expo-splash-screen";
4+
import AsyncStorage from "@react-native-async-storage/async-storage";
45

56
import { McpContextProvider } from "@/client";
67
import { useAlerts } from "@/components/Alerts";
78
import { SettingsProvider } from "@/hooks/useSettings";
89

10+
const REDIRECT_COUNT_KEY = "oauth_redirect_count";
11+
const MAX_REDIRECTS = 3;
12+
13+
const getRedirectCount = async (): Promise<number> => {
14+
const count = await AsyncStorage.getItem(REDIRECT_COUNT_KEY);
15+
return count ? parseInt(count, 10) : 0;
16+
};
17+
18+
const incrementRedirectCount = async (): Promise<number> => {
19+
const newCount = (await getRedirectCount()) + 1;
20+
await AsyncStorage.setItem(REDIRECT_COUNT_KEY, newCount.toString());
21+
return newCount;
22+
};
23+
24+
const resetRedirectCount = async (): Promise<void> => {
25+
await AsyncStorage.removeItem(REDIRECT_COUNT_KEY);
26+
};
27+
928
export default function ProtectedLayout() {
1029
const params = useGlobalSearchParams<{ code?: string; error?: string }>();
1130
const { showAlert } = useAlerts();
1231

1332
const [idleMcp, setIdleMcp] = useState(false);
1433
const [idleSettings, setIdleSettings] = useState(false);
34+
const isHandlingError = useRef(false);
1535

1636
const mcpCallback = useCallback(
1737
(error?: Error) => {
1838
setIdleMcp(true);
39+
router.setParams({ code: undefined }); // Always clear code param
1940

20-
if (!error) router.setParams({ code: undefined });
21-
else
22-
showAlert({
23-
type: "error",
24-
title: "MCP Error",
25-
message: error.message,
26-
timeout: 5000,
27-
});
41+
if (!error) {
42+
// Success - reset redirect count
43+
resetRedirectCount();
44+
return;
45+
}
46+
47+
// Prevent concurrent error handling
48+
if (isHandlingError.current) return;
49+
isHandlingError.current = true;
50+
51+
// Check if this is an auth-related error requiring re-authentication
52+
const errorMessage = error.message.toLowerCase();
53+
const isAuthError =
54+
errorMessage.includes("invalid_grant") ||
55+
errorMessage.includes("invalid grant") ||
56+
errorMessage.includes("invalid token") ||
57+
errorMessage.includes("authorization code") ||
58+
errorMessage.includes("oauth client") ||
59+
errorMessage.includes("unauthorized");
60+
61+
if (isAuthError) {
62+
// Auto-redirect with loop protection
63+
(async () => {
64+
try {
65+
const redirectCount = await incrementRedirectCount();
66+
if (redirectCount > MAX_REDIRECTS) {
67+
await resetRedirectCount();
68+
showAlert({
69+
type: "error",
70+
title: "Authentication Failed",
71+
message:
72+
"Unable to authenticate. Please try again later or contact support.",
73+
timeout: 10000,
74+
});
75+
isHandlingError.current = false;
76+
return;
77+
}
78+
router.replace("/authorize");
79+
} catch {
80+
isHandlingError.current = false;
81+
}
82+
})();
83+
return;
84+
}
85+
86+
// For other errors, show alert
87+
showAlert({
88+
type: "error",
89+
title: "Connection Error",
90+
message: error.message,
91+
timeout: 5000,
92+
});
93+
isHandlingError.current = false;
2894
},
2995
[showAlert],
3096
);

apps/agent/src/app/(protected)/login.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Redirect, router, useLocalSearchParams } from "expo-router";
2-
import { useCallback } from "react";
2+
import { useCallback, useState } from "react";
33
import { View } from "react-native";
44
import * as Linking from "expo-linking";
55
import * as SplashScreen from "expo-splash-screen";
@@ -14,8 +14,12 @@ import Footer from "@/components/layout/Footer";
1414
import FormTitle from "@/components/forms/FormTitle";
1515
import LoginForm from "@/components/forms/LoginForm";
1616

17-
const getErrorMessage = (err: any) => {
18-
if (!(err instanceof AuthError)) return "Unknown error occurred!";
17+
const getErrorMessage = (err: any): string => {
18+
if (!(err instanceof AuthError)) {
19+
// Check for OAuth-related errors in raw error messages
20+
const rawMessage = err?.message || String(err);
21+
return rawMessage;
22+
}
1923
switch (err.code) {
2024
case AuthError.Code.INVALID_CREDENTIALS:
2125
return "Invalid username or password";
@@ -28,9 +32,22 @@ const getErrorMessage = (err: any) => {
2832
}
2933
};
3034

35+
// Check if error indicates a stale/invalid authorization code
36+
const isStaleCodeError = (errorMessage: string): boolean => {
37+
const lowerMessage = errorMessage.toLowerCase();
38+
return (
39+
/invalid.*(grant|code|authorization)/i.test(errorMessage) ||
40+
/authorization.*code/i.test(errorMessage) ||
41+
/oauth.*client/i.test(errorMessage) ||
42+
lowerMessage.includes("invalid_grant") ||
43+
lowerMessage.includes("expired")
44+
);
45+
};
46+
3147
export default function Login() {
3248
SplashScreen.hide();
3349
const { code } = useLocalSearchParams<{ code?: string }>();
50+
const [validCode, setValidCode] = useState<string | undefined>(code);
3451

3552
const tryLogin = useCallback(
3653
async ({
@@ -44,7 +61,7 @@ export default function Login() {
4461
}) => {
4562
try {
4663
const url = await login({
47-
code: code ?? "",
64+
code: validCode ?? "",
4865
credentials: { email, password },
4966
rememberMe,
5067
fetch: (url, opts) => fetch(url.toString(), opts as any),
@@ -56,10 +73,19 @@ export default function Login() {
5673
else Linking.openURL(url);
5774
} catch (error) {
5875
const errorMessage = getErrorMessage(error);
76+
77+
// Detect stale authorization code errors
78+
if (isStaleCodeError(errorMessage)) {
79+
router.setParams({ code: undefined });
80+
setValidCode(undefined);
81+
// Generic message - don't expose OAuth internals
82+
throw new Error("Your session has expired. Please log in again.");
83+
}
84+
5985
throw new Error(errorMessage);
6086
}
6187
},
62-
[code],
88+
[validCode],
6389
);
6490

6591
const { connected } = useMcpClient();

0 commit comments

Comments
 (0)