Custom OAuth2 Provider with Expo and Universal Links

I’ve implemented a custom OAuth2 OpenID provider (Vipps Login) using Better Auth and Expo. The flow isn’t pure browser-based, during login, the user is redirected from my app to the Vipps app for confirmation and then back to the Better Auth callback (/auth/oauth2/callback/vipps). The flow works fine when I let Vipps redirect back to Safari:
1. App → Browser.openAuthSessionAsync() → Vipps page → user opens Vipps app → approves login
2. Vipps redirects to https://api.mydomain.com/auth/oauth2/callback/vipps?state=xxx&code=xxx
3. That endpoint validates state, redirects to myapp:///?cookie=xxx, Safari shows the “Open in App?” alert → app is opened. In the final step, my app receives the cookie query param and sets it like this:
authStorage.setItem(
`${Constants.expoConfig?.scheme as string}_cookie`,
getSetCookie(String(cookie))
);
auth.$store.notify("$sessionSignal");
authStorage.setItem(
`${Constants.expoConfig?.scheme as string}_cookie`,
getSetCookie(String(cookie))
);
auth.$store.notify("$sessionSignal");
This works fine, but the only downside is that Safari briefly opens Safari and the OS‑style "Open App?" alert. I tried to improve that using Universal Links so Vipps could redirect directly back to my app instead of via Safari. That part works UX‑wise, but then I get a state_mismatch from Better Auth during this callback:
await auth.oauth2.callback["vipps" as ":providerid"]({
query: { code: String(code), state: String(state) },
fetchOptions: {
onResponse: (context) => console.log("onResponse", JSON.stringify(context, null, 2)),
onError: (context) => console.log("onError", JSON.stringify(context, null, 2)),
onSuccess: (context) => console.log("onSuccess", JSON.stringify(context, null, 2)),
},
});
await auth.oauth2.callback["vipps" as ":providerid"]({
query: { code: String(code), state: String(state) },
fetchOptions: {
onResponse: (context) => console.log("onResponse", JSON.stringify(context, null, 2)),
onError: (context) => console.log("onError", JSON.stringify(context, null, 2)),
onSuccess: (context) => console.log("onSuccess", JSON.stringify(context, null, 2)),
},
});
After inspecting Better Auth’s code, I see it expects a signed state cookie to be present to compare against, but in the universal‑link flow, that cookie isn’t set (since the app, not Safari, calls the callback). Any ideas on how to solve this, while also keeping it secure? Would like to avoid opening Safari for better UX...
0 Replies
No replies yetBe the first to reply to this messageJoin

Did you find this page helpful?