This is a premium alert message you can set from Layout! Get Now!

SvelteKit Auth with AWS Cognito

0

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?

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 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 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

Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.
Post a Comment

Search This Blog

To Top