aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.43k stars 2.13k forks source link

Migrating from v5 to v6 Auth, SSR, SvelteKit #12578

Open asmajlovicmars opened 11 months ago

asmajlovicmars commented 11 months ago

Before opening, please confirm:

JavaScript Framework

Web Components

Amplify APIs

Authentication, GraphQL API

Amplify Categories

auth

Environment information

``` # Put output below this line System: OS: macOS 13.6 CPU: (10) arm64 Apple M1 Pro Memory: 65.52 MB / 32.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.9.0 - ~/.volta/tools/image/node/20.9.0/bin/node Yarn: 1.22.19 - ~/.volta/bin/yarn npm: 10.1.0 - ~/.volta/tools/image/node/20.9.0/bin/npm pnpm: 8.10.3 - ~/.volta/bin/pnpm bun: 1.0.7 - /opt/homebrew/bin/bun Browsers: Brave Browser: 119.1.60.114 Chrome: 119.0.6045.159 Safari: 17.1 npmPackages: @playwright/test: ^1.39.0 => 1.39.0 @sveltejs/adapter-auto: ^2.1.1 => 2.1.1 @sveltejs/kit: ^1.27.6 => 1.27.6 aws-amplify: ^6.0.2 => 6.0.2 prettier: ^3.1.0 => 3.1.0 prettier-plugin-svelte: ^3.1.0 => 3.1.0 svelte: 5.0.0-next.4 => 5.0.0-next.4 svelte-check: ^3.6.0 => 3.6.0 tslib: ^2.6.2 => 2.6.2 typescript: ^5.2.2 => 5.2.2 vite: ^4.5.0 => 4.5.0 npmGlobalPackages: corepack: 0.20.0 npm: 10.1.0 ```

Describe the bug

I'm excited about the v6, and started testing a migration, but quickly got stuck on SSR side.

// v5 - server side - this used to work seamlessly

import { Amplify, withSSRContext } from 'aws-amplify';
import { awsmobile } from '$lib/utils/aws/private-envs';

export const handle = async ({ event, resolve }): Promise<Response> => {
  const { request } = event;

  const req = {
    ...request,
    headers: {
      ...request.headers,
      cookie: request.headers.get('cookie'),
    },
  };

  Amplify.configure({ ...awsmobile, ssr: true });
  const { Auth } = withSSRContext({ req });

  // ...
};

Now I'm trying to get it going with v6 on the server side, and not sure how to do it:

import type { PageServerLoad } from './$types';
import { Amplify } from 'aws-amplify';
import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth/server';
import awsconfig from '../../aws-exports';

export const load = (async ({ request, cookies }) => {
    const c = cookies.getAll();
    console.log(c);

    try {
        Amplify.configure(awsconfig, {
            ssr: true
        });

        const session = await fetchAuthSession({
            token: {
                value: Symbol('eyJraWQiOi...')
            }
        });

        const user = await getCurrentUser({
            token: {
                value: Symbol('eyJraWQiOiI...')
            }
        });

        // regardless if it's a valid idToken, accessToken, or refreshToken, the following error is thrown:
        // e: Error: Attempted to get the Amplify Server Context that may have been destroyed.

        console.log({ session, user });
    } catch (e) {
        console.log(e);
    }
    return {};
}) satisfies PageServerLoad;

However, on the V6 client side, everything works as expected:

<script lang="ts">
    import { onMount } from 'svelte';
    import type { PageData } from './$types';

    import { Amplify } from 'aws-amplify';
    import { getCurrentUser, signIn } from 'aws-amplify/auth';
    import awsconfig from '../../aws-exports';

    export let data: PageData;

    onMount(async () => {
        Amplify.configure(awsconfig);
        const user = await getCurrentUser();
        console.log({ user });

        if (!user) {
            const signedIn = await signIn({
                username: '...',
                password: '...'
            });
            console.log({ signedIn });
        }
    });
</script>

Expected behavior

Expected behaviour is that the server side authenticates, and provides a cognitoUser same as in V5.

Reproduction steps

  1. SvelteKit:
  2. npm create svelte@latest my-app
  3. cd my-app
  4. npm install
  5. npm run dev
  6. amplify init
  7. amplify import auth
  8. amplify push

Code Snippet

I'm excited about the v6, and started testing a migration, but quickly got stuck on SSR side.

// v5 - server side - this used to work seamlessly

import { Amplify, withSSRContext } from 'aws-amplify';
import { awsmobile } from '$lib/utils/aws/private-envs';

export const handle = async ({ event, resolve }): Promise<Response> => {
  const { request } = event;

  const req = {
    ...request,
    headers: {
      ...request.headers,
      cookie: request.headers.get('cookie'),
    },
  };

  Amplify.configure({ ...awsmobile, ssr: true });
  const { Auth } = withSSRContext({ req });

  // ...
};

Now I'm trying to get it going with v6 on the server side, and not sure how to do it:

import type { PageServerLoad } from './$types';
import { Amplify } from 'aws-amplify';
import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth/server';
import awsconfig from '../../aws-exports';

export const load = (async ({ request, cookies }) => {
    const c = cookies.getAll();
    console.log(c);

    try {
        Amplify.configure(awsconfig, {
            ssr: true
        });

        const session = await fetchAuthSession({
            token: {
                value: Symbol('eyJraWQiOi...idToken or accessToken from cognito cookies')
            }
        });

        const user = await getCurrentUser({
            token: {
                value: Symbol('eyJraWQiOiI...idToken or accessToken from cognito cookies')
            }
        });

        // regardless if it's a valid idToken, accessToken, or refreshToken, the following error is thrown:
        // e: Error: Attempted to get the Amplify Server Context that may have been destroyed.

        console.log({ session, user });
    } catch (e) {
        console.log(e);
    }
    return {};
}) satisfies PageServerLoad;

However, on the V6 client side, everything works as expected:

<script lang="ts">
    import { onMount } from 'svelte';
    import type { PageData } from './$types';

    import { Amplify } from 'aws-amplify';
    import { getCurrentUser, signIn } from 'aws-amplify/auth';
    import awsconfig from '../../aws-exports';

    export let data: PageData;

    onMount(async () => {
        Amplify.configure(awsconfig);
        const user = await getCurrentUser();
        console.log({ user });

        if (!user) {
            const signedIn = await signIn({
                username: '...',
                password: '...'
            });
            console.log({ signedIn });
        }
    });
</script>

Log output

``` // Put your logs below this line Error: Attempted to get the Amplify Server Context that may have been destroyed. at getAmplifyServerContext (file:///Users/asmajlovic/code/dexp-fe-v2/node_modules/.pnpm/@aws-amplify+core@6.0.2/node_modules/@aws-amplify/core/dist/esm/adapterCore/serverContext/serverContext.mjs:29:11) at Proxy.fetchAuthSession (file:///Users/asmajlovic/code/dexp-fe-v2/node_modules/.pnpm/@aws-amplify+core@6.0.2/node_modules/@aws-amplify/core/dist/esm/singleton/apis/server/fetchAuthSession.mjs:9:31) at load (/Users/asmajlovic/code/dexp-fe-v2/src/routes/be/+page.server.ts:16:49) at Module.load_server_data (/Users/asmajlovic/code/dexp-fe-v2/node_modules/.pnpm/@sveltejs+kit@1.27.6_svelte@5.0.0-next.4_vite@4.5.0/node_modules/@sveltejs/kit/src/runtime/server/page/load_data.js:57:41) at eval (/Users/asmajlovic/code/dexp-fe-v2/node_modules/.pnpm/@sveltejs+kit@1.27.6_svelte@5.0.0-next.4_vite@4.5.0/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:150:41) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) ```

aws-exports.js

No response

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

nadetastic commented 11 months ago

Hi @asmajlovicmars, thank you for opening this issue.

With Amplify JS v6 we provide an adapter for only Next.js, although the underlying generic adapters may be used with other frameworks. We will provide instructions of using the generic adapters with other frameworks soon, but for now II'll mark this as a feature request and follow up once the instructions are available.

taoatmars commented 11 months ago

@nadetastic same problem here looks like we have a situation that will break our production code and no way around it. is there a possibility that you can point me in the right direction for how to use the generic adapters?

asmajlovicmars commented 11 months ago

Primarily, these modifications enable compatibility between SvelteKit and Amplify v6, supporting both client-side rendering (CSR) and server-side rendering (SSR). While the entire application has not been converted yet, and all edge cases have not been tested, this marks a progressive step towards full integration.

// signin.svelte
// ...
import amplifyConfig from '../../amplifyconfiguration.json';
Amplify.configure({ Auth: authConfig }, { ssr: true }); // ssr: true will enable creation of cookies
await signIn({username: '...', password: '...'}); // will create CognitoIdentity jwt cookies
invalidateAll() // or use a milder version, just to make a new request, and trigger hooks
// ...
// hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import amplifyConfig from '../../amplifyconfiguration.json';

import { Amplify } from 'aws-amplify';
// import { getCookiePayload, getSpecificCookie } from '$lib/utils/cookie-helpers';

type Cookie = { name: string; value: string } | null;

const getSpecificCookie = (cookies: any, name: string) =>
    cookies
        .map((cookie: Cookie) => (cookie?.name?.endsWith(name) ? cookie?.value : null))
        .filter((value: string) => value !== null)[0];

const getCookiePayload = (cookie: string) => {
    const payload = cookie?.split('.')[1];
    const parsedPayload = JSON.parse(atob(payload));
    return parsedPayload;
};

Amplify.configure(amplifyConfig, { ssr: true }); // not sure if { ssr: true } is a must, or needed here...

export const handleUser = (async ({ event, resolve }): Promise<Response> => {
    const { request, cookies, locals, getClientAddress } = event;

    const allCookies = cookies.getAll();
    const cognitoCookies = allCookies.filter(
        (cookie) => cookie.name.startsWith('CognitoIdentityServiceProvider') && cookie.value !== null
    );

    if (cognitoCookies?.length === 0) {
        locals.jwtAccessToken = null;
        locals.jwtIdToken = null;
        locals.idTokenPayload = null;
        locals.accessTokenPayload = null;

        locals.userId = null;
        locals.username = null;
        locals.email = null;
        locals.userGroups = null;

        console.log('❌ no cookies:');

        return resolve(event);
    }

    const idToken = getSpecificCookie(cognitoCookies, 'idToken');
    const accessToken = getSpecificCookie(cognitoCookies, 'accessToken');
    const idTokenPayload = idToken && getCookiePayload(idToken);
    const accessTokenPayload = accessToken && getCookiePayload(accessToken);

    locals.jwtAccessToken = accessToken;
    locals.jwtIdToken = idToken;
    locals.idTokenPayload = idTokenPayload;
    locals.accessTokenPayload = accessTokenPayload;

    locals.userId = idTokenPayload?.sub;
    locals.username = idTokenPayload?.sub;
    locals.email = idTokenPayload?.email;
    locals.userGroups = idTokenPayload?.['cognito:groups'];

    console.log('✅ signed-in email:', locals.email);
    return resolve(event);
}) satisfies Handle;
// +layout.svelte or any other +page.svelte
<script lang="ts">
    import { fetchAuthSession, getCurrentUser } from '@aws-amplify/auth';
    import { Amplify, type ResourcesConfig } from 'aws-amplify';
    import amplifyConfig from '../amplifyconfiguration.json';

    import type { LayoutData } from './$types';
    import { onMount } from 'svelte';

    export let data: LayoutData;

    const authConfig: ResourcesConfig['Auth'] = {
        Cognito: {
            userPoolId: amplifyConfig.aws_user_pools_id,
            userPoolClientId: amplifyConfig.aws_user_pools_web_client_id
            }
    };

    Amplify.configure({ Auth: authConfig}, { ssr: true });

    onMount(async () => {
        await fetchAuthSession(); // will refresh tokens!!! 🎉
    });
</script>

<nav>
    <ul class="horizontal-nav">
        <li>
            <a href="/">Home</a>
        </li>
        <li>
            <a href="/signin">Sign In</a>
        </li>
    </ul>
</nav>
<slot />
swaminator commented 11 months ago

@asmajlovicmars thank you for your request. To improve the SSR experience in V6, we have moved to an adapter pattern that requires a custom implementation per framework. While we have shipped V6 with a Next.js adapter built-in, we would love to support more. In the next few days we are going to publish a document about the adapter with an example of how to build one. We are treating this issue as priority and will get back to you once we have the document published.

asmajlovicmars commented 11 months ago

Thank you, @swaminator! I'm progressing through the migration from version 5 to 6, and things are improving. However, it would be much easier and more efficient to use a dedicated Vite/SvelteKit adapter. I'm looking forward to it, especially since we've committed to using both Amplify and SvelteKit, and not giving up 😁

nadetastic commented 10 months ago

Hi @asmajlovicmars @taoatmars following up here - we have published documentation on how to use the generic adapters and can be viewed here - https://docs.amplify.aws/react/build-a-backend/server-side-rendering/

Let me know if you have any questions.

asmajlovicmars commented 10 months ago

This is excellent! Thank you @nadetastic. Will test ASAP.

asmajlovicmars commented 10 months ago

@nadetastic, @swaminator Thank you both!

Just two corrections in the docs, getCurrentUser import on line 8, and line 57 should be called with the context:

line 8: import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth/server';

line 57: const { username } = await getCurrentUser(contextSpec);
kewur commented 9 months ago

hi, I'm trying to have calls to my backend in asp.net and from my angular application. I understand the documentation and the sample on here, however, I'm unsure how to make the calls with the amplify auth token on the first load of the page, I understand that behind the scenes a cookie is being set and the server uses the provider to get said cookie (that has auth tokens).

my first question: are all of the calls that needs to return user specific context all needs to be wrapped with the "runWithAmplifyServerContext" from the client side? Or did I misunderstood something here, does "runWithAmplifyServerContext" replace Amplify.configure() ?

second question: my ssr provider is angular ssr, however my backends that needs to provide user context are in ASP.net core, do I need to create a custom solution that extracts cookies from http requests if I was to onboard with what's happening here?

would love it if I can get some clarity on if this solution helps my case or I need to implement something on my own for this

benmccann commented 4 months ago

I don't know if it addresses with this issue specifically, but there is now a SvelteKit Amplify adapter: https://github.com/gzimbron/amplify-adapter. If there are any issues, it'd be great to solve them there so that folks don't have to re-invent the wheel. The full list of adapters is available on https://www.sveltesociety.dev/packages?category=sveltekit-adapters - a few platforms have multiple adapters available

If anyone from AWS is interested in adding official first-party support for SvelteKit, please ping me and I'd be happy to add you to SvelteKit's partners program so that you can get direct support from the maintainers in supporting your integration

dlamon1 commented 3 months ago

@benmccann The amplify-adapter does not include methods for handling server side cookies or generating the server side API method.

dlamon1 commented 3 months ago

Hi @asmajlovicmars @taoatmars following up here - we have published documentation on how to use the generic adapters and can be viewed here - https://docs.amplify.aws/react/build-a-backend/server-side-rendering/

Let me know if you have any questions.

Have the docs changed ? I don't see a guide for generic implementation

HuiSF commented 3 months ago

Hi @dlamon1 apologies for the inconvenience, the documentation about the generic SSR adapter currently is available under the "Gen1" selector: https://docs.amplify.aws/gen1/react/build-a-backend/server-side-rendering/