DEV Community

Kanahiro Iguchi for MIERUNE

Posted on

AWS Amplify (Gen2) with SvelteKit: authentication on SSR

GitHub logo Kanahiro / sveltekit-amplify-starter

SvelteKit + Amplify Gen2 (Hosting + Backend) Template

sveltekit-amplify-starter

SvelteKit + Amplify Gen2 (Hosting, Backend)

usage

  1. Copy or Fork this repository
  2. Connect your repository in the Amplify Console
  3. Done

tips

  • ./src/routes/AmplifyInit.svelte automatically loads ./amplify_outputs.json generated by Amplify CLI on build time or sandbox mode. ./amplify_outputs.json should not be modified manually and so it is .gitignored.
  • Server Side Rendering (SSR) works good but streaming does not work.

references




TL;DR

  • AWS Amplify can run SvelteKit with SSR.
  • There is a implementation of SSR authentication for Next.js but not for SvelteKit.
  • Is this article, I'll show you my implementation for SSR authentication.

SvelteKit and AWS Amplify

https://kit.svelte.dev/

SvelteKit is a full-stack framework based on Svelte frontend library. SvelteKit supports server-side rendering (SSR). Next.js is in this field but I prefer SvelteKit because we can write very simple and efficient codes in SvelteKit, it's great.

AWS Amplify is service to build and deploy web application with some AWS services such as Cognito/Lambda/DynamoDB/S3 or so without deep knowledge about them.

In these days Amplify supports SSR and there are some examples to deploy SvelteKit with Amplify.

https://docs.aws.amazon.com/amplify/latest/userguide/get-started-sveltekit.html

It is nice we can deploy SvelteKit application only by connecting GitHub repo to Amplify. One point, we should note that SSR "Streaming" is not supported yet.

Amplify Gen2

https://docs.amplify.aws/javascript/

Actually, I'm not familiar with Amplify "Gen1". I talk about Gen2 in entrie this article.

Amplify Gen2 omits CLI command to add Amplify Backends(auth, data...). Instead of CLI, you can declaratively specify backends by codes in TypeScript and Gen2 provides us with "sandbox" feature, it enables us to launch backends only for your development, this is similar to "branching" in Supabase but it is interesting that Amplify make sandboxes for local development.

Amplify Auth

We can add authentication like this:

// amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';
export const auth = defineAuth({
    loginWith: {
        email: true
    }
});

// amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
const backend = defineBackend({
    auth
});
Enter fullscreen mode Exit fullscreen mode

Traditionally, you can access Amplify Auth(Cognito) from frontend only with following codes:

// initialization
import { Amplify } from 'aws-amplify';
import outputs from './amplify_outputs.json'; // generated by Amplify CLI

Amplify.configure(outputs);

// signin
import { signIn } from 'aws-amplify/auth'

await signIn({
  username: "hello@mycompany.com",
  password: "hunter2",
})
// credentials stored in LocalStorage
// ...perform authorized actions for backends with credentials
Enter fullscreen mode Exit fullscreen mode

This approach have been standard in Amplify but in context of SSR we need better approach than this. Assuming we have public pages and private pages. Is SSR, server should return private pages only for authenticated users. To adjust authentication processes for SSR, we need to:

  1. send credentials from client to server
  2. verify credentials
  3. navigate only verified users to private pages

Adapter for Next.js

For Next.js, an "adapter" is officially provided by Amplify.

https://docs.amplify.aws/react/build-a-backend/server-side-rendering/

It is not for SvelteKit. Then I tried to write some codes.

Adapter for SvelteKit

The procedure to serve pages in SvelteKit is similar to in Next.js. I wrote adapter for SvelteKit quoting some codes of adapter-nextjs. The following is parts of them:

Initialization

import { Amplify } from 'aws-amplify';
import outputs from '../../amplify_outputs.json';

Amplify.configure(outputs, { ssr: true });
Enter fullscreen mode Exit fullscreen mode

{ ssr: true } means to use cookie for storing credentials instead of LocalStorage. Once signed in, credentials are wrote to cookie and they are sent to server on accessing any pages.

Response Hooks

// src/hooks.server.ts

import { redirect, type Handle } from '@sveltejs/kit';
import { fetchAuthSession } from 'aws-amplify/auth/server';

import { createRunWithAmplifyServerContext } from '$lib/adapter-sveltekit';

import outputs from '../amplify_outputs.json';

// init auth-checker with outputs once when the server starts
const runWithAmplifyServerContext = createRunWithAmplifyServerContext(outputs);

export const handle: Handle = async ({ event, resolve }): Promise<Response> => {
    if (!event.url.pathname.startsWith('/private')) {
        return resolve(event);
    }

    const authenticated = await runWithAmplifyServerContext({
        event,
        operation: async (contextSpec) => {
            try {
                const session = await fetchAuthSession(contextSpec);
                return session.tokens?.accessToken !== undefined && session.tokens?.idToken !== undefined;
            } catch (error) {
                console.log(error);
                return false;
            }
        }
    });

    if (!authenticated) {
        redirect(303, '/');
    } else {
        return resolve(event);
    }
};
Enter fullscreen mode Exit fullscreen mode
  • handle() in hooks.server.ts is called on before every response. Only verified users can access pages under /private routes.
  • The signatures are designed to mimic adapter-nextjs

$lib/adapter-sveltekit is the most important parts, let's go next.

adapter-sveltekit

src/lib/adapter-sveltekit/
├── createCookieStorage.ts
├── createRunWithAmplifyServerContext.ts
├── createTokenValidator.ts
├── index.ts
└── isValidCognitoToken.ts
Enter fullscreen mode Exit fullscreen mode

There some files but createRunWithAmplifyServerContext.ts is the essential part of this module.

// src/lib/adapter-sveltekit/createRunWithAmplifyServerContext.ts

import type { RequestEvent } from '@sveltejs/kit';
import {
    createAWSCredentialsAndIdentityIdProvider,
    createKeyValueStorageFromCookieStorageAdapter,
    createUserPoolsTokenProvider,
    runWithAmplifyServerContext as runWithAmplifyServerContextCore,
    type AmplifyOutputs,
    type AmplifyServer
} from 'aws-amplify/adapter-core';
import { sharedInMemoryStorage, parseAmplifyConfig } from 'aws-amplify/utils';

import { createTokenValidator } from './createTokenValidator';
import { createCookieStorage } from './createCookieStorage';

type RunWithAmplifyServerContextOptions = {
    event: RequestEvent | null;
    operation: (contextSpec: AmplifyServer.ContextSpec) => boolean | Promise<boolean>;
};

function createRunWithAmplifyServerContext(outputs: AmplifyOutputs) {
    const resourcesConfig = parseAmplifyConfig(outputs);

    const runWithAmplifyServerContext = async ({
        event,
        operation
    }: RunWithAmplifyServerContextOptions) => {
        const keyValueStorage = event
            ? createKeyValueStorageFromCookieStorageAdapter(
                    createCookieStorage(event.cookies),
                    createTokenValidator({
                        userPoolId: resourcesConfig.Auth?.Cognito?.userPoolId,
                        userPoolClientId: resourcesConfig.Auth?.Cognito?.userPoolClientId
                    })
                )
            : sharedInMemoryStorage;

        const credentialsProvider = createAWSCredentialsAndIdentityIdProvider(
            resourcesConfig.Auth!,
            keyValueStorage
        );
        const tokenProvider = createUserPoolsTokenProvider(resourcesConfig.Auth!, keyValueStorage);

        return await runWithAmplifyServerContextCore(
            resourcesConfig,
            {
                Auth: { credentialsProvider, tokenProvider }
            },
            operation
        );
    };

    return runWithAmplifyServerContext;
}

export { createRunWithAmplifyServerContext };
Enter fullscreen mode Exit fullscreen mode
  • This codes mimic Next.js one.
  • Accept RequestEvent in SvelteKit instead of "Context" in Next.js

Workaround for signOut

aws-amplify/auth provides signOut() but this doesn't work for SSR mode, it might discard only LocalStorage. The cookies wrote by Amplify seems to have "HttpOnly" so we have to revoke cookies from server.

// src/routes/signin/+page.svelte
<form method="POST" action="?/signOut">
    <button type="submit">Sign Out</button>
</form>
Enter fullscreen mode Exit fullscreen mode
// src/routes/signin/+page.server.ts
export const actions = {
    signOut: async ({ cookies }) => {
        // remove all cookies startsWith "CognitoIdentityServiceProvider"
        const cognitoCookies = cookies
            .getAll()
            .filter((cookie) => cookie.name.startsWith('CognitoIdentityServiceProvider'));
        for (const cookie of cognitoCookies) {
            cookies.set(cookie.name, '', { maxAge: 0, path: '/' });
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

This is a good case to use form action in SvelteKit. By sending POST request via form and invoke an action named as "signOut", server overwrites the cookie as "expired". We can find cookies of Amplify by checking prefix "CognitoIdentityServiceProvider".

Perfect! You can authenticate users in SSR context in SvelteKit!.

Conclusion

I shown that SvelteKit can be used in Amplify, with SSR authentication. SvelteKit is the great framework and I hope that Amplify, which deploy applications easily, can support SvelteKit more.

The all codes which can be launched are here:

https://github.com/Kanahiro/sveltekit-amplify-starter

Future works

  • Auth UI for Svelte like React or Vue.

Top comments (0)