The last quarter of 2022 saw some really cool announcements in the frontend world. One of them was NextAuth.js becoming Auth.js and bringing the awesome developer experience of NextAuth.js to other popular web frameworks. With the backing of Vercel and an amazing community, Auth.js will soon be a go-to solution for authentication in all of the popular web frameworks.
But what if you don’t use Next.js or React? What if you use SvelteKit? Well, you’re in luck because SvelteKit Auth is here to save the day. SvelteKit Auth is a SvelteKit module that provides authentication for SvelteKit apps.
In this article, we’ll cover:
SvelteKit Auth is a SvelteKit module that provides authentication for SvelteKit apps. Built on top of Auth.js, SvelteKit Auth allows you to add authentication providers and customize the authentication flow of your app.
Let’s create a new SvelteKit project and add AWS Cognito authentication to it. We’ll add AWS Cognito authentication using custom credentials, and then get auth token and session data on both the server and client side until the inner layouts.
First, let’s scaffold a new SvelteKit project using the official guide with TypeScript:
npm create svelte@latest skauth-congito-demo
This will give us bare metal SvelteKit.
Next, we’ll add the latest version of SvelteKit Auth to our project:
pnpm install @auth/sveltekit@next @auth/core@next
We will be using AWS Cognito for authentication. We need to set up a new AWS Cognito user pool and an app client. You can set up the AWS Cognito user pool using this official guide. Once you have COGNITO_USER_POOL_ID
and COGNITO_CLIENT_ID
, you can carry on with implementation.
Now that we have our AWS Cognito user pool and app client ready, we will add the custom credentials auth to our SvelteKit project. To begin, let’s add AWS Cognito SDK to our project:
pnpm install amazon-cognito-identity-js
We’ll add the following environment variables to our project. We’ll use these environment variables to get the user pool ID and app client ID:
# .env COGNITO_USER_POOL_ID=us-east-1_XXXXXXXXX COGNITO_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXX
Working with domain driven architecture, we will keep all our auth-related modules in the src/lib/domain/auth
directory.
Create a new file src/lib/domain/auth/services/Cognito.ts
and add the following code to it:
/** * @file Cognito.ts * File containing the Cognito service */ import { COGNITO_USER_POOL_ID, COGNITO_CLIENT_ID } from '$env/static/private'; import { AuthenticationDetails, CognitoRefreshToken, CognitoUser, CognitoUserPool, CognitoUserSession } from 'amazon-cognito-identity-js'; export type CognitoUserSessionType = CognitoUserSession; const CONFIGS = { UserPoolId: COGNITO_USER_POOL_ID, ClientId: COGNITO_CLIENT_ID }; // Create a new Cognito User Pool const Pool = new CognitoUserPool(CONFIGS); // Wrapper function to create a new Cognito User from the User Pool const User = (Username: string): CognitoUser => new CognitoUser({ Username, Pool }); /** * Login to Cognito User Pool using the provided credentials. * This will return the session data at the time of login. * * @param Username - Email address of the user to login * @param Password - Password of the user to login * @returns - Promise with the result of the login */ export const getSession = (Username: string, Password: string): Promise<CognitoUserSession> => { return new Promise((resolve, reject) => User(Username).authenticateUser(new AuthenticationDetails({ Username, Password }), { onSuccess: resolve, onFailure: reject, }) ); }; /** * Refresh the access token of the provided user. * We will use this method to refresh the access token from our axios interceptor * * @param sessionData - Session data of the user with the refresh token * @returns - Promise with the new user object with tokens and expiration date */ export const refreshAccessToken = async (sessionData: { refreshToken: string; }): Promise<CognitoUserSession> => { const cognitoUser = Pool.getCurrentUser(); // Check if the user is logged in if (!cognitoUser) { throw new Error('No user found'); } // Refresh the session const RefreshToken = new CognitoRefreshToken({ RefreshToken: sessionData.refreshToken, }); return new Promise<CognitoUserSession>((resolve) => { cognitoUser.refreshSession(RefreshToken, (_resp, session: CognitoUserSession) => { resolve(session); }); }); }
This file implements all required methods to log into the AWS Cognito user pool and refresh the access token. We will be using this file in our SvelteKit Auth module.
SvelteKit Auth leverages server-side hooks
provided by SvelteKit to implement authentication features. In our app, we will customize and create src/hooks.server.ts
.
Here is how it will look like. This might look a little complex but we will go through each step in detail:
/** * @file src/hooks.service.ts * File containing the hooks service */ // Import the SvelteKit Auth module import { SvelteKitAuth } from "@auth/sveltekit" import Credentials from "@auth/core/providers/credentials" // Import the Cognito service that we created earlier import { getSession, refreshAccessToken, type CognitoUserSessionType } from "$lib/domains/auth/services/Cognito" // Type of the user object returned from the Cognito service import type AuthUser from "$lib/domains/auth/types/AuthUser"; // Import the secret key from the environment variables import { AUTH_SECRET } from "$env/static/private"; interface AuthToken { accessToken: string; accessTokenExpires: number; refreshToken: string; user: { id: string; name: string; email: string; }; } /** * Extract the user object from the session data. This is a helper function that we will use to extract the user object from the session data returned from the Cognito service. */ const extractUserFromSession = (session: CognitoUserSessionType): AuthUser => { if (!session?.isValid?.()) throw new Error('Invalid session'); const user = session.getIdToken().payload; return { id: user.sub, name: `${user.name} ${user.family_name}`, email: user.email, image: user.picture, accessToken: session.getAccessToken().getJwtToken(), accessTokenExpires: session.getAccessToken().getExpiration(), refreshToken: session.getRefreshToken().getToken(), } } /** * Create the token object from the user object. This is a helper function that we will use to create the token object from the user object returned from the Cognito service. */ const createTokenFromUser = (user: AuthUser): AuthToken => { return { accessToken: user.accessToken, accessTokenExpires: user.accessTokenExpires, refreshToken: user.refreshToken, user: { id: user.id, name: user.name, email: user.email, image: user.image, }, } } export const handle = SvelteKitAuth({ secret: AUTH_SECRET, providers: [ Credentials({ type: 'credentials', id: 'credentials', name: 'Cognito', credentials: { email: { label: "Email", type: "email", placeholder: "[email protected]" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { if (!credentials) return null try { const response = await getSession(credentials?.email, credentials?.password) return extractUserFromSession(response) } catch (error) { console.error(error); return null } } }) as any, ], /** * Since we are using custom implementation; we have defined URLs for the login and error pages */ pages: { signIn: "/auth/login", error: "/auth/login", }, callbacks: { /** * This callback is called whenever a JWT is created or updated. * For the first time login we are creating a token from the user object returned by the authorize callback. * For subsequent requests we are refreshing the access token and creating a new token from the user object. If the refresh token has expired * */ async jwt({ token, user, account }: any) { // Initial sign in; we have plugged tokens and expiry date into the user object in the authorize callback; object // returned here will be saved in the JWT and will be available in the session callback as well as this callback // on next requests if (account && user) { return createTokenFromUser(user); } // Return previous token if the access token has not expired yet if (Date.now() < token?.accessTokenExpires) { return token; } try { const newUserSession = await refreshAccessToken({ refreshToken: token?.refreshToken, }) const user = extractUserFromSession(newUserSession); return createTokenFromUser(user); } catch(error) { console.error(error); throw new Error('Invalid session'); } }, /** * The session callback is called whenever a session is checked. By default, only a subset of the token is * returned for increased security. We are sending properties required for the client side to work. * * @param session - Session object * @param token - Decrypted JWT that we returned in the jwt callback * @returns - Promise with the result of the session */ async session({ session, token }: any) { session.user = token.user session.accessToken = token.accessToken session.error = token.error return session; }, }, });
In the above code, we have defined the handle
function that SvelteKit will use to handle the authentication. We used the SvelteKitAuth function from the SvelteKit Auth module to create the handle
function. We also used Credentials
from the SvelteKit Auth module to create the credentials provider.
The Credentials
function takes an object as an argument. The object has the following properties:
type
: Type of the provider. In our case, it is credentials
id
: ID of the provider. In our case, it is credentials
name
: Name of the provider. In our case, it is Cognito
credentials
: Object containing the credentials. In our case, it is email
and password
authorize
: The function will be called when the user tries to log in. In our case, we call the getSession
function that we created earlier and return the user object from the session dataIn the pages
property, we have defined the URLs for the login and error pages. SvelteKit Auth will redirect the user to the login page if not authenticated. Because we have kept the same URLs for error and sign-in, we will receive the error message on the login page in query params.
In callbacks
, we have implemented the jwt
and session
methods. The jwt
method will be called whenever a JWT is created from the authorize
method we have defined. The session
method is called whenever a session is checked.
Now that we have SevelteKit Auth configured, we need to create a root layout that will be used by all the pages.
Create a src/routes/+layout.svelte
file and add the following code:
<script lang="ts"> import { signOut } from "@auth/sveltekit/client" import { page } from "$app/stores" </script> <div> <header> {#if $page.data.session} <div> <strong>Hello {$page.data.session.user?.name}</strong> <button on:click|preventDefault={signOut} class="button">Sign out</button> </div> {:else} <a href="/auth/login" class="buttonPrimary">Sign in</a> {/if} </header> <slot /> </div>
SvelteKit Auth will set page data with the session object as soon as the user authenticates. We can use the session
data to check whether the user is authorized.
In our example, If the user is authenticated, we will show the user name and a sign-out button. We will show a sign-in button if the user is unauthenticated. Here, the signOut
function is used directly from the SvelteKit Auth module. This function will sign out the user and redirect the user to the login page.
Now that we have the root layout, we can create the login page. We will create a src/routes/auth/login/+page.svelte
file and add the following code:
<script lang="ts"> import { signIn } from "@auth/sveltekit/client" import { invalidateAll } from '$app/navigation'; const handleSubmit = async (event: any) => { const data = new FormData(event.target); try { await signIn('credentials', { email: data.get('email'), password: data.get('password') }); } catch (error) { await invalidateAll(); } } </script> <h1>Login</h1> <div> <form name="login" method="POST" on:submit|preventDefault={handleSubmit}> <input name="email" type="email" placeholder="Email Address" /> <input name="password" placeholder="Password" type="password" /> <button>Sign In</button> </form> </div>
In the above code, we used the signIn
function from the SvelteKit Auth module to sign in the user. This will call authorize
, which we defined in Credentials
. If the user is authenticated successfully, the jwt
callback will be called and the user will be redirected to the homepage with the session data we returned from session
callback in hooks.server.ts
.
As discussed in the root layout, SvelteKit Auth will set the session data in the page store. We can access this data in the sub-layouts. To make sure that our authenticated routes are not accessible to unauthenticated users, we will place all of our authenticated routes under the src/lib/routes/(auth)
directory. Here, we are leveraging the amazing advanced layout technique of SvelteKit.
Now let’s create a +layout.server.ts
file in that directory:
import { redirect } from '@sveltejs/kit'; import type { LayoutServerLoad } from './$types'; import { getAccount } from '$lib/domains/auth/api/getAccount'; export const load: LayoutServerLoad = async ({ locals }) => { // Get the session from the locals const session = (await locals.getSession()) as any; // If the user is not authenticated, redirect to the login page if (!session?.user?.id || !session?.accessToken) { throw redirect(307, '/auth/login'); } // Get the account details at the root layout level so that we can use it in the sub layouts const account = await getAccount(session?.user?.id, session?.accessToken); // If the account is not found, redirect to the login page if (!account) { throw redirect(307, '/auth/login'); } // On success, we can send the session and account data to the sub layouts return { session, account, }; };
Now all of the data in session, i.e., the required tokens and user account details, are available on both server and client side and we can use them to make authenticated requests to the backend and/or for any other purpose.
Auth.js is a useful library for the implementation of authentication for most popular web frameworks today. It has made it quite easy to implement redirection-based logins and, on top of that, it provides great flexibility and allows us to implement our own authentication logic as per requirement.
What we have seen in this article is just a small part of what Auth.js can do. I highly recommend you check out the guide to getting started with Auth.js to learn more.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
13 Replies to "SvelteKit Auth with AWS Cognito"
Attempting to follow along with this guide and receive the following error when using the signIn function
Error: Not found: /auth/csrf
at resolve (/node_modules/@sveltejs/kit/src/runtime/server/respond.js:395:13)
at resolve (/node_modules/@sveltejs/kit/src/runtime/server/respond.js:236:5)
at Object.#options.hooks.handle (/node_modules/@sveltejs/kit/src/runtime/server/index.js:41:55)
at Module.respond (/node_modules/@sveltejs/kit/src/runtime/server/respond.js:233:40)
at runMicrotasks ()
at processTicksAndRejections (node:internal/process/task_queues:96:5)
Just wondering if there was some type of config I missed or if there is a problem with the package itself. I know it is experimental but would expect a core feature to work.
Mhhh that’s weird. I managed to have the library working. I twisted and adapted the tutorial to fit my project though.
Is there any thing you changed?
Maybe try deleting Cookies or try another browser just to test
Glad it helped you 🙂
Awesome content!
However, some info are missing about “import type AuthUser from “$lib/domains/auth/types/AuthUser”;”
You could also precise for “import { AUTH_SECRET } from “$env/static/private”;”
But really small details. Still going over the article. Thanks a lot, it’s perfect timing for me.
When I try this code, I get: NotAuthorizedException: Client XXXXXXXXXXX is configured for secret but secret was not received
Is this supposed to be configured for login with client id and secret, or just the client id? Which auth flow do you use? You mention using the AUTH_SECRET, so I’m assuming its using both the client id and secret. Also, I was using the Cognito provider for Auth.js, but I would like to stop using the Cognito Hosted UI, and switch to something like this instead.
Ah, nevermind. I changed my client to not require a secret and it worked.
I still don’t know where you define the
`import { getAccount } from ‘$lib/auth/api/getAccount’;`
There is no such file at this location :/
You can define it to get user’s complete data from your backend 🙂
could you explain further? I followed all your steps, but I am still getting this error. What should I be defining?
When running the command
pnpm install @auth/sveltekit@next @auth/core@next
I get an error
ERR_PNPM_NO_MATCHING_VERSION  No matching version found for @auth/sveltekit@next
The only way I could get it to run was using the command:
pnpm install @auth/sveltekit@’0.3.0′ @auth/core@’0.3.0′
@auth/core is at V 0.12.0
Thank you!
Hey there is an error when we use CookieStorage