Azure AD Authentication in SvelteKit

Varun
10 min readMay 16, 2023

Do you need to add authentication to your SvelteKit app? One popular way to do this is to use Azure Active Directory (Azure AD), a cloud-based identity and access management service from Microsoft. With Azure AD, you can easily add user authentication to your app without having to manage user accounts and passwords yourself. In this guide, we’ll see how to create an app in Azure AD and implement Azure AD authentication in a SvelteKit app using the Authorization Code Flow.

Step 1: Create an App in Azure Active Directory

Before we can use Azure AD for authentication, we need to create an app in our Azure AD tenant. Here’s how to do it:

  1. Log in to the Azure portal.
  2. Select your Azure AD tenant from the menu on the left.
  3. Click on the “App registrations” menu item.
  4. Click on the “New registration” button.
  5. Give the app a name and choose the “Web” application type.
  6. Enter the “Redirect URI” for the app. This is the URL where Azure AD will redirect the user after authentication. For example, if the app is hosted at http://localhost:5173, Redirect URI should be http://localhost:5173/callback.
  7. Click on the “Register” button.
  8. Go to the “Certificates and Secrets” section of the app and create a new client secret by giving it a name and an expiration date.

Congratulations! You’ve now created an app in Azure AD. Make a note of the “Application (client) ID”, the “Directory (tenant) ID” and the “Client Secret” value, as we’ll need these later.

Step 2: Understanding the Authorization Code Flow

Before we dive into the code, let’s take a moment to understand how the Authorization Code Flow works as defined by OAuth2. This is the flow that Azure AD uses for authentication.

The Authorization Code Flow consists of the following steps:

  1. The client (i.e. your app) redirects the user to the authorization server (i.e. Azure AD), along with some parameters, such as the client ID and the Redirect URI.
  2. The user logs in to the authorization server and consents to the requested permissions.
  3. The authorization server redirects the user back to the client app, along with an authorization code.
  4. The client app exchanges the authorization code for an access token and a refresh token.
  5. The client app uses the access token to access protected resources on behalf of the user.

Step 3: Create a Sveltekit App and Implement Azure AD Authentication

Now that we understand the Authorization Code Flow, let’s create a SvelteKit app and implement Azure AD authentication. Here’s how to do it:

Before we start with creating a sveltekit app, please ensure that you have the latest version of Node installed in your system.

  1. Create a sveltekit app. We will use the Skeleton project as the starting point and use Typescript for type checking.
npm create svelte@latest sveltekit-aad-auth

Once the app is created, cd into the project folder and install the dependencies.

cd sveltekit-aad-auth
npm i

Once done, open the project in your favorite text editor.

2. Update the project folder structure as follows:

a) Since sveltekit allows us to group layouts together, we will create folder named “(protected)” under routes and move the +page.svelte file into this folder. Only authenticated users can access the resources in (protected) folder.

b) We will also create a “callback” folder under routes and add a +server.ts file in it. This name should be the same as mentioned in the redirect URI while registering the Azure AD app.

c) Add a hooks.server.ts file under the src folder. In sveltekit, all server requests first go through the “handle” function defined in the hooks.server file. This is where we check if the user is requesting a protected resource and serve the page only if the said user is authenticated. Update the hooks.server.ts file with the following code:

import type { Handle } from "@sveltejs/kit";

export const handle: Handle = async ({ event, resolve }) => {
return await resolve(event);
};

At the end of this step, the folder structure should look like this

Folder structure
Folder structure after step 2

3. Start the development server.

npm run dev -- --open

The application should open in your browser displaying the default page.

Default page served by sveltekit
Default page served by sveltekit

If we were to go into the hooks.server.ts file and update the code as follows:

import type { Handle } from "@sveltejs/kit";

export const handle: Handle = async ({ event, resolve }) => {
if (event.route.id && event.route.id.indexOf("(protected)") > 0) {
return new Response("Authentication required");
}
return await resolve(event);
};

the following page will be served:

Page served after updating the hooks.server.ts file
Page served after updating the hooks.server.ts file

We can now update this logic to send the user to Microsoft authentication page instead of displaying the “Authentication required” message.

4. Stop the development server and install the following dependency.

npm i -D @azure/msal-node

5. Create a .env file in the root of the project and update it with the following entries:

CLOUD_INSTANCE="https://login.microsoftonline.com/"
TENANT_ID=<tenant id from step 1>
CLIENT_ID=<client id from step 1>
CLIENT_SECRET=<client secret from step 1>
REDIRECT_URI=<callback uri from step 1>

Next create a “lib” folder under “src”. Inside “lib” create another folder called “auth” with “config.ts” and “services.ts” files. After this step the folder structure should look like this:

Folder structure after step 5
Folder structure after step 5

6. Run the server again.

npm run dev -- --open

7. Update the lib > auth > config.ts as follows:

import {
CLIENT_ID,
CLOUD_INSTANCE,
TENANT_ID,
CLIENT_SECRET,
} from "$env/static/private";

export const msalConfig = {
auth: {
clientId: CLIENT_ID,
authority: CLOUD_INSTANCE + TENANT_ID,
clientSecret: CLIENT_SECRET,
},
};

8. As mentioned earlier, the first step in the Authorization code flow is redirecting the client to Microsoft’s authentication page. This is done by the getAuthCodeUrl method provided by the msal-node library.

In order to implement this, update the lib > auth > services.ts file with the following code:

import type { RequestEvent } from "@sveltejs/kit";
import {
ConfidentialClientApplication,
CryptoProvider,
ResponseMode,
} from "@azure/msal-node";
import { REDIRECT_URI } from "$env/static/private";
import { dev } from "$app/environment";
import { msalConfig } from "./config";

const msalInstance = new ConfidentialClientApplication(msalConfig);
const cryptoProvider = new CryptoProvider();

export const redirectToAuthCodeUrl = async (event: RequestEvent) => {
const { verifier, challenge } = await cryptoProvider.generatePkceCodes();
const pkceCodes = {
challengeMethod: "S256",
verifier,
challenge,
};

const authCodeUrlRequest = {
redirectUri: REDIRECT_URI,
responseMode: ResponseMode.QUERY,
codeChallenge: pkceCodes.challenge,
codeChallengeMethod: pkceCodes.challengeMethod,
scopes: [],
};

try {
const authCodeUrl = await msalInstance.getAuthCodeUrl(authCodeUrlRequest);
event.cookies.set("pkceVerifier", verifier, {
httpOnly: true,
path: "/",
secure: !dev,
});
return authCodeUrl;
} catch (err) {
console.log(err);
}
};

Azure AD uses Authorization Code flow with PKCE (proof key for code exchange). The CryptoProvider class in the msal-node library can be used to generate the pkce codes that are sent to Azure AD while authentication. Our app should verify that the same codes are sent back to us from Azure after the authentication has been successful.

Next go to the hooks.server.ts file and update the code as follows:

import { redirect, type Handle } from "@sveltejs/kit";
import { redirectToAuthCodeUrl } from "$lib/auth/services";

export const handle: Handle = async ({ event, resolve }) => {
if (event.route.id && event.route.id.indexOf("(protected)") > 0) {
const authCodeUrl = await redirectToAuthCodeUrl(event);
if (authCodeUrl) throw redirect(302, authCodeUrl);
}
return await resolve(event);
};

Now if we go to http://localhost:5173, the client should automatically redirect to https://login.microsoftonline.com/

The user will need to login and provide consent for our app to access their email and profile details.

Based on whether the authentication is successful or not, Azure will send a “code” or “error” in the query string of the redirect URL. It is up to us to interpret this response and serve the app accordingly.

9. Open the lib > auth > services.ts file and update the code as follows:

import type { RequestEvent } from "@sveltejs/kit";
import {
ConfidentialClientApplication,
CryptoProvider,
ResponseMode,
} from "@azure/msal-node";
import { REDIRECT_URI } from "$env/static/private";
import { dev } from "$app/environment";
import { msalConfig } from "./config";

const msalInstance = new ConfidentialClientApplication(msalConfig);
const cryptoProvider = new CryptoProvider();

const cookiesConfig = {
httpOnly: true,
path: "/",
secure: !dev,
};

export const redirectToAuthCodeUrl = async (event: RequestEvent) => {
const { verifier, challenge } = await cryptoProvider.generatePkceCodes();
const pkceCodes = {
challengeMethod: "S256",
verifier,
challenge,
};

const authCodeUrlRequest = {
redirectUri: REDIRECT_URI,
responseMode: ResponseMode.QUERY,
codeChallenge: pkceCodes.challenge,
codeChallengeMethod: pkceCodes.challengeMethod,
scopes: [],
};

try {
const authCodeUrl = await msalInstance.getAuthCodeUrl(authCodeUrlRequest);
event.cookies.set("pkceVerifier", verifier, cookiesConfig);
return authCodeUrl;
} catch (err) {
console.log(err);
}
};

export const getTokens = async (event: RequestEvent) => {
const code = event.url.searchParams.get("code");
const error = event.url.searchParams.get("error");
if (code) {
const authCodeRequest = {
redirectUri: REDIRECT_URI,
code,
scopes: [],
codeVerifier: event.cookies.get("pkceVerifier"),
};
try {
const tokenResponse = await msalInstance.acquireTokenByCode(
authCodeRequest
);
event.cookies.set(
"accessToken",
tokenResponse.accessToken,
cookiesConfig
);
event.cookies.set("idToken", tokenResponse.idToken, cookiesConfig);
event.cookies.set(
"account",
JSON.stringify(tokenResponse.account),
cookiesConfig
);
return "/";
} catch (err) {
console.log(error);
throw new Error("Error getting access tokens");
}
} else if (error) {
throw new Error(error);
} else {
throw new Error("Invalid authentication request");
}
};

We add a getTokens method that accepts a request event as an input parameter. We can retrieve the query parameter from this event object to check if it returned a “code” or “error”. Passing the “code” and the “pkce verifier” (that was generated while getting the auth code url) to the acquireTokenByCode method will give us the access and id tokens. We can save these in a http only cookie for later use and send the user back to the home page as shown in the above code.

We then need to call the getToken method in the routes > callback > +server.ts file as follows:

import { redirect } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getTokens } from "$lib/auth/services";

export const GET: RequestHandler = async (event) => {
const redirectTo = await getTokens(event);
throw redirect(302, redirectTo);
};

Since the user is getting redirected to “/”, we need to update the hooks.server.ts file to redirect the users to the authentication server only if they are not already authenticated. Update the hooks.server.ts file as follows:

import { redirect, type Handle } from "@sveltejs/kit";
import { redirectToAuthCodeUrl } from "$lib/auth/services";

export const handle: Handle = async ({ event, resolve }) => {
if (event.route.id && event.route.id.indexOf("(protected)") > 0) {
if (!event.cookies.get("idToken") || !event.cookies.get("accessToken")) {
const authCodeUrl = await redirectToAuthCodeUrl(event);
if (authCodeUrl) throw redirect(302, authCodeUrl);
}
}
return await resolve(event);
};

With this we have successfully authenticated the user, however there are a few enhancements we need to make in order to improve the user experience and protect the app against CSRF attacks.

At present, after the user is authenticated, we are always redirecting them to the “/” page, even if the user initiated the authentication from a different page. In order to prevent this, we are provided with a state parameter that can be used to tell the authentication server from which page the request was initiated. After completing the authentication process, the auth server returns the state parameter as is and can hence be used to redirect the user back to the resource that was initially requested.

The state parameter can also be used to determine that the response is actually coming from the authentication server and not from a malicious party by adding a unique nonce (or csrf token). We need to store this token in the server before sending the request to the authentication server and verify that the same token is returned after a successful login.

10. Update the lib > auth > services.ts file as follows:

import type { RequestEvent } from "@sveltejs/kit";
import {
ConfidentialClientApplication,
CryptoProvider,
ResponseMode,
} from "@azure/msal-node";
import { REDIRECT_URI } from "$env/static/private";
import { dev } from "$app/environment";
import { msalConfig } from "./config";

const msalInstance = new ConfidentialClientApplication(msalConfig);
const cryptoProvider = new CryptoProvider();

const cookiesConfig = {
httpOnly: true,
path: "/",
secure: !dev,
};

export const redirectToAuthCodeUrl = async (event: RequestEvent) => {
const { verifier, challenge } = await cryptoProvider.generatePkceCodes();
const pkceCodes = {
challengeMethod: "S256",
verifier,
challenge,
};
const csrfToken = cryptoProvider.createNewGuid();
const state = cryptoProvider.base64Encode(
JSON.stringify({
csrfToken,
redirectTo: event.url.pathname,
})
);
const authCodeUrlRequest = {
redirectUri: REDIRECT_URI,
responseMode: ResponseMode.QUERY,
codeChallenge: pkceCodes.challenge,
codeChallengeMethod: pkceCodes.challengeMethod,
scopes: [],
state,
};

try {
const authCodeUrl = await msalInstance.getAuthCodeUrl(authCodeUrlRequest);
event.cookies.set("pkceVerifier", verifier, cookiesConfig);
event.cookies.set("csrfToken", csrfToken, cookiesConfig);
return authCodeUrl;
} catch (err) {
console.log(err);
}
};

export const getTokens = async (event: RequestEvent) => {
const state = event.url.searchParams.get("state");
if (state) {
const decodedState = JSON.parse(cryptoProvider.base64Decode(state));
const csrfToken = event.cookies.get("csrfToken");
if (decodedState.csrfToken === csrfToken) {
const code = event.url.searchParams.get("code");
const error = event.url.searchParams.get("error");
if (code) {
const authCodeRequest = {
redirectUri: REDIRECT_URI,
code,
scopes: [],
codeVerifier: event.cookies.get("pkceVerifier"),
};
try {
const tokenResponse = await msalInstance.acquireTokenByCode(
authCodeRequest
);
event.cookies.set(
"accessToken",
tokenResponse.accessToken,
cookiesConfig
);
event.cookies.set("idToken", tokenResponse.idToken, cookiesConfig);
event.cookies.set(
"account",
JSON.stringify(tokenResponse.account),
cookiesConfig
);
return decodedState.redirectTo;
} catch (err) {
console.log(error);
}
} else if (error) {
throw new Error(error);
}
} else {
throw new Error("CSRF token mismatch");
}
} else {
throw new Error("State parameter missing");
}
};

We have updated the redirectToAuthCodeUrl method to include a state object that has a GUID as a CSRF token and a “redirect to path”.

In the getTokens method we retrieve this state object from the query parameters to get the CSRF token and the “redirect to path”.

11. Finally we need to implement the logout functionality.

First, add a getLogoutUri method in the lib > auth > services.ts file as follows:

export const getLogoutUri = () => {
return `${msalConfig.auth.authority}/oauth2/v2.0/logout`;
};

Next, under routes > (protected) folder add a +layout.ts file and a “logout” folder with a +server.ts file. After this step, the folder structure should look like this:

Folder structure after step 11
Folder structure after step 11

In the routes > (protected) > logout > +server.ts file, add the following code:

import { redirect } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getLogoutUri } from "$lib/auth/services";

export const GET: RequestHandler = async ({ cookies }) => {
cookies.delete("accessToken");
cookies.delete("idToken");
cookies.delete("account");

throw redirect(302, getLogoutUri());
};

Finally update the routes > (protected) > +layout.ts with the following code:

<nav>
<a href='/logout'>Logout</a>
</nav>

<slot />

<style>
:global(*) {
margin: 0;
padding: 0;
box-sizing: border-box;
}
nav {
display: flex;
justify-content: end;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.25);
}
a {
text-decoration: none;
color: unset;
border: 1px solid #000;
border-radius: 2px;
padding: 10px;
}
a:hover {
background: rgba(0, 0, 0, 0.15);
}
</style>

This should display a Logout button at the top of the page, clicking which should log the user out of the application.

The source code for the app can be found at https://github.com/varu87/sveltekit-aad-auth

--

--

Varun

Full stack develeoper | Stand up comedian | Amateur story teller