Okay after ramming with this for several days I have to kindly ask the community.
I have a Svelte project, which integrates with Keycloak instance. All my keycloak functions are located in separate keycloakInteraction.ts
from which i reuse them.
My goal is: initialize the keycloak instance (enter login and password if previous session rotten or didn't exist) and then serve particular data based on obtained token.
The problem is that +layout.server.ts
that is serving data is executed prior to +layout.svelte that initializes keycloak, and while there is no token yet +layout.server.ts
serves empty array.
One of the workarounds is just to reload the page, so when the page is reset after user logged in, token is already in memory and data can be served.
However in my implementation the page won't automatically reset if the token has been expired and user has to relogin in the same window. I can't figure out how to workaround it with the variables. I also wonder if there are other graceful ways to login and then serve content based on user_group keycloak token property without reloading the page.
Any ideas will be much appreciated.
my +layout.svelte
:
<script>
import '../app.css';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import {
initKeycloakOnClient,
keycloakClientTokenStoreOnClient,
} from '$lib/keycloakInteraction';
export let data;
$: keycloakState = $keycloakClientTokenStoreOnClient;
$: isAuthenticated = keycloakState.authenticated;
onMount(async () => {
await initKeycloakOnClient();
hasReloaded = sessionStorage.getItem("hasReloadedAfterLogin");
if (isAuthenticated && !hasReloaded) {
console.log("User just logged in, reloading the page...");
sessionStorage.setItem("hasReloadedAfterLogin", "true");
location.reload();
}
});
</script>
{#if isAuthenticated}
<!--MENU HTML CODE IS HERE-->
{#each data.routes as route}
{/each}
<!--...and other data used provided by +layout.server.ts-->
<slot />
{:else}
<div class="h-screen flex justify-center items-center">
<p>Loading...</p>
</div>
{/if}
My +layout.server.ts
:
import type { LayoutServerLoad } from './$types';
import { selectAllClients, selectAllEngagements } from '$lib/server/database/queries';
import {
validateClientTokenOnServer,
resetKeycloakClientTokenStore
} from '$lib/keycloakInteractionResource';
import { keycloak } from '$lib/keycloakInteraction';
const routes = [
{
name: 'Link#1',
},
{
name: 'Link#2',
}
];
export const load: LayoutServerLoad = async ( request ) => {
let userGroup: string = null;
//getting clientToken from server-side cookies that were put by storeKeycloakTokenOnServer()
const clientToken = request.cookies.get('client_token');
if (clientToken) {
await validateClientTokenOnServer(clientToken);
const decodedToken = JSON.parse(atob(clientToken.split(".")[1]));
userGroup = decodedToken?.user_groups || [];
const availableData = await selectAllData(userGroup);
const availableRoutes = userGroup.includes("testGroup") ? routes.filter(route => route.name !== 'Link#2') : routes;
await resetKeycloakClientTokenStore();
return {
data: availableData,
routes: availableRoutes
}
}
}
My keycloakInteraction.ts
:
import Keycloak from 'keycloak-js';
import { writable } from 'svelte/store';
const keycloakUrl = import.meta.env.VITE_AUTH_SERVICE_URL;
const keycloakRealm = import.meta.env.VITE_AUTH_SERVICE_REALM;
const keycloakClientId = import.meta.env.VITE_AUTH_SERVICE_CLIENT_ID;
const keycloak = new Keycloak({
url: keycloakUrl,
realm: keycloakRealm,
clientId: keycloakClientId,
});
export const keycloakClientTokenStoreOnClient = writable({
authenticated: false,
token: null,
userGroups: [],
firstName: null,
lastName: null
});
export const initKeycloakOnClient = async () => {
try {
await keycloak.init({
checkLoginIframe: false,
onLoad: 'login-required',
});
if (keycloak.authenticated) {
await updateKeycloakStoreOnClient(); // performs keycloakClientTokenStoreOnClient.set ...
await storeKeycloakTokenOnServer(keycloak.token);
}
} catch (error) {
console.error('Keycloak initialization failed:', error);
}
Return a flag by +layout.server.js
, check if it's true or false together with isAuthenticated, as it would mean that you are authenticated, but server part has not yet provided you data.
+layout.server.js
:
import type { LayoutServerLoad } from './$types';
import { selectAllClients, selectAllEngagements } from '$lib/server/database/queries';
import {
validateClientTokenOnServer,
resetKeycloakClientTokenStore
} from '$lib/keycloakInteractionResource';
import { keycloak } from '$lib/keycloakInteraction';
const routes = [
{
name: 'Link#1',
},
{
name: 'Link#2',
}
];
export const load: LayoutServerLoad = async ( request ) => {
let userGroup: string = null;
//getting clientToken from server-side cookies that were put by storeKeycloakTokenOnServer()
const clientToken = request.cookies.get('client_token');
if (!clientToken {
return {
tokenObtained: false
}
}
if (clientToken) {
await validateClientTokenOnServer(clientToken);
const decodedToken = JSON.parse(atob(clientToken.split(".")[1]));
userGroup = decodedToken?.user_groups || [];
const availableData = await selectAllData(userGroup);
const availableRoutes = userGroup.includes("testGroup") ? routes.filter(route => route.name !== 'Link#2') : routes;
await resetKeycloakClientTokenStore();
return {
tokenObtained: true,
data: availableData,
routes: availableRoutes
}
}
}
+layout.svelte
:
onMount(async () => {
await initKeycloakOnClient();
if (isAuthenticated && !data.tokenObtained) {
console.log("User just logged in, reloading the page...");
location.reload();
}
});