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:
- What is SvelteKit Auth?
- Building our SvelteKit app
- AWS Cognito setup
- AWS Cognito implementation
- Creating the root layout
- Redirecting the user to the login page
- Session and redirection
What is SvelteKit Auth?
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.
Building our SvelteKit 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
AWS Cognito setup
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
AWS Cognito implementation
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.
Adding a custom credentials auth
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: "test@test.com" }, 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 iscredentials
id
: ID of the provider. In our case, it iscredentials
name
: Name of the provider. In our case, it isCognito
credentials
: Object containing the credentials. In our case, it isemail
andpassword
authorize
: The function will be called when the user tries to log in. In our case, we call thegetSession
function that we created earlier and return the user object from the session data
In 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.
Creating the root layout
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.
Redirecting 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
.
Session and redirection
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.
Conclusion
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.
The post SvelteKit Auth with AWS Cognito appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/0Qq2F8O
Gain $200 in a week
via Read more