Multi-tenant (fully federated) authentication and authorization library for KeyCloak in SvelteKit apps. Enables role-based-access-controls configuration in Keycloak, and SvelteKit app role metadata access in SSR. Uses a Hybrid Authentication flow and JWT tokens.
Needed a solution for multitenant authentication in a SvelteKit app intended to run in a containerized environment. Intended for apps that need fully federated authentication (i.e. enterprise B2B apps serving customers with varying authentication requirements.) Most auth solutions are single-tenant, including most keycloak libraries for sveltekit.
This library enables apps to use multiple keycloak realms mapping customer user email domains to realms.
Some added benefits of this library:
yarn install -S sveltkit-keycloak-multitenant
See example: https://github.com/ryvdx/sveltekit-keycloak-multitenant-example (/demoapp)
(Remainder can be setup outside of your app. See docker-compose in root and Readme in example link above.)
Variable | Purpose | Example (Default) |
---|---|---|
KEYCLOAK_URL | URL of your Keycloak server taking OIDC authentication calls. | https://auth.myapp |
KEYCLOAK_INTERNAL_URL | Intenal URL of your Keycloak server within containerized network. | http://keycloak:8085 |
LOGIN_PATH | Relative path to user email form pre-login. | /auth/login |
LOGOUT_PATH | path where you want to redirect to post logout. (Route must have a server side +page.server.ts/js file) | /auth/logout |
POST_LOGIN_PATH | (Optional) post authentication redirect if initial landing was not a deep link. (default / if not set) | /homepage |
User info and user roles is stored in locals by hooks.server.ts.
// See https://kit.svelte.dev/docs/types#app
/// <reference types="@auth/sveltekit" />
import type { UserInfo } from '$lib/server/keycloakservice';
declare global {
namespace App {
interface Locals {
user: UserInfo | null,
}
}
}
export {};
import type { Handle } from "@sveltejs/kit";
import { KeyCloakHandle } from "sveltekit-keyloak-multitenant";
import { env } from "$env/dynamic/private";
export const handle: Handle = KeyCloakHandle({
keycloakUrl: env.KEYCLOAK_URL,
keycloakInternalUrl: env.KEYCLOAK_INTERNAL_URL,
loginPath: env.LOGIN_PATH,
logoutPath: env.LOGOUT_PATH,
postLoginPath: env.POST_LOGIN_PATH,
});
Alternatively if you have other hooks middleware functions:
import { sequence } from '@sveltejs/kit/hooks';
import type { Handle } from "@sveltejs/kit";
import { KeyCloakHandle } from "sveltekit-keyloak-multitenant";
import { env } from "$env/dynamic/private";
export const handle: Handle = sequence(
KeyCloakHandle({
keycloakUrl: env.KEYCLOAK_URL,
keycloakInternalUrl: env.KEYCLOAK_INTERNAL_URL,
loginPath: env.LOGIN_PATH,
logoutPath: env.LOGOUT_PATH,
postLoginPath: env.POST_LOGIN_PATH,
}),
);
Define a route that matches the ENV variable LOGIN_PATH that includes the basic email submission form. KeyCloakHandle will map the user to a tenant using email (domain), and redirect the user to the appropriate realm for authenication. Will also pass the email to the authentication for convenience so user does not have to specify that twice.
Form requirements:
<form method="post" action="?/login">
<label for="email">email</label>
<input type="email" name="email" />
<button>Sign In</button>
</form>
Implement a route LOGOUT_PATH is going to be set to. This route must have +page.svelte AND +page.server.ts (to force server-side after logout) No logic required in +page.server file, and customize +page.svelte for whatever logout message your want.
Pass through locals variables out as page data in +layout.server.ts. (Note, for pages that hide/show features within the page by role, you can pass the UserInfo object to the page renderer in that pages load method as well.)
import type { LayoutServerLoad } from "./$types"
export const load: LayoutServerLoad = async (event) => {
return {
user: event.locals.user,
}
}
Example +layout.svelte file. (You can do anything with the UserInfo metadata returned in the laste step.) This shows:
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<nav>
{#if data.user}
<span>Welcome: {data.user.username}</span>
<span>tenant: {data.user.tenant}</span>
<a href="/">Home</a>
<a href="/protectedRoute1">Some Protected Route</a>
<a href="/protectedRoute2">Another Protected Route</a>
{#if data.user.roles && data.user.roles.includes('admin')}
<a href="/admin">Admin</a>
{/if}
<a href="/auth/logout" data-sveltekit-preload-data="off">Logout</a>
{/if}
</nav>
<slot />
<style>
... your style here ...
</style>
data-sveltekit-preload-data="off" required on logout link along with the +page.server file for the LOGOUT_PATH. This ensures a server side response to the logout. This will end the user session in Keycloak, clear the locals, which then lets your Layout files update using an unauthenticated sate. On logout, when page returns to client, it will expire the refresh cookie and any other cookies.
System runs using a YAML configuration file declaring tenants. Users mapped to realms using email domains. If email domain / tenant mapping does not exist, it will re-read the yaml file and try again. (Enables quick edit of the file and injected/updated on the fly.) Note: do not use "master" realm for customer apps per Keycloak best practices. Create per-customer new realms. (For setup of KeyCloak, see https://github.com/ryvdx/sveltekit-keycloak-multitenant-example)
mustangs:
client_id: 'webapp'
client_secret: 'yourclientsecrethere'
realm: 'mustangs'
email_domain: 'mustangs.com'
camaros:
client_id: 'webapp'
client_secret: 'yourclientsecrethere'
realm: 'camaros'
email_domain: 'camaros.io'
Optional: Extend this with any additional metadata to customize your per-tenant experience:
mustangs:
client_id: 'webapp'
client_secret: 'yourclientsecrethere'
realm: 'mustangs'
email_domain: 'mustangs.com'
customer_logo: '',
default_locale: 'en-US'
Note: If you want type support for those attributes in +layout.svelte/+page.svelte, you can extend the definition in app.d.ts:
import type { UserInfo } from '$lib/server/keycloakservice';
interface MyAppUserInfo extends UserInfo {
customer_logo:string;
default_locale:string;
}
declare global {
namespace App {
interface Locals {
user: MyAppUserInfo | null,
}
}
}
export {};