In the previous version of Tabunganku, we had to manually put user data into the database to use Tabunganku. The process was complicated, and the application can only be used by one person. In the new version, Keycloak handles user management in the new version. This is how I implement Keycloak for a Svelte application.

Login Function

Keycloak documentation provides information how to do client-side authentication using its Javascript adapter. According to the documentation, this is how to use Keycloak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import Keycloak from 'keycloak-js';

const keycloak = new Keycloak({
    url: 'http://keycloak-server${kc_base_path}',
    realm: 'myrealm',
    clientId: 'myapp'
});

try {
    const authenticated = await keycloak.init();
    console.log(`User is ${authenticated ? 'authenticated' : 'not authenticated'}`);
} catch (error) {
    console.error('Failed to initialize adapter:', error);
}

It imports Keycloak class from keycloak-js library, instantiate a Keycloak object, and call its init function. The keycloak variable declared here will try to connect to a realm named myrealm using a client myapp. What is a realm and a client? In Keycloak, a realm functions as a space where you can store users, roles, and groups, while a client serves as an identifier for an entity connecting to the specified realm. An entity can be anything, from a simple REST API to a mobile application.

To secure the application and allow access only to authenticated users, I use Svelte’s layout file to instantiate a Keycloak object. A layout file allows child pages to reuse resources that have been defined in the layout.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export const load: LayoutLoad = async ({data}) => {
  let instance = {
    url: `${PUBLIC_KEYCLOAK_PROTOCOL}://${PUBLIC_KEYCLOAK_URL}:${PUBLIC_KEYCLOAK_PORT}/`,
    realm: 'Tabungan',
    clientId: 'tabungan-app'
  };

  let keycloak = new Keycloak(instance);
  let kcInitOpts: KeycloakInitOptions = { 
    onLoad: "login-required", 
    checkLoginIframe: false,
  };
  
  let keycloakPromise;
  if (browser) {
    keycloakPromise = keycloak.init(kcInitOpts).then((auth) => {
      if (auth) {
        document.cookie= "kc-cookie=" + keycloak.token + "; path=/; SameSite=strict";
        return keycloak;
      }
    });
  }

  return {
    keycloak: keycloakPromise,
  };
};

In the snippet above, load function instantiates a new Keycloak object with the parameters defined by instance and kcInitOpts variables, returning keycloakPromise—a Promise object from the init function that can be used in +layout.svelte. If Keycloak is successfully initialized, it will store a token in cookie named kc-cookie.

1
2
3
4
5
6
7
8
9
import { onMount } from 'svelte';
export let data;
export let {keycloak} = data;

onMount(() => {
  if (!keycloak) {
    window.location.reload();
  }
});

In +layout.svelte, it imports the keycloak variable defined in +layout.ts file and checks if Keycloak has been started. The page will reload if not, and after the reload, a login page will appear.

Logout Function

Implementing logout function is similar with login. I have to use the same keycloak object defined in the +layout.ts file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import type { PageLoad } from './$types';
import { PUBLIC_TABUNGANKU_URL } from '$env/static/public';

export const load: PageLoad = async ({parent, data}) => {
  let parentData = await parent();
  let {keycloak} = parentData;
  return {
    keycloak: keycloak,
    home: PUBLIC_TABUNGANKU_URL,
  }
};

When the logout page is loaded, it will call await parent(), which will return data defined on the parent page(layout page). After retreiving the keycloak object, I pass it to the +logout.svelte page and call the logout function from there.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { onMount } from 'svelte';
import type {KeycloakLogoutOptions} from 'keycloak-js';

export let data;
onMount(
  () => setTimeout(() => {
    let logoutOptions: KeycloakLogoutOptions = {
      redirectUri: data.home,
    };
    if (data.keycloak) {
      data.keycloak.logout(logoutOptions);
    }
  }, 3000)
);

This will log the user out of Tabunganku after displaying the logout notification for 3 seconds.

Passing Keycloak Token

To read user data in the backend, we need to include Keycloak token in the header request.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export const POST: RequestHandler = async ({ url, request, fetch }) => {
  const form = await request.formData();

  const cookieValue = cookie.parse(request.headers.get('Cookie'));
  const kcCookie = cookieValue['kc-cookie'];

  const toSend = {
    "category_name": form.get('name'),
    "transaction_type": form.get('transaction_type'),
  };

  const requestHeaders: HeadersInit = new Headers();
  requestHeaders.set('Authorization', `Bearer ${kcCookie}`);

  const res = await fetch(`${TABUNGANKU_BE_URL}/categories/add`, {
    method: 'POST',
    body: JSON.stringify(toSend),
    headers: requestHeaders,
  });
  ...
};

In this snippet, the system extracts the token value from the kc-cookie cookie and puts it in the Authorization header.

  • Tabunganku-fe Github Repository: link
  • Keycloak documentation: link