KINDERAS.COM

Fri Jul 21 2023

Implementing login and authentication for SvelteKit using AWS Cognito

In this article we'll describe how to use AWS Cognito for auth in a SvelteKit project. We will not be using any library such as AWS Amplify. The reason for this is that we want to understand all the steps required, and because external libraries can cause problems when running in Edge environments that doesn't support all of Node.js.

Overall architecture

  • We are using the AWS Cognito provided login page (no custom login UI)
  • All communication with AWS Cognito is server to server
  • Tokens are stored in cookies

Configuring AWS Cognito.

Before we get into the application implementation, we need to configure AWS Cognito.

For this example we'll be using the hosted UI provided by Cognito, however you can implement your own sign in page if you need to.

To configure Cognito you can follow the documentation provided by AWS. Just use the defaults, and see the section below when configuring the app client.

Setting up the app client

Since we are handling all communication with the Cognito API from the server side, we will use the Confidential client option when configuring the App Client during the Cognito setup (see the image below).

Note: For the Hosted UI - Allowed callback URLs section you can use http://localhost:5173/callback/. This can be updated later.

A screenshot of the AWS Cognito app client configuration wizard. The Confidential client option is selected.
Use the «Confidential client» option when configuring the App Client

Summary of App client setting

Anything not mentioned below can be left as the default values suggested by the setup wizard.

  • App client
    • App type - Confidential client
    • App client name - [can be anything you want]
    • Client secret - Generate a client secret selected
    • Leave the rest to default values
  • Hosted UI settings
    • Allowed callback URLs - http://localhost:5173/callback/

The SvelteKit app

Go ahead and create a basic SvelteKit app as described in the documentation.

There are 4 main subjects we are going to cover.

  1. Allowing the user to sign in or create a user
  2. Getting id, access and refresh tokens
  3. Protecting certain routes/pages
  4. Updating the id and access token using the refresh token

The basic setup for the SvelteKit project

The project has three routes.

  1. Home route - (/) Shows a login link and the email of the logged in user
  2. Callback - (/callback) This is the callback url we used when setting up the App Client in the «Setting up Cognito» section above.
  3. Server hook - This is a server hook that handles tokens and cookies.
  4. Protected - (/protected) This route requires a valid token

We will also need some environment variables. Create an .env file and add the variables as follows:

  • COGNITO_CLIENT_ID - You'll find this under «App client information» as «Client ID» after you have created the App Client.
  • COGNITO_CLIENT_SECRET - Found right below the Client id as «Client secret».
  • COGNITO_BASE_URI - This is the «Cognito domain» for your user pool, for under your user pool, App Integration, Configuration for all app clients.
  • CF_PAGES_URL - This is the url for the SvelteKit app. Since I'm using Cloudflare this is automatically provided. Locally you can set this to http://localhost:5173/

Presenting a way for the user to sign in or create a user

Let's start with the root page and add a link for the user to sign in, and display the users e-mail if a user is signed in. Here are the Svelte file and the server side loader function for the home page.

Note: You don't need to generate the sign-in url on the server side, I just like to keep all auth logic on the server side.

The getSignInUrl() function below creates a url to the Cognito domain and the /login path. It passes all the credentials and a redirect url, which decided where to send the user after a successful login.

The getRedirectUrl() function looks like below.

It's really important that this this url exactly matches the url you added to the «Allowed callback URLs» list in the App Client setup, including any trailing slashes!

If the user clicks the «Sign in (or sign up)» link, they will end up on the Cognito hosted login page. They can create a user or log in.

A screen shot of the AWS Cognito default login UI for an app client
The default login page provided by AWS Cognito

After creating a new user or logging in they are redirected to the url we passed via the redirect_url parameter. That url doesn't exist yet. Let's take a look at this route next.

Handling logins and generating tokens

When the login Cognito page has handled creating the user, or logging the user in, it will redirect to the redirect_url we generated for the login link via the getSignInUrl() function above. The path for this route will be /callback. (This is the url you added to the Allowed callback URLs in the App Client when configuring Cognito).

The callback route is an API route, not a page. It's role is to handle the code passed to it from the Cognito login system and swap it for tokens. This is called a «Authorization code grant» flow, using OAuth terminology.

Let's create the src/routes/callback/+server.ts and see what the steps are for this route.

  • The authorization code is passed as a query param called code. We need to fetch the code and validate that it exists.
  • We pass the code along with our credentials to the /oauth2/token/ path and get some tokens back. This is done using the getTokens() function.
  • Then we store the id token and the refresh token in cookies to be used later (in the server hook).
  • Then we redirect back to the home page.
A note on refresh tokens. You might have heard that it's a bad idea to a store refresh token locally on the users machine. This is true, unless you have a revocation strategy, as Cognito does. Refresh tokens can be invalidated at any point in Cognito (via the api or by logging the user out), And, with short lived access and id tokens the refresh token will be validated by Cognito whenever new access and id tokens are requested.

Now, let's take a look at the getTokens() function.

This function can handle both generating the initial tokens using an authorization code, as well as taking a refresh token and fetching updated id and access tokens. We'll see how to refresh the tokens using this function later when we implement the server hook.

Protecting routes and refreshing tokens

The last part of (this long ass article) is the server hook handle function. The server hook is a SvelteKit specific feature, in other frameworks you might know it as middleware. We'll define the hook at src/hooks.server.ts and implement the handle function.

This function runs every time the SvelteKit server receives a request — whether that happens while the app is running, or during prerendering — and determines the response.

We'll use the hook for several tasks:

  • Read the id token cookie (if it is set) and parse it.
  • Set the email address in the locals object, to make it available the load function for pages that have a server load function.
  • For any route or page which requires the user to be logged in (this applies to anything under /protected in this example)
    • Check if the id token still exists
    • If not, get the refresh token
    • If we have the refresh token, create a new id token and update to cookie
    • If the refresh token does not exist, sign the user out and show the login page

Here's the code for the server hook handle function:

Any page or route under /protected will now require a valid token.

The getSignOutUrl function looks like this:

Final thoughts

This is an example application, hence it's not a complete production ready ...thing. You would need obvious stuff like a way for the user to sign out (use the /logout endpoint) and better error handling, UI for displaying error situations, passing a state to redirect the user to the correct page and so on. These things are not in the scope of this article, you can probably figure them out if you have gotten this far.

Resources