S
SolidJS2mo ago
ofj3kso

Auth guard

Hey there. I was trying to find a good solution to handle authentication and set up a guard that will redirect users to the authentication page if they are not authenticated. While reading this discussion (https://github.com/solidjs/solid-router/discussions/364), I couldn't help but notice this comment "This has the 2nd most upvotes of all time with no response" I didn't like so much the solutions, I want to propose mine and get @ryansolid comment on it, as I feel like this is an area with room for improvement, but I'm aware my solution might have blind spots or limitations I haven't considered
import {
Router,
Route,
redirect,
RoutePreloadFuncArgs,
} from "@solidjs/router"

const checkAuth = query(async function (
args: RoutePreloadFuncArgs,
auth: boolean
) {
"use server"

// COULD ALSO GET USER STATUS HERE

if (auth && args.location.pathname === "/auth") {
throw redirect("/")
}

if (!auth && args.location.pathname !== "/auth") {
throw redirect("/auth")
}
},
"checkAuth")

const App: Component = () => {
return (
<Router>
<Route
preload={args => createAsync(() => checkAuth(args, true))}
>
<Route path="/" component={Home} />
<Route path="/auth" component={Auth} />
{/* All other routes */}
</Route>
</Router>
)
}
import {
Router,
Route,
redirect,
RoutePreloadFuncArgs,
} from "@solidjs/router"

const checkAuth = query(async function (
args: RoutePreloadFuncArgs,
auth: boolean
) {
"use server"

// COULD ALSO GET USER STATUS HERE

if (auth && args.location.pathname === "/auth") {
throw redirect("/")
}

if (!auth && args.location.pathname !== "/auth") {
throw redirect("/auth")
}
},
"checkAuth")

const App: Component = () => {
return (
<Router>
<Route
preload={args => createAsync(() => checkAuth(args, true))}
>
<Route path="/" component={Home} />
<Route path="/auth" component={Auth} />
{/* All other routes */}
</Route>
</Router>
)
}
--- Optional: Lazy Loading Components
// Same checkAuth function either with or without "query"

const App: Component = () => {
return (
...
<Route path="/" component={lazy(() => import('./Home'))} />
<Route path="/auth" component={lazy(() => import('./Auth'))} />
{/* All other routes */}
...
)
}
// Same checkAuth function either with or without "query"

const App: Component = () => {
return (
...
<Route path="/" component={lazy(() => import('./Home'))} />
<Route path="/auth" component={lazy(() => import('./Auth'))} />
{/* All other routes */}
...
)
}
Benefits if you choose to use lazy loading: - Reduces initial bundle size - Improves initial load time
5 Replies
ryansolid
ryansolid2mo ago
Yes this is the sort of thinking.. but the "true" probably needs to come from the server to verify. So my thinking is you should hoist the auth check to server middleware and then read from it inside your server functions that can then throw redirects or do the proper handling. That being said for something that isn't too granular I imagine the middleware itself could potentially handle the redirect before you are looking at specific endpoints. I believe the action methods should work even out in middleware.
ofj3kso
ofj3ksoOP2mo ago
Tks for the rapid response. I agree with what you said, but I'm also seeking a solution that works universally for both Solid and SolidStart users. Plus my assumption is that auth guards should prevent component mounting entirely rather than running as side effects after components have already begun rendering. (Unfortunatly all the one I've seen so far use this approach) I think the above code should just be able to handle the redirect logic, while the dev should handle the source of auth state. I can see people using something like Supabase client SDK (propably simpler apps) to handle auth state and other people fetching that info from the server (SolidStart). One of my first tests was indeed using a middleware, but reading the doc here https://docs.solidjs.com/solid-start/advanced/middleware#limitations it's specified that "Middleware does not run on every request, especially during client-side navigations. Relying on it for authorization would create a significant security vulnerability". Therefore I'm considering both the case the auth states comes directly form a server function or from a client lib that handles the auth
ryansolid
ryansolid2mo ago
The middleware does run on every request, just not every client navigation will make a request. That's odd wording. The thing is nothing in preload will stop the page from rendering. So you have to assume on client nav after the case if you go and ask the server if you are authenticated it is still going to start rendering the new page while waiting for that answer.. sure it isn't going to get far and the Suspense/Transitions will not actually show anything to the user, before your redirect takes over. The thing is if we ask first outside of the request it will waterfall.. so you want to ask the question inside each data (API endpoint essentially) if you can't answer the question from the client only. I always ask people what they expect to happen in these scenarios to see how it fits. Because nothing here changes the physics, just where it happens. Like in one world maybe during SSR you setup some sort of access token in middleware and you pass it into a Context Provider (in a createResource/createAsync that will serialize) at the top of your page and then the client and server both have access to it. But tokens do expire so I imagine there is more logic there. Of course then you need to send the token to server to validate on API calls.. this is a classic client centric model approach. That way it can answer the question synchronously.
ofj3kso
ofj3ksoOP2mo ago
Thanks for clarifying the middleware behavior - the documentation could definitely use an update I see what you mean about preload not truly preventing component mounting. Essentially, it's a classic OAuth-style approach with JWT, incorporating logic that listens for changes. This reminds me of Firebase Auth, which seems to use a fast memory storage for tokens. This allows you to verify a token on the server by checking it against the stored memory and to create a WebSocket on the client for updates on signup, logout, and token refresh. Anyway I don't want to bother you further, I really appreciate your time and insights. Thank you for the conversation.
ryansolid
ryansolid2mo ago
Yeah I mean that is one approach.. I'm interested in what others do.. because checking against server everytime not inside things you were already fetching would cause a waterfall which isn't great (unless you were in constant contact which isn't always an option on serverless).

Did you find this page helpful?