T
TanStack7mo ago
variable-lime

Tanstack Start + Firebase Auth

Hey friends, for the life of me I cannot find the needle in the haystack here. Looking for some hand-holding ❤️ I'm trying to setup Firebase auth with TSS. Unlike supabase, FB auth is only client side, so I'm trying to wrap my RootDocument with a context provider.
function RootComponent() {
const { user } = useAuthContext();
return (
<AuthContextProvider>
<RootDocument>
<Outlet />
</RootDocument>
</AuthContextProvider>
);
}
function RootComponent() {
const { user } = useAuthContext();
return (
<AuthContextProvider>
<RootDocument>
<Outlet />
</RootDocument>
</AuthContextProvider>
);
}
export const AuthContextProvider = ({ children }: any) => {
const [user, setUser] = React.useState<any>(null);
const [loading, setLoading] = React.useState(false);

React.useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
console.log("USER FOUND!", user);
setUser(user);
} else {
console.log("NO USER FOUND!");
setUser(null);
}
setLoading(false);
});

return () => unsubscribe();
}, []);

return (
<AuthContext.Provider value={{..user }}>
{loading ? <div>Loading...</div> : children}
</AuthContext.Provider>
);
};
export const AuthContextProvider = ({ children }: any) => {
const [user, setUser] = React.useState<any>(null);
const [loading, setLoading] = React.useState(false);

React.useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
console.log("USER FOUND!", user);
setUser(user);
} else {
console.log("NO USER FOUND!");
setUser(null);
}
setLoading(false);
});

return () => unsubscribe();
}, []);

return (
<AuthContext.Provider value={{..user }}>
{loading ? <div>Loading...</div> : children}
</AuthContext.Provider>
);
};
I'm setting my root with context
export const Route = createRootRouteWithContext<{
user: any;
queryClient: QueryClient;
}>()({
export const Route = createRootRouteWithContext<{
user: any;
queryClient: QueryClient;
}>()({
but I never get an updated context on my /_authed.tsx route.
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
console.log("Checking if user is authenticated...");
if (!context.user) {
console.log("User is not authenticated!");
throw redirect({ to: "/login" });
}
console.log("User is authenticated!", context.user);
},
errorComponent: ({ error }) => {
throw error;
},
});
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
console.log("Checking if user is authenticated...");
if (!context.user) {
console.log("User is not authenticated!");
throw redirect({ to: "/login" });
}
console.log("User is authenticated!", context.user);
},
errorComponent: ({ error }) => {
throw error;
},
});
23 Replies
variable-lime
variable-limeOP7mo ago
Here's what I've tried on the client-side login screen..
const handleSignIn = async (provider: "github" | "apple" | "google") => {
try {
const providers = {
google: new GoogleAuthProvider(),
github: new GithubAuthProvider(),
apple: new OAuthProvider("apple"),
};

const typedProvider =
providers[provider] ??
(() => {
throw new Error("Invalid provider");
})();

await signInWithPopup(auth, typedProvider);

// Invalidate the auth query
// await queryClient.invalidateQueries(authQueries.user());
router.invalidate();
router.navigate({ to: "/" });
} catch (error) {
console.error("Sign in error:", error);
}
};
const handleSignIn = async (provider: "github" | "apple" | "google") => {
try {
const providers = {
google: new GoogleAuthProvider(),
github: new GithubAuthProvider(),
apple: new OAuthProvider("apple"),
};

const typedProvider =
providers[provider] ??
(() => {
throw new Error("Invalid provider");
})();

await signInWithPopup(auth, typedProvider);

// Invalidate the auth query
// await queryClient.invalidateQueries(authQueries.user());
router.invalidate();
router.navigate({ to: "/" });
} catch (error) {
console.error("Sign in error:", error);
}
};
I see in the client-side console the user is found in my auth-provider, but I don't ever see it getting reset in the context thus, my /_authed.tsx never redirects me to "/" Hoping I'm not too far away from a solution? 🙂 Hey @Manuel Schiller any chance you can throw some eyes on this one? I think my ticket got lost in the sauce 🙂
fair-rose
fair-rose7mo ago
ticket?😆 do you have a complete example?
variable-lime
variable-limeOP7mo ago
I can get a code blitz up, I think this might be less about an error and more about education TBH. Though, I might be hitting some same issues as https://discord.com/channels/719702312431386674/1342288291134836848 TLDR; trying to use firebase auth, which is mostly client side. I through a context provider around my root doc but unsure on how to get it in my route context.
fair-rose
fair-rose7mo ago
never used firebase auth are there any examples for remix or nextjs available that could be adapted maybe?
variable-lime
variable-limeOP7mo ago
ooooh, let me look for a remix example. What'd be the tss equivalent of "https://remix.run/docs/en/1.19.3/tutorials/blog#actions" ?
variable-lime
variable-limeOP7mo ago
ServerFn? Challenging to make a stackblitz for firebase TBH lol,
fair-rose
fair-rose7mo ago
yeah server functions
variable-lime
variable-limeOP7mo ago
let me run down that route and see what happens 🙂 Might be close to a way forward ... TLDR; in case anyone else is trying this: 1) I have two firebase auths setup, Admin auth (server) + regular auth (client sdk) 2) Client side uses the signInWith<thing>, I'm using oauth right now 3) On successful client side signup, it gets the idToken from the user credential (returned from signInWith<thing> object) 4) Immediately calls server function to create a JWT off the session, sets it as a cookie. 5) Login route then redirects to _authed. 6) _root beforeLoader runs a server function that gets the cookie, reverses that to get the user
const userIdToken = await serverAuth.verifySessionCookie(jwt);
const user = await serverAuth.getUser(userIdToken.uid);

if (!user) {
return { isAuthenticated: false, user: null };
}

return AuthStateSchema.parse({
isAuthenticated: true,
user: {
uid: user.uid,
email: user.email,
displayName: user.displayName,
photoURL: user.photoURL,
},
});
const userIdToken = await serverAuth.verifySessionCookie(jwt);
const user = await serverAuth.getUser(userIdToken.uid);

if (!user) {
return { isAuthenticated: false, user: null };
}

return AuthStateSchema.parse({
isAuthenticated: true,
user: {
uid: user.uid,
email: user.email,
displayName: user.displayName,
photoURL: user.photoURL,
},
});
fair-rose
fair-rose7mo ago
would be nice if you can provide a complete example once you are done implementing in case someone else looks for the same thing
variable-lime
variable-limeOP7mo ago
am I understanding router.invalidate() correctly? I'm assuming if I call this, it would force my _authed.tsx beforeLoader to re-run?
fair-rose
fair-rose7mo ago
yes
variable-lime
variable-limeOP7mo ago
hmm... I'm not seeing that happen
fair-rose
fair-rose7mo ago
is it currently rendered?
variable-lime
variable-limeOP7mo ago
import { Button } from "@/components/ui/button";
import { signOut } from "@/services/auth.api";
import { createFileRoute, useRouter } from "@tanstack/react-router";

export const Route = createFileRoute("/_authed/")({
component: RouteComponent,
});

function RouteComponent() {
const router = useRouter();

const handleSignout = async () => {
console.log("Signing out!");
await signOut();
router.invalidate();
};


return (
<div>
Hello "/"!
<Button onClick={handleSignout}>Sign Out</Button>
</div>
);
}
import { Button } from "@/components/ui/button";
import { signOut } from "@/services/auth.api";
import { createFileRoute, useRouter } from "@tanstack/react-router";

export const Route = createFileRoute("/_authed/")({
component: RouteComponent,
});

function RouteComponent() {
const router = useRouter();

const handleSignout = async () => {
console.log("Signing out!");
await signOut();
router.invalidate();
};


return (
<div>
Hello "/"!
<Button onClick={handleSignout}>Sign Out</Button>
</div>
);
}
Clicking the signOut button, I see it call my server function which returns
ServerFn Request: app_services_auth_api_ts--signOut_createServerFn_handler
ServerFn Response: 200
- Payload: {"result":{"isAuthenticated":false,"user":null},"error":{"$undefined":0},"context":{}}
ServerFn Request: app_services_auth_api_ts--signOut_createServerFn_handler
ServerFn Response: 200
- Payload: {"result":{"isAuthenticated":false,"user":null},"error":{"$undefined":0},"context":{}}
But it appears the console logs in browser still show it's authenticated (stale objs?)
variable-lime
variable-limeOP7mo ago
on login - hits server function, on logout, only hits my sign out server function. I'd guess I'd expect my _root beforeLoader to attempt to get the user again since I called router.invalidate() and I don't see it hitting my server function again
No description
fair-rose
fair-rose7mo ago
really hard to debug like this, ideally provide a github repo with a complete setup WITHOUT any actual firebase auth so just fake that
variable-lime
variable-limeOP7mo ago
Yeah, let me see if I can get that posted quickly 🙂 Oh interesting, I hooked things up to a zustand store to mock the auth / state functions & it appears to be running fine.
variable-lime
variable-limeOP7mo ago
No description
No description
variable-lime
variable-limeOP7mo ago
Definitely a race-case right now with firebase, invalidation (thus checking the user state) is happening before firebase has finished and returning. If I login, then refresh browser it redirects fine. Logout, then refresh browser, it redirects fine. Sorry for the extra verbose comms in here, I'm hoping that hiding in a thread will help someone else in the future 🙂
fair-rose
fair-rose7mo ago
no worries, just don't expect much help right now 😆 if you have concrete questions about start etc let me know
variable-lime
variable-limeOP7mo ago
1 specific start question cropped up, dropped it as a proper question in the channel in case others face similar issue
variable-lime
variable-limeOP7mo ago
alright!! I think I finally have it all sorted out... I'm sure I have some missing auth middleware, but would love a quick glance at this setup..if it's looking decent, I can throw together a quick example for the community. TLDR; It follows a pretty basic session cookie setup. https://stackblitz.com/edit/tanstack-router-t2n9inda?file=app%2Fservices%2Fauth.api.ts @Manuel Schiller I'd love your take on this approach, if you think this is looking good... I can help form it into a starter I'm sure. Lowkey hoping someone much smarter than me can run with this 😂
Andrew Peacock
StackBlitz
Router Start Basic Auth Example (forked) - StackBlitz
Run official live example code for Router Start Basic Auth, created by Tanstack on StackBlitz
fair-rose
fair-rose7mo ago
looks ok to me!

Did you find this page helpful?