next-auth icon indicating copy to clipboard operation
next-auth copied to clipboard

SvelteKitAuth: How to update session from client to server and vice versa

Open aakash14goplani opened this issue 1 year ago • 16 comments

What is the improvement or update you wish to see?

Scenario 1: Client to Server

In SvelteKitAuth, how to communicate session updates from client to server?

In NextAuth, we do something like:

const { data: session, status, update } = useSession()
<button onClick={() => update({ name: "John Doe" })}>Edit name</button>

Scenario 2: Server to Client

In SvelteKitAuth, we manage session within jwt({ ... }) and session({ ... }) callbacks. Let's consider a scenario in which user object was mutated by some event (like response from API call), how to update global session object in that case?

Is there any context that might help us understand?

The NextAuth has many helper methods exposed for both Client and Server API that makes it easy t implement above mentioned scenarios. How do we implement those in SvelteKitAuth?

Does the docs page already exist? Please link to it.

https://next-auth.js.org/getting-started/client#updating-the-session

aakash14goplani avatar Nov 15 '23 14:11 aakash14goplani

@balazsorban44 @ThangHuuVu @ndom91

aakash14goplani avatar Dec 18 '23 15:12 aakash14goplani

Seems like it's not implemented.

stalkerg avatar Jan 15 '24 06:01 stalkerg

Okey, it's actually supported, you can use next function:

import { base } from '$app/paths';
import type { Session } from '@auth/core/types';

export async function updateSession(data: Session): Promise<Session | null> {
  const sessionUrl = `${base}/auth/session`;

  const csrfTokenResponse = await fetch(`${base}/auth/csrf`);
  const { csrfToken } = await csrfTokenResponse.json();

  const res = await fetch(sessionUrl, {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      data,
      csrfToken,
    }),
  });

  const json = await res.json();
  return json;
}

also you should extend JWT callback in SvelteKitAuth options:

     async jwt({
        token,
        user,
        session,
        trigger,
      }: {
        token: JWT,
        user: SessionUser,
        session?: Session,
        trigger?: string | undefined,
      }) {
        if (trigger === 'signIn' && user) {
          Object.assign(token, { user });
        } else if (trigger === 'update') {
          Object.assign(token, { user: session?.user });
        }
        return token;
      },

it's should cover update trigger.

stalkerg avatar Jan 15 '24 10:01 stalkerg

Hello @stalkerg - thanks for your efforts. Updating session from client to server is still broken in SvleteKitAuth. The code you have provided won't work as the session-token cookie does not gets updated. You can refer this PR (and associated issues) for more detail understanding. Thanks.

aakash14goplani avatar Jan 15 '24 10:01 aakash14goplani

@aakash14goplani it's working fine for me, I can update session user and it's keeps during browser restart. cookie authjs.session-token also updated correctly.

stalkerg avatar Jan 15 '24 10:01 stalkerg

The PR #9497 is more about run getSession or set session from the SSR, if you will run my function from the client it will not a case. Also, with a small change it will work even on SSR.

stalkerg avatar Jan 15 '24 10:01 stalkerg

The PR #9497 is more about run getSession or set session from the SSR, if you will run my function from the client it will not a case. Also, with a small change it will work even on SSR.

Which small change Yury? Can you please let me know?

aakash14goplani avatar Jan 15 '24 13:01 aakash14goplani

@aakash14goplani this function can return also cookie and after that you can proxy a such cookie to the browser.

return { json, cookie: res.headers.getSetCookie() };

something like this, after in your SSR function you can just set a such cookies.

stalkerg avatar Jan 16 '24 01:01 stalkerg

After recent changes made in SvelteKitAuth v0.10 and v0.11, I am using following approach for to-and-fro communication.

Client to Server

General Idea: Update data by making API call

  • Client initiates an API request and passes a query param (from ".svelte" files)
    async function updateUserData() {
      await fetch(base + '/api/update-user-data?query=update-user-data', {
        method: 'POST'
      });
    }
    
  • We can create a dummy API route to handle this request. (routes/api/update-user-data)
    export const POST = (() => {
      try {
        return new Response('User data updated', { status: 200 });
      } catch (error: any) {
        return new Response('Error while updating user data: ' + error?.message, { status: 200 });
      }
    }) satisfies RequestHandler;
    
  • Since SvelteKitAuth is configured via hooks, it can intercept all the incoming network request. We can filter the one with query params within the "jwt" callback. Why "jwt" callback - because these callbacks are triggered multiple times and on every network requests.
    async jwt({ token, account, profile }) {
      ...
      const shouldUpdateToken = args.event.url.searchParams.get('query') === 'update-user-data';
      if (shouldUpdateToken) {
        console.log('token before update [library]: ', token?.library);
        token = {
          ...token,
          library: 'SvelteKitAuth: v0.11.1' // <- add items here, hardcode values or extract out of request payload
        };
        console.log('token after update [library]: ', token?.library);
      }
      return token;
    }
    
  • With the introduction of auth(), the newly added values will persist in cookies and hence server will be updated with latest data without user having to re-login (WHICH WAS NOT POSSIBLE for versions <v0.10)

Server to Client

General Idea: Update data using hydration

  • In the root "layout.ts" file, we can return session object to client. Please note - I said "layout.ts" and not "layout.server.ts"
  • The "layout.server.ts" will hydrated only once during initial page load and if user updates data mid-way, they would have to refresh the page or re-login to see updated data on client side. Hence idea is to stick with "layout.ts" file.
  • In "layout.ts", there is a special hack that must be implemented (you can do this in layout.server.ts but I prefer layout.ts). The hack is to include "url" parameter and do some operation using url parameter. This is important as "url" parameter will force this load function to execute on every navigation change and thus we can return latest session information back to client!
  • We can create a dummy end point that returns the session data (routes/api/get-user-data)
    export const GET = (async ({ locals }) => {
      const session = await locals.auth();
      return new Response(JSON.stringify(session), { status: 200 });
    }) satisfies RequestHandler;
    
  • Here is the complete code
    export const load = (async ({ url, fetch, data }) => {
      let session;
      try {
        if (browser) {
          if (url.href.includes('xxx')) console.log(''); // <- very important
          const sessionObject = await fetch(url.origin + base + '/api/get-user-data');
          session = await sessionObject.json();
        } else {
          session = data.session;
        }
      } catch (ex: any) {
        console.log('Error fetching session data, defaulting to parent data');
        session = data.session;
      }
      return { session };
    }) satisfies LayoutLoad;
    
  • Client can get latest data using $page.data.session

This is the long route that I have to implement in SvelteKit as there are no helper methods exposed by SvelteKitAuth (when compared with NextAuth). So @balazsorban44 @ThangHuuVu and @ndom91 can you please go through this approach and let me know if this is correct way (for now) or something could be improved?

Example Repo: https://github.com/aakash14goplani/sveltekit-auth-session-token/tree/new-version-test

aakash14goplani avatar Feb 04 '24 14:02 aakash14goplani

Having similar issue.

My authjs.session-token cookie doesn't get updated while the session expire in DB gets updated when updateSession happens. I'm using Email provider with Next.js app.

So extending session in DB won't make session extended since the cookie's expiration is not updated.

benevbright avatar Mar 04 '24 11:03 benevbright

It's actually implemented here but for some reason my cookie doesn't get updated.

benevbright avatar Mar 04 '24 12:03 benevbright

Ok. I found what was my issue. This explained . I was calling auth on Next.js server component rendering and I expected it would update the cookie. But It doesn't as the comment explains. I should call the API separately from client component useEffect or whatever. Now cookie gets updated correctly.

benevbright avatar Mar 04 '24 12:03 benevbright

Ok. I found what was my issue. This explained . I was calling auth on Next.js server component rendering and I expected it would update the cookie. But It doesn't as the comment explains. I should call the API separately from client component useEffect or whatever. Now cookie gets updated correctly.

@benevbright - Glad that you're able to find the solution but you posted in a wrong thread. This thread is meant for the "SvelteKit" framework and not "Next" framework!

aakash14goplani avatar Mar 04 '24 14:03 aakash14goplani

@aakash14goplani you're right. I hid my comments as off topic. 🙏

benevbright avatar Mar 04 '24 14:03 benevbright

comment by @stalkerg works for me. I am doing the complete registration when I need the user to fill the profile after registration.

here is my code.

export const actions: Actions = {
  /**
   *
   * @param {RequestEvent} event sveltekit request event
   * @returns
   */
  complete: async (event: RequestEvent) => {
    const form = await superValidate(event.request, zod(schema));
    const session = (await event.locals.auth()) as QuantmAdapterSession;

    if (!form.valid) {
      return fail(400, { form });
    }

    const name = form.data.name;

    const getopts = { method: 'GET' };
    const postopts = { method: 'POST' };
    const headers = { 'Content-Type': 'application/json' };

    const refresh = async (team: Team) =>
      event
        // get authjs crsf token
        .fetch(`${base}/auth/csrf`, { ...getopts })
        .then(response => response.json())
        // assign user to team
        .then(csrf => {
          // @ts-expect-error we know there will be user.
          session.user.team_id = team.id;
          return JSON.stringify({ data: session, ...csrf });
        })
        // update token with updated user
        .then(body => event.fetch(`${base}/auth/session`, { ...postopts, headers, body }))
        .then(response => response.json())
        .then(() => ({ form, team }));

    return api().auth.createTeam({ name }).then(refresh);
  },
};

debuggerpk avatar Mar 18 '24 05:03 debuggerpk

I haven't read through all of the posts in this thread, but there is an update / unstable_update method that's returned from NextAuth() in the Next.js side of things (live usage example here: https://next-auth-example.vercel.app/client-example).

So for implementing this in SvelteKit + Auth.js projects, maybe its helpful to look at that next-auth implementation. Yall can find it here: https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/lib/actions.ts#L110-L133

ndom91 avatar Apr 12 '24 12:04 ndom91

Found the solution (at least for now) https://blog.aakashgoplani.in/how-to-exchange-data-between-client-and-server-using-sveltekitauth

aakash14goplani avatar Jul 14 '24 10:07 aakash14goplani