Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attack vector arising from naive developer use of the +layout.server.js tree #6315

Open
cdcarson opened this issue Aug 26, 2022 · 78 comments
Open
Labels
feature / enhancement New feature or request
Milestone

Comments

@cdcarson
Copy link
Contributor

Describe the bug

tldr;

  • The existence of the +layout.server.js tree will lead folks to put logic there rather than in +page.server.js.
  • Because SvelteKit decides on the client what to fetch from the tree, there are edge cases where important (e.g., authorization) logic in +layout.server.js is skipped.
  • This may seem a contrived edge case, but it is an attack vector that needs to be addressed through documentation.

Consider the following case. (Repo link below)

src
└── routes
    └── launch-codes
        ├── +layout.server.js
        ├── +page.server.js
        └── +page.svelte

Let's say I naively (but I think quite naturally) do the following things:

  • I place authentication/authorization logic in the +layout.server.js file. Perhaps I plan to elaborate this section of the site with launch-codes/[id].
  • Meanwhile, I use +page.server.js to fetch a paged list of codes from the database.

Essentially I have separated the concerns of authorization and data collection.

Now this is totally secure as long as my user remains signed in. But let's say the following happens:

  • The user goes to /launch-codes. This first page view runs the authorization logic in +layout.server.ts.
  • They are signed out, say on another tab, or because the cookie expires, or just by deleting the cookie in the console.
  • In the original tab, now signed out, they go to /launch-codes?page=2. The authorization logic in +layout.server.ts is skipped, and the signed out user is shown the launch codes.

I don't think there's a way to reliably fix this in the framework. The client side router decides what to fetch, and that AFAICT will always be manipulable. The only way to "fix" it is by documentation.

(Or we could get rid of the layout data tree notion entirely. Not a bad option, IMO, but a separate losing argument.)

I know all this may seem contrived, but:

  • It's not that far-fetched for a developer to assume that separating authorization concerns is partly what +layout.server.js is for.
  • It's not that far-fetched to assume that some apps may require quick cookie expiration. Think your bank.
  • It exists. It's an attack vector.

Reproduction

Reproduction here, as minimal as I could make it: https://github.com/cdcarson/yellow-submarine.git

git clone https://github.com/cdcarson/yellow-submarine.git
cd yellow-submarine
npm i
npm run dev
  • Go to http://127.0.0.1:5173/launch-codes
  • Assuming you are not signed in, you'll be redirected to a sign in page, and redirected back after clicking the button
  • Navigate through the paginated list
  • In the tab's console, delete the signedIn cookie. (I.e. the cookie has expired or you've been signed out on another tab.)
  • Without refreshing the page, click to another page in the list. You will be shown that page, rather than being redirected to /sign-in

I made a separate route with a "non-naive" implementation, getting rid of layout.server.js and doing the authorization in +page.server.ts.

  • Go to http://127.0.0.1:5173/launch-codes-fixed
  • Replicate the steps above. This time, after deleting the cookie, subsequent navigations through the list at /launch-codes-fixed will be redirected to /sign-in

Logs

No response

System Info

System:
    OS: macOS 12.4
    CPU: (8) arm64 Apple M1
    Memory: 70.06 MB / 8.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.13.2 - ~/.nvm/versions/node/v16.13.2/bin/node
    npm: 8.1.2 - ~/.nvm/versions/node/v16.13.2/bin/npm
  Browsers:
    Chrome: 104.0.5112.101
    Safari: 15.5
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.66 
    @sveltejs/kit: next => 1.0.0-next.442 
    svelte: ^3.44.0 => 3.49.0 
    vite: ^3.0.4 => 3.0.9

Severity

serious, but I can work around it

Additional Information

No response

@CaptainCodeman
Copy link
Contributor

For security you need to check auth on every request. Every route can be fetched directly, without using the app UI at all, and each fetch is independent of anything that happened previously (although they can "collaborate" with the new await parent()

@Rich-Harris
Copy link
Member

It sounds like what you want is a way for a load function to say 'always run me, even if you think you don't need to'. Straw man:

import { gateAuthenticated } from '$lib/auth';
import type { LayoutServerLoad } from '../../../.svelte-kit/types/src/routes/launch-codes/$types';
import type { LaunchCodeLayoutData } from './shared';

export const load: LayoutServerLoad = (event): LaunchCodeLayoutData => {
+	event.alwaysRun();
	const signedIn = gateAuthenticated(event);
	return { signedIn };
};

@cdcarson
Copy link
Contributor Author

@Rich-Harris My gut is that this is a "more documentation" issue. event.alwaysRun() could just as well be omitted as await parent() (or however it is now recommended to ensure the entire tree is invalidated.) Granted, to footgun the omission is in one +layout.server.js rather than in each leaf. Might make sense.

@cdcarson
Copy link
Contributor Author

Ack, I see I wrote this:

Granted, to footgun the omission is in one +layout.server.js rather than in each leaf. Might make sense.

Ungarbled: For the vector to be in play, the developer would only have to forget to do event.alwaysRun() in the one +layout.server.js where the logic exists, rather than forgetting to repeat the logic in any one of many +page.server.jss.

So yes, I'd be on board with event.alwaysRun() if it's guaranteed that the client router couldn't override it.

The footgun would still exist, but event.alwaysRun() affords the opportunity to explain how to avoid it in a concrete. memorable way.

@TranscendentalRide
Copy link

Why not +auth.server.js since authentication is among the basics of apps.

@cdcarson
Copy link
Contributor Author

cdcarson commented Sep 1, 2022

@TranscendentalRide Adding an +auth.server.js filename would light a dumpster fire 🔥. (See the discussions in the repo.) But I see where you're going. Right now the +layout.server load tree is all about caching, a fundamentally different concern from auth. So, maybe something like this in +layout.server.js...

// SvelteKit type...
export type LayoutServerGuard = (event: RequestEvent) => MaybePromise<void>;


// src/routes/kittens/[kittenId]/dashboard/+layout.server.ts...

// as now, refetched only when invalidated
export const load: LayoutServerLoad = async (event): Promise<KittenDashboardData> => {
   const { kittenId } = event.params
   const kitten = await db.findUnique({where: {id: kittenId}});
   return { kitten };
}

// called for every navigation to page in `/kittens/[id]/dashboard/*`
export const guard: LayoutServerGuard  = async (event): Promise<void> => {
  // authorize the current user in relation to the kitten dashboard
  // could be just reading a cookie in many cases -- i.e. quick
  // if the user isn't authorized, throw error or redirect, otherwise void
}

...where guard, if it exists, would (1) always be called and (2) always beawaited before load (if load is called.) There's also a case where one would want guard without load. Note that LayoutServerGuard deliberately doesn't return anything, maintaining its independence & simplicity.

But maybe that's gilding the lily. I'd be happy with alwaysRun if that's simpler to implement.

@TranscendentalRide
Copy link

@cdcarson Maybe... but that shouldn't matter

I believe the declarative benefits of this "plus paradigm", ie +function.location.js affords us not to need to dig into the code to see how it renders. Back pedaling on it give more power to the idea that this is the wrong direction.

@AdrianGonz97
Copy link
Member

Today I learned about an interesting quirk of SvelteKit's invalidation that I think we can use to our advantage for this exact scenario. We can force the load function of +layout.server.ts to rerun every time we navigate to and between the child routes of a layout group.

This issue was referenced in this video demonstrating the current problem of protecting routes by way of only checking the auth in +layout.server.ts files, without awaiting the parent on each route.

Check out this repo that I forked from @huntabyte to see it in action. To accomplish this, I'm adding url.href here as an almost equivalent event.alwaysRun() function that Rich presented, but not quite.

Adding a url.href anywhere in the load function works because url.href has now become a dependency of said load function. If any dependency of the load function changes, the load function will rerun, as specified here.

By adding a url.href in the layout load function, we no longer have to add an await parent() in each child +page.server.ts load function.

Obviously, this isn't an ideal solution by any means, but it's an interesting one that I stumbled upon.

@ivanhofer
Copy link
Contributor

Adding my two cents here:

Handling authorization is currently easy to mess up.

  1. The main issue was already described above:
    If you don't explicitly run all checks on each request, someone could access data he isn't allowed to see.
    Unexperienced users probably won't notice the issue. They will think if they protect a node inside this tree structure, each leave will also be protected.
    Having to manually add await parent() is too easy to forget. Even if you are paying attention, you will probably mess it up at some point in the future and you won't spot the issue until it's to late.

  2. Another smaller issue is: because all load functions per default run in parallel, data could get fetched from the DB before a parent load function detects that the user is not authorized. Not a big issue since the data will not get passed to the user, but also not ideal, since it could make an expensive call to the DB.

  3. And yet another issue I can see is that regular endpoints must also be protected individually. Which is fine, but I guess this is not clear to all developers.

You will end up with a lot of duplicated code because you need to check authorization in each exported function of every +*.server.js file. And also easy to mess up when you alter the check somewhere, but forget to change it everywhere else.

There were already described a few solutions in the comments above:

  • event.alwaysRun(): I don't think this is a good solution as it only solves issue 1
  • export const guard inside +*.server.js files: would solve issue 1 and 2
  • +auth.server.js: would solve all 3 issues

In my opinion adding +auth.server.js would be a great addition to SvelteKit

  • Before a load or an endpoint function get's called, all +auth.server.js files get checked. If one of those checks throw an error object, nothing gets executed and the error Response get's returned.
  • Inside the auth file, someone could implement advanced auth checks, handling POST and DELETE independently. As those files probably tend to be just a few lines short, it would be really easy to understand. In contrast to a load function where you need to find out where auth code stops and where the actual data fetching happens.
  • Having auth defined as a file inside a directory makes it obvious that the route is protected somehow.
  • I also really like that the order matches the flow in which those files get called:
     /route
     	/+auth.server.js
      	/+layout.server.js
      	/+page.server.js
     	/+page.svelte
    auth happens before layout which happens before page and at the end the .svelte file gets rendered.

I think this would fit really well into the current concepts of SvelteKit, would make thinks more obvious and easier to maintain.

Would a +auth.js (without server) also make sense? For my use-cases probably not, but maybe someone could need it.

And If someone does not like the additional file, he does not have to use it and can implement auth like we currently have to do.

@cdcarson
Copy link
Contributor Author

cdcarson commented Dec 23, 2022

@ivanhofer Question. Let's say I have a whole bunch of routes for kittens/[id]/dashboard, all of which share the same authorization logic...

.
└── routes
    └── kittens
        └── [id]
            └── dashboard
                ├── +auth.server.ts                
                ├── +layout.server.ts
                ├── +page.server.ts
                ├── revenue
                │   └── +page.server.ts
                └── settings
                    └── +page.server.ts

Would whatever is exported from routes/kittens/[id]/dashboard/+auth.server.ts run before all the descendant loads? For example, would it run before the load in routes/kittens/[id]/dashboard/revenue/+page.server.ts? Or is is just run before the layout and page loads in its own directory?

@CaptainCodeman
Copy link
Contributor

CaptainCodeman commented Dec 23, 2022

As a fundamental rule, authorization has to happen on each request to the server before data is loaded and returned (and making the load incorporate the user identity or role is significantly better than "did they reach this load function somehow").

If you don't do it in the page load, and don't make that load conditional on the layout load by awaiting it, then all you have is a veneer of security ... as long as people navigate your app correctly. There's nothing stopping anyone requesting the __data.json files directly without even going through your app UI.

I like the concept of the +auth file that applies to everything below it, but IMO there's no reason to make it specific to auth - what it effectively becomes is middleware that can be defined to run at any point in the routing tree and might often be used for auth, but could be useful for other things. So why not something like the current hooks, which is running at the root?

@ivanhofer
Copy link
Contributor

Would whatever is exported from routes/kittens/[id]/dashboard/+auth.server.ts run before all the descendant loads? For example, would it run before the load in routes/kittens/[id]/dashboard/revenue/+page.server.ts? Or is is just run before the layout and page loads in its own directory?

Yes, or else you would not really gain any benefits except from splitting auth into a separate file.
So the order would be:

  1. run all +auth.server.js
  2. run all +layout.server.js
  3. run all +page.server.js
  4. render the page tree (.svelte files)

For simplicity reasons I have omitted the non-server files.

So basically everything stays the same, just the auth concept get's executed first. It is basically the same as +layout.server.js with the small addition that the full tree get's executed each time a request gets made.

As I'm writing this I realize that I haven't thought of the distinction between layout and page for the auth perspective.

  • Is it enough to let the user decide inside that file via if-else if he wants to run the check just for this page or the full tree?
  • Maybe the +auth.server.js file could export a export const layout = () => ... and a export const page = () => ... function?
  • Do we need +layout.auth.server.js and +page.auth.server.js? please not 😅

I like the concept of the +auth file that applies to everything below it, but IMO there's no reason to make it specific to auth - what it effectively becomes is middleware that can be defined to run at any point in the routing tree and might often be used for auth, but could be useful for other things. So why not something like the current hooks, which is running at the root?

So you are basically talking about #6731?

Maybe the existing +*.server.js files could optionally expose some functions

  • doBefore: runs before all load and endpoint functions
  • doAfter: runs after all load and endpoint functions
  • other ?

I'm currently not aware of any usecase where something would need to run in a similar way for the full tree as authorization requires it. If it just involves a single page, you can simply call a function at the top of the load function. Authorization is a big concern that probably most bigger applications will need. So having a distinct auth feature makes sense in my opinion.

@ivanhofer
Copy link
Contributor

I'm currently not aware of any usecase where something would need to run in a similar way for the full tree as authorization requires it

Found another use-case where something needs to run for all path segments: page metadata or more precisely breadcrumb navigation. But in this case, it just needs to run a single time and not for every request.

For such a feature you probably would need to get an object with metadata e.g. title and href for each path segment and combine them into an array with the correct order at the end of a request. This information can be passed as a data prop to the layout. Currently this also requires you to use await parent() for each path segment or else you would not be able to combine the information.

It is a bit painful to implement this currently, but I'm not sure if such a feature should be offered by SvelteKit directly.
On the other hand an easier solution to the authorization problem should definitely be offered by SvelteKit.

@cdcarson
Copy link
Contributor Author

cdcarson commented Dec 26, 2022

I'm of two minds about this.

If SvelteKit keeps the notion of layout.server.ts it should definitely provide a way to guard routes hierarchically (i.e. what @ivanhofer is talking about, where a guard defined in a layout node runs before all the child loads. Importantly, the guards should operate separately from the load functions.

But after messing around with layout.server.ts, I've abandoned it (at least for now) except at the root level, to read an auth cookie and fetch profile data. For my project caching other data on the user's browser doesn't make much sense. If Bob and Alice share a set of data, they should see the same thing, fetched fresh from the server. So for me at least the original concern of this issue -- i.e. assuming that auth logic in a layout load would run on each applicable request -- has gone away. So, maybe get rid of +layout.server.ts? Just a thought.

DoodlesEpic added a commit to DoodlesEpic/next-auth that referenced this issue Dec 29, 2022
This authorization section was added to make sure a few caveats with
SvelteKit were well documented to anyone using the library.

The problem is documented here: sveltejs/kit#6315

Essentially, propagation of data between leafs is not guaranteed when
using the +layout.server.ts file as its load function is not guaranteed
to rerun every page change. The current approach to solve this is to do
authorization in each +page.server.ts file and additionally make sure to
grab the session data by awaiting the parent instead of directly
accessing the $page store, to make sure the information there is
current.
@Smirow
Copy link

Smirow commented Dec 30, 2022

Interesting debate. I'll join @cdcarson case (the first at least 😅), it's likely more of a documentation/best practice thing.
+layout.server.ts is still very useful for it's main purpose: load data at a layout level, which can be used by +layout.svelte or any of its childs.
And responding to @ivanhofer, I consider that being able to load data while waiting for user authorization is quite a nice feature to have.
An opt-in method, like export const guard or a specific file will be a nice addition.

ThangHuuVu added a commit to nextauthjs/next-auth that referenced this issue Jan 4, 2023
* chore(docs): Session management sample for Svelte

Added a code sample for managing the session through the $page store.
The sample demonstrates how to retrieve the session data in the root
+page.server.ts file and make it globally accessible through the $page
store, simplifying state management in the application. The previous
examples already used the data available in this store but did not show
how to set it.

* docs: Add authorization section to SvelteKit docs

This authorization section was added to make sure a few caveats with
SvelteKit were well documented to anyone using the library.

The problem is documented here: sveltejs/kit#6315

Essentially, propagation of data between leafs is not guaranteed when
using the +layout.server.ts file as its load function is not guaranteed
to rerun every page change. The current approach to solve this is to do
authorization in each +page.server.ts file and additionally make sure to
grab the session data by awaiting the parent instead of directly
accessing the $page store, to make sure the information there is
current.

* docs: Fix small typesafety mistake in SvelteKit

PageLoad type should actually be PageServerLoad. Not setting this does
not actually generate any problems other than TypeScript complaining
that this type is not actually exported.

* docs: Add handle hook authorization management

Another way to handle authorization is through a path-based method. This
added part of the documentation uses the handle hook to protect certain
routes based on their path. The previous method which is per-component
is still present.

* docs: Simplify component approach for Svelte auth

Using event.locals.getSession() exposed by SvelteKitAuth instead of
relying in the root layout file making that available in the $page
store.

* docs: Complete SvelteKit authorization docs

Finalize the explanation for the URI-based approach and also clarify
interactions with the component-based approach.

* docs: Add formatting to vars in the SvelteKit docs

Format the variables like this: `var` so that it appears clearly as code
when reading the documentation.

Co-authored-by: Thang Vu <[email protected]>
@dslatkin
Copy link

dslatkin commented Jan 7, 2023

I like the concept of the +auth file that applies to everything below it, but IMO there's no reason to make it specific to auth - what it effectively becomes is middleware that can be defined to run at any point in the routing tree and might often be used for auth, but could be useful for other things. So why not something like the current hooks, which is running at the root?

Agreed! Naming is hard and calling it +auth.server.ts will inevitably pigeonhole people's thinking about how they might use the feature.

What about using the word hook? If going with a file, something like +hook.server.js, or if exporting from +layout.server.js maybe it's a function called hook.

Of course the word hook is already used in src/hooks.client.js and src/hooks.server.js, but I think keeping it singular hook instead of hooks would help with the distinction.

@coryvirok
Copy link
Contributor

coryvirok commented Jan 8, 2023

FWIW, I think the simplest way to describe the correct way of handling authentication is to say, all authentication logic needs to go into hooks.server.ts before the call to resolve(event). Full stop. Every other options runs into complications like parallel loading, easily forgettable guard checks, server vs client loading, etc.

The issue I've found with putting it into hooks.server.ts is that you essentially have to do route pattern matching to determine if you want to guard the route. Which is made slightly easier with route groups. And since layout groups can be nested (who knew?!) you can essentially just sprinkle in (protected) into your route tree to protect disparate hierarchies of routes

e.g.

  • src/routes/
    • (protected)
      • +page.svelte
    • login/
      • +page.svelte
export const handle = (async ({ event, resolve }) => {
    const user = await getUserFromCookieOrHeader(event)
    if (shouldProtectRoute(event.route.id) && !user) {
        throw error(401) // render the error.svelte page where you check for 401 and optionally render a login component
    }
    return resolve(event)
}) satisfies Handle

function shouldProtectRoue(routeId: string) {
    return routeId.startsWith('/(protected)/')
}

It's not pretty but seems to work.

Looking into the Next.js Auth framework for Kit it appears this is what they do. https://github.com/nextauthjs/next-auth/blob/main/packages/frameworks-sveltekit/src/lib/index.ts#L143

In fact, the comment linked to above describes the issue this thread is addressing pretty succinctly.

@coryvirok
Copy link
Contributor

Noodling on the above for a second... You could also appropriate layout groups for role based access control. This opens up some interesting possibilities like nested ACL checks. E.g. "/" is public, "/dashboard" requires a user, "/dashboard/settings" requires an admin role. etc.

Simple example:

  • src/routes
    • (app)
      • (requiresAdmin)
        • admin/
          • +page.svelte
      • (requiresUser)
        • dashboard/
          • +page.svelte
      • login/
        • +page.svelte
        • (requiresAdmin)
          • secret-admin-functionality/
            • +page.svelte
    • (marketing)
      • +page.svelte
export const handle = (async ({ event, resolve }) => {
    const user = await getUserFromCookieOrHeader(event)
    const role = user?.role

    // Look for the layout group "requiresUser" in the route tree and use that to determine if everything
    // in the tree hierarchy requires a user is present in the cookie/header
    if (event.route.id.includes('/(requiresUser)/') && !user) {
        throw error(401, 'requires authentication')
    }
    // Check for the "requiresAdmin" layout group and run the appropriate RBAC logic
    if (event.route.id.includes('/(requiresAdmin)/') && role !== 'ADMIN') {
        throw error(401, 'requires admin')
    }
    return resolve(event)
}) satisfies Handle

I'm not a fan of the directory/file-naming-as-an-api approach that Kit takes, but it does allow for some useful configurations.

@ivanhofer
Copy link
Contributor

@coryvirok I agree, in SvelteKits current state, auth handling is best handled in hooks.server.js, if you really want to make sure to not accidentially introduce security vulnerabilities.

For static routes and just a few roles this can be done in a managable amount of code like you showed above.
But things can get more complex really fast:

  • what if you want to respond with an error page that has the same layout as the group the path is in?
    e.g. /dashboard should show the usual frame around the content (error message) and /settings should show the settings-menu. With the plain hooks.server.js approach, you will see a plain error page, without the possibility for users to "recover from their mistake". A simple "go back" button isn't always the best option.
  • what if you want to show custom error messages depending on the route?
    e.g. (requiresUser) for an unathenticated on /settings should show a message that says "You need to login first", but for an admin it should show "you need to change your profile in Azure AD". If you have different outcomes for different roles an routes, you need to replicate parts of your folder structure in hooks.server.js. Rename a route, and forgetting to take a look at the auth logic and you will have an unprotected route.
  • what if your authentication logic depends on a dynamic slug?
    e.g. user A is owner of the project with id 1 and should be able to access /projects/1 but not projects/2 because it belongs to user B. Inside hooks.server.js you can't access the params in a typesafe way (issue when refactoring) and you would need to add code that is specific to that part of the application because you need to load the project from the db to see if it belongs to that user. Having some similar routes with all those checks and you probably will not ever touch the auth code again because it grows in complexity really fast and therefore is not easy to maintain.

In my opinion having a folder-based auth solution really benefits when handling with complex use-cases. Autorization is not just black or white and therefore needs strong support from the metaframework. It should not be left to user-land. At least not in it's current state where it is far too easy to mess up.

@coryvirok
Copy link
Contributor

Ya I'm with you on each point. I don't think the solution I added above is a good solution, just one that works with the framework as it is.

Regarding the error component Chrome, I opened a discussion about this yesterday when I realized errors in hooks.server.ts were rendered with the fallback error.html file - #8393

Regarding folder based auth strategy, I know @benmccann is/was interested in creating a way to configure routes that didn't rely on folder/filenames and could be more programmatic. That's the method that other frameworks use to enable complex auth guards like the ones you mention.

Eg. Routes.js would define the url path + params + param validation + before/after hooks, etc.

IMO Adding more and more logic into the folder names or file names diverges the framework further and further from convention that devs are used to. Which is why I'm not a big fan and I'd consider the solution I posted above to be a hack.

@coryvirok
Copy link
Contributor

As I mention here (#6315 (comment)) just do it all in hooks.server.js. It's not pretty, but it works to secure all server-side routes.

This is what Next Auth for SvelteKit does - https://github.com/nextauthjs/next-auth/blob/d094c6f4d9881af5b803fa707aadd8819e2427cd/packages/frameworks-sveltekit/src/lib/index.ts#L138

@dslatkin
Copy link

dslatkin commented Oct 22, 2023

As I mention here (#6315 (comment)) just do it all hooks.server.js.

The problem is not that there's is not a way to do route-level hooks, authentication, guards (whatever one wants to call them). There are ways to do it. The problem is that +layout.server.js load functions feel far too intuitive a place for newbies to put this kind of logic in and leads to far too dangerous an outcome by doing so.

Fixing the issue means fixing the footgun. This could mean making a new feature be the more obvious place to do it, or it could mean fixing the docs to explicitly provide advice on how to do it.

I will say that matching on something like (protected) in hooks.server.ts and combining that with type safety mentioned earlier feels like, to me, the best technique so far to guard a route if something explicit is mentioned in the docs.

@timothycohen
Copy link
Contributor

But "the right thing to do" is still open ended if you need to fetch some data that will be used for an authorization check and also as page data (or data used further down the route chain).

I'm not sure I understand. I would think that an app typically does not fetch data "for an authentication check", it instead fetches data "given the user's authentication". And that any data shown thereafter should be based on the data fetched already given by the user's authentication.

A more concrete example like how the launch codes were brought up earlier might make sense for me.

True, an app usually fetches data "given the user's authentication". For example /account/transactions. Route level middleware or docs leading to hooks.server.ts would be perfect for this.

But any time you have a reference to some shared asset, and that reference isn't stored in the user schema, the app will have to fetch data "for an authorization check". hooks.server.ts / route level middleware feel unintuitive for this.

Concrete example: the layout route is /transactions/[id] (/negotiation-history, /messages, /payments, /etc) and the id references some transaction between two users. The app has managers with permissions scoped to view transactions of the users they manage. On every nav, and before any of the child load functions run, the app should authorize the request for the transaction based on the current user's permissions. The app must therefore first get the user ids of the transaction participants, probably by getting the transaction from the db.

LayoutServerLoad feels more intuitive than Handle here because you're going to use the transaction data as props to the child pages. SvelteKit's current solution with LayoutServerLoad is multiple await parent()s, which are probably called in the child loads to use the layout data. But even if you're aware of the footgun, because the auth check and the layout cache opt-out happen in two different files, it's easy to forget to apply await parent() to child routes that don't need the parent data. The other current solution, hooks.server.ts, always protects you but requires some workaround for the data, either by type casting in event.locals with a hope it doesn't change underneath you, making a cacher, calling the db twice, adding a type definition to app.d.ts, etc. And again, because this is less intuitive, someone would only do this if they were aware of the hazards in the first place.

For the basic (and more critical) authentication case, adding a more intuitive place would remove the footgun, but as soon as someone wants to pass data around used by a permission check, they'll think, "Oh, I'll just do it in +layout.server.ts" and the footgun reemerges. A framework solution for that scenario could be something like an export const cache defaulting to true in +layout.server.ts. Something to put all the responsibility in a single line of the layout file instead of in every child. The risk is that people overuse that and cause unnecessary data waterfalls when hooks.server.ts would have sufficed.

Whether or not either scenario is addressed by the framework, the hazards of both would at least be mitigated by updating the docs and tutorial. Theres a PR open for the docs, but not the tutorial AFAIK.

@gipo355
Copy link

gipo355 commented Nov 11, 2023

I support the idea of non root +hooks.

The first thing I tried when moving on to sveltekit was creating a hook for a specific route and all sub routes as that felt like the most natural thing to do.

Was quickly forced on to docs and had to put in the root.

Some times I just want to create Middleware and separate it from the main hooks, isolating it only for selected endpoints.

It just feels the closest to every http framework out there to me.

Adding to this, as a raw idea, if hooks were scopable and made an utility prop like use(req,res,next) available, it could potentially unlock the whole express ecosystem and make writing sveltekit Middleware easy. (things like rate limiter, redis sessions, helmet etc)

@jose-manuel-silva
Copy link

jose-manuel-silva commented Dec 3, 2023

Given the upcoming changes with Svelte 5 and the potential for streamlining the code structure, I'd like to suggest a breaking change: removing +hooks.server.js and incorporating the handle function directly into +layout.server.js, and the handle function would run on every request made to the child pages of the +layout.server.js. This could offer a more straightforward approach while maintaining the integrity of the load function. What are your thoughts on this idea?

dummdidumm pushed a commit that referenced this issue Dec 12, 2023
@mostrecent
Copy link

FWIW, what I find also confusing:

A rerun of +layout.server.ts via await parent() on children server load functions does not update $page.data.propsFromLayoutServer on the client (assuming the props from layout.server.ts did change and are not overwritten by a children server load function). So you need to pass the ones where you expect change through the children again. But then I can just take the props from locals from hooks.server.ts in the first place (which is more secure too).

Is there a reason that a rerun of +layout.server.ts does not populate (update) $page?

@timothycohen
Copy link
Contributor

timothycohen commented Dec 20, 2023

That's got a separate issue at #9355. It's unintuitive (to me) that although the load function reruns and the fresh data is available to children via await parent(), it doesn't propagate to the page data. dummdidumm pointed out this will force it to behave the way you think it should:

export const load = async ({ url }) => {
	url.pathname;

(still need to await parent() to ensure it always reruns between child nav though!)

@saturnonearth
Copy link

Since SvelteKit works around folder based routing, what if hooks worked in the same way? That way, if you need auth middle ware code, just throw a "hooks" file at the root level of where you want anything below it protected.

hooks.server.ts <--- can still have root level hooks for everything else
(public)
     hooks.ts <--- Only want your analytics on content-focused part of your site? Add it here
     ...
(protected)
    hooks.server.ts <--- write your auth code here, or anything else you want
    +layout.svelte
    +page.svelte
    ...

@mostrecent
Copy link

mostrecent commented Dec 21, 2023

@timothycohen Thanks! Re the general discussion, I use +layout.server.ts to give updates about the current authorization status to the view BUT the actual authorization always happens in hooks.server.ts and +page.server.ts files (detailed in #9355 (comment)).

And why need both hooks.server.ts and +layout.server.ts: Former always runs sequentially and before the rest, latter can do both, running sequentially or simultaneously which is good for latency. so they have different use cases.

@mostrecent
Copy link

mostrecent commented Dec 22, 2023

Follow-up: This is more tricky than I thought. Using the url.pathname hack to let +layout.server.ts populate $page on every request led to a server request on every page navigation even if the page was fully preloaded. This isn't perfect either.

So, how can we let +layout.server.ts populate $page every time a children endpoint/+page.server.ts is requested, not more not less? And without passing the required data through every +page.server.ts ' response?

@WhyAsh5114
Copy link

WhyAsh5114 commented Dec 30, 2023

So, how can we let +layout.server.ts populate $page every time a children endpoint/+page.server.ts is requested, not more not less? And without passing the required data through every +page.server.ts ' response?

@mostrecent I'm using an array of unprotected routes (or prerendered routes), in my project.
You can create such an array and check if the url.pathname is within it, if so simply return without rerunning the other logic which might be needed for non-prerendered page, which will only be run when url.pathname does not belong to the unprotectedRoutes array.

import { redirect } from "@sveltejs/kit";
const unprotectedRoutes = ["/", "/login", "/offline"];

export const load = async ({ locals, url }) => {
  /*
     if block will only run when page is a protected route
     to run only when non-prerendered, modify array according to your project structure
  */
  if (!unprotectedRoutes.includes(url.pathname)) {
    const session = await locals.getSession();
    if (!session) {
      throw redirect(303, `/login?callbackURL=${url.pathname}`);    // or just return {}
    }
  }
};

@GimpMaster
Copy link

I know I’m late to this conversation but I’ve been spending the last few days trying to figure out best approaches for protecting pages from being rendered whether doing it via SSR or CSR.

I get that hooks.server.ts seems to be the method for SSR redirects but there doesn’t seem to be a great way for hooking into client side navigation. In my react apps I write my own router that had an onBeforeNavigation callback that would return a promise Boolean that I could stop navigating happening or return a URL that would redirect.

I think if sveltekit would update its hooks.client.ts with something to this effect it would solve client side navigation blocking in one location instead of the suggested approach of put an empty page.server.ts load function in every single page which seems prone to user error. Also I’m pretty sure that would mean we are doing a server request before navigating.

@Leftium
Copy link

Leftium commented Jan 8, 2024

I get that hooks.server.ts seems to be the method for SSR redirects but there doesn’t seem to be a great way for hooking into client side navigation. In my react apps I write my own router that had an onBeforeNavigation callback that would return a promise Boolean that I could stop navigating happening or return a URL that would redirect.

SvelteKit has beforeNavigate and onNavigate. These can work if put in the +page.layout.svelte. However:

  • Any error (in these callbacks) prevents the client-side auth redirect from working: the user just gets access to the route, even if they don't have permissions.
  • Client-side code is easier to attack (to skip auth redirect).

It's simpler and safer to just use an empty +page.server.ts. Yes, it requires a server request before navigating, but I think that is safer and more secure.

Ideally, SvelteKit would provide some mechanism so all routes/sub-routes are protected unless there is an explicit opt-out.

@Blackwidow-sudo
Copy link

Blackwidow-sudo commented Jan 18, 2024

I get that hooks.server.ts seems to be the method for SSR redirects but there doesn’t seem to be a great way for hooking into client side navigation. In my react apps I write my own router that had an onBeforeNavigation callback that would return a promise Boolean that I could stop navigating happening or return a URL that would redirect.

That's exactly what i have thought so many times:
Why is there no handle for hooks.client.js that runs before each client side navigation? It would make it much easier to implement route guards on the client side. But yeah, im aware that this would be no good solution.

@coryvirok
Copy link
Contributor

I get that hooks.server.ts seems to be the method for SSR redirects but there doesn’t seem to be a great way for hooking into client side navigation. In my react apps I write my own router that had an onBeforeNavigation callback that would return a promise Boolean that I could stop navigating happening or return a URL that would redirect.

That's exactly what i have thought so many times: Why is there no handle for hooks.client.js that runs before each client side navigation? It would make it much easier to implement route guards on the client side. But yeah, im aware that this would be no good solution.

Isn't this doable via onNavigate() in layout.svelte?

https://kit.svelte.dev/docs/types#public-types-onnavigate

Also, the new reroute functionality might allow you to do this. But I haven't tested this use case for it.

@GStudiosX2
Copy link

Really hope something ends up happening I don't want to have +page.server.ts for all my protected routes

@emmbm
Copy link

emmbm commented May 3, 2024

Really hope something ends up happening I don't want to have +page.server.ts for all my protected routes

If your routes are protected, I presume it's because they access sensible data. In that case, do you not already have a +page.server.ts to query said data where relevant? If you use client-side fetching (e.g. endpoints or some client-side library like supabase-js) then your protection should already be managed at the service's level.

If you protect routes only to keep some parts of your UI private without them containing specially sensible data, then using layout-based protection is completely fine.

@aakash14goplani
Copy link

This is how I am checking session state in the application

Server Side

hooks.server.ts

const userSessionInterceptor = (async ({ event, resolve }) => {
  const sessionId = event.cookies.get(lucia.sessionCookieName);
  if (!sessionId) {
    event.locals.user = null;
    event.locals.session = null;
    redirect(302, `${LOGOUT_PAGE}`);
  } else {
    // continue with the session
  }
}) satisfies Handle;

Client Side

src/routes/+layout.svelte

const checkUserSessionValidity = debounce(async () => {
  const request = await fetch(
    `${window.location.origin}${BASE_PATH}/api/get-session-status`
  );
  const response = await request.json();
  const isSessionValid = !!response?.status;
  if (!isSessionValid) {
    goto(LOGOUT_PAGE);
  }
}, 1 * 1000);

beforeNavigate(() => {
  checkUserSessionValidity();
  return true;
});

src/routes/api/get-session-data/+server.ts

export const GET = (async (event) => {
  const sessionId = event.cookies.get(lucia.sessionCookieName);
  return new Response(JSON.stringify({ status: sessionId }), { status: 200 });
}) satisfies RequestHandler;

So far things are working great!

@GStudiosX2
Copy link

GStudiosX2 commented May 4, 2024

Really hope something ends up happening I don't want to have +page.server.ts for all my protected routes

If your routes are protected, I presume it's because they access sensible data. In that case, do you not already have a +page.server.ts to query said data where relevant? If you use client-side fetching (e.g. endpoints or some client-side library like supabase-js) then your protection should already be managed at the service's level.

If you protect routes only to keep some parts of your UI private without them containing specially sensible data, then using layout-based protection is completely fine.

Now that I think about it though I may not need to protect the routes really because the data on them is gonna be blocked unless you have the permission to view it/modify it but it still would be nice for a clean way to just block pages under a route

I don't really store anything sensitive anyway because I have oauth login and I just have basically username and discord user id and then some things that are public anyway

@coryvirok
Copy link
Contributor

Really hope something ends up happening I don't want to have +page.server.ts for all my protected routes

If your routes are protected, I presume it's because they access sensible data. In that case, do you not already have a +page.server.ts to query said data where relevant? If you use client-side fetching (e.g. endpoints or some client-side library like supabase-js) then your protection should already be managed at the service's level.
If you protect routes only to keep some parts of your UI private without them containing specially sensible data, then using layout-based protection is completely fine.

Now that I think about it though I may not need to protect the routes really because the data on them is gonna be blocked unless you have the permission to view it/modify it but it still would be nice for a clean way to just block pages under a route

I don't really store anything sensitive anyway because I have oauth login and I just have basically username and discord user id and then some things that are public anyway

#6315 (comment)

I've been doing it this way for over a year now. See the comment for pros and cons.

@harunzafer
Copy link

AdrianGonz97

I've created a new sveltekit-2 project and moved the files over from your repo. Adding url.href doesn't have any effect. Trying the scenario from Huntabyte's video, I can still see Fetched customers from database in the logs.

@CaptainCodeman
Copy link
Contributor

I wrote a blog article to try and describe this issue and suggest an approach that I've found useful in my own apps:

https://captaincodeman.com/securing-your-sveltekit-app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature / enhancement New feature or request
Projects
None yet
Development

No branches or pull requests