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.
- Each customer has fully configurable authentication per their enterprise reqiurements. (i.e. bring-your-own-identity-provider, 2-factor Auth for others, different authentication rules for every customer)
- Roles and Role-based-access-controls configuration done entirely in Keycloak, simplify complexity of managing multitenancy within your app.
- Easy to add new tenants at run-time using a simple externally loaded dependancy. (Yaml file that maps email domains to realms.)
Some added benefits of this library:
- Single implementation in hooks.server.ts handles all routes and API calls
- Low-client trust: HTTP-only strict domain cookies used. No access tokens shared with client. Code exchange for access tokens, token refresh, logout kept in secure network between containers.
yarn install -S sveltkit-keycloak-multitenant
See example: https://github.com/ryvdx/sveltekit-keycloak-multitenant-example (/demoapp)
- Setup .ENV variables
- add userinfo definition in app.d.ts locals
- Add KeyCloakHandle to src/hooks.server.ts file
- Make a basic login form
- Make a Logout route (must have +page.server file to force SSR)
- Integrate your navigation and logout into layout.server.ts
(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:
- input with type=email and name=email
- post action="?/login" (KeyCloakHandle handle will intercept, +page.server.ts not required for LOGIN_PATH)
<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:
- protected route rendering for authenticated users
- rendering of special routes based on custom roles you define in KeyCloak
- (No rendering for unauthenticated/logged-out user. Will just render the content defined by LOGIN_PATH route in the slot.)
<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 {};