This project is deprecated and no longer maintained.
Hello, in this post I would like to show you my implementation of Keycloak JavaScript Adapter in Svelte App.
Feel free to use Table Of Content.
You can find the code on Github.
matejbucek / Secure_Svelte_With_Keycloak
Example of securing Svelte app with Keycloak
Let's start from scratch. First thing we need is the Keycloak server running. You can download the latest version here, or you can use for example docker.
Table Of Contents
- Setting up Keycloak Realm and Client
- Preparing new Sapper project
- Creating the Auth and the User classes
- Creating AuthGuard and RoleGuard components
- Using our classes and components
- Conclusion
1. Setting up Keycloak Realm and Client
Now let's login to our Administration console and create new Realm, Client and some user and role for testing purposes.
After we set up a Realm, we also have to create new Client, so we can then use this authorization server.
Now we will create new default role and new user.
Now we should be good to go to the next step.
2. Preparing new Sapper project
At first we have to create new project. I am going to use Sapper and TypeScript, but you can do the same thing with Svelte and JavaScript.
npx degit "sveltejs/sapper-template#rollup" my-app
Now let's convert our project to TypeScript.
node scripts/setupTypeScript.js
The last thing that is left to do is adding Keycloak JS script tag to our template.html and then we can continue to the fun part.
To make our life easier our Keycloak server provides the right version of JS file for us so our tag should look like this.
<script src="https://{your_server_url}/auth/js/keycloak.js"></script>
So our template.html should look something like this.
...
<link rel="stylesheet" href="global.css">
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
<link rel="icon" type="image/png" href="favicon.png">
<script src="https://{your_server_url}/auth/js/keycloak.js"></script>
<!-- Sapper creates a <script> tag containing `src/client.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
...
3. Creating the Auth and the User classes
The Auth class will handle most of the stuff. The User class is just so we can easily keep track of actual user.
I am using the default flow, which is the Authorization Flow. But you can obviously change that to e. g. Implicit Flow.
You can find more here in the official documentation.
Auth.class.ts - whole code on Github
import { writable } from 'svelte/store';
import { User } from "./User.class";
//Template for loacl storage mapping
export type localStorageMapping = {"access_token": string, "refresh_token": string, "exp": string};
export class Auth {
//The actual keycloak connector
private keycloak: any;
//Used mapping
private localStorageMapping: localStorageMapping;
//This keeps track whether Auth and Role guards can call buildUser method
private initialized: any;
//This class builds the actual User from access token
public buildUser(): User {
let parsed = this.keycloak.tokenParsed;
if(!parsed){
return null;
}
//If you also want the resource roles, just concat them here
return new User(parsed["sub"], parsed["preferred_username"], parsed["given_name"], parsed["family_name"], parsed["realm_access"]["roles"]);
};
public constructor(config: {}, localStorageMapping?: localStorageMapping) {
//Keycloak class is not defined, because we add that library into the template.html
//@ts-ignore
this.keycloak = new Keycloak(config);
this.initialized = writable(false);
if(localStorageMapping){
this.localStorageMapping = localStorageMapping;
}else{
this.localStorageMapping = {
"access_token": "access_token",
"refresh_token": "refresh_token",
"exp": "exp"
};
}
//Check, if user is authenticated
if (localStorage.getItem(this.localStorageMapping.access_token) !== null) {
this.refresh();
}
}
public isInitialized(): any{
return this.initialized;
}
//Makes the initialization process with given parameters
private init(initParams: {}) {
this.keycloak
.init(initParams)
.then((authenticated) => {
if (authenticated) {
localStorage.setItem(
this.localStorageMapping.access_token,
this.keycloak.token
);
localStorage.setItem(
this.localStorageMapping.refresh_token,
this.keycloak.refreshToken
);
localStorage.setItem(this.localStorageMapping.exp, this.keycloak.tokenParsed["exp"]);
//Setting the update (refresh) of our token
this.keycloak.updateToken(5).then((refreshed) => {
if (refreshed) {
localStorage.setItem(
this.localStorageMapping.access_token,
this.keycloak.token
);
localStorage.setItem(
this.localStorageMapping.refresh_token,
this.keycloak.refreshToken
);
localStorage.setItem(this.localStorageMapping.exp, this.keycloak.tokenParsed["exp"]);
}
});
}
this.initialized.set(true);
})
.catch(function (e) {
console.error(e);
});
this.keycloak.onTokenExpired = () => {
//Setting the update (refresh) of our token
this.keycloak.updateToken(5).then((refreshed) => {
if (refreshed) {
localStorage.setItem(
this.localStorageMapping.access_token,
this.keycloak.token
);
localStorage.setItem(
this.localStorageMapping.refresh_token,
this.keycloak.refreshToken
);
localStorage.setItem(this.localStorageMapping.exp, this.keycloak.tokenParsed["exp"]);
}
});
};
}
//This builds initial parameters and add the access token and refresh token.
//You can also use the check-sso. More in official docs.
private buildInitParams(onLoad: string = "login-required", silentCheckSsoRedirectUri?: string): any {
return {
onLoad,
token: localStorage.getItem(this.localStorageMapping.access_token),
refreshToken: localStorage.getItem(this.localStorageMapping.refresh_token),
silentCheckSsoRedirectUri
};
}
public login() {
this.init(this.buildInitParams());
}
public refresh() {
this.init(this.buildInitParams());
}
public logout() {
localStorage.removeItem(this.localStorageMapping.access_token);
localStorage.removeItem(this.localStorageMapping.refresh_token);
localStorage.removeItem(this.localStorageMapping.exp);
this.keycloak.logout();
}
//Checks whether there is the back redirect from auth server
public checkParams(){
let params = (new URL(document.location.href.replace("#", "?"))).searchParams;
if(params.get("state") && params.get("session_state") && params.get("code")){
this.init(this.buildInitParams());
}
}
}
User.class.ts - whole code on Github
export class User{
//The sub parameter of access token
private userId: string;
private username: string;
private firstname: string;
private lastname: string;
private roles: Array<string>;
public constructor(userId: string, username: string, firstname: string, lastname: string, roles: Array<string>){
this.userId = userId;
this.username = username;
this.firstname = firstname;
this.lastname = lastname;
this.roles = roles;
}
//Checks whether user has all of the roles
public hasRole(role: string | Array<string>): boolean{
if(role instanceof Array){
let contains = true;
role.forEach((r) => {
contains = contains && this.roles.includes(r);
});
return contains;
}
return this.roles.includes(role);
}
//getters...
}
4. Creating AuthGuard and RoleGuard components
Now we will take a look at how we would actually check, whether the user is authenticated or not.
Let's define Svelte component for that purpose.
<script>
import { onMount } from 'svelte';
import { goto } from '@sapper/app';
import { authStore } from './stores';
let auth;
let unsub;
let initialized;
$: if(auth) {
auth.initialized.subscribe(i => {
initialized = i;
});
};
$: user = (initialized) ? auth.buildUser() : null;
let forceLogin = false;
let manual = false;
export {
forceLogin,
manual
}
onMount(() => {
unsub = authStore.subscribe(value => {
auth = value;
});
if(forceLogin && user === null){
goto("/login");
}
});
</script>
{#if user && manual}
<slot name="authed"></slot>
{:else if !user && manual}
<slot name="not_authed"></slot>
{:else if user && !manual}
<slot></slot>
{/if}
Don't worry about that stores.js, we will define it later.
So as you can see it is really simple. You can specify whether you want user to be redirected to login page, if user is not logged in. It also allows you to manually specify which of your tags should be displayed when user is authenticated and when he isn't.
Let me show you little example:
<AuthGuard>
<h1>This will be showed to authenticated user.</h1>
</AuthGuard>
<AuthGuard forceLogin=true>
<h1>This will force user to login.</h1>
</AuthGuard>
<AuthGuard manual=true>
<h1 slot="authed">This will be showed to authenticated user.</h1>
<h1 slot="not_authed">This will be showed to not authenticated user.</h1>
</AuthGuard>
Now let's take look on RoleGuard. This component will help you check whether user has right roles.
<script>
import { onMount } from "svelte";
import { authStore } from './stores';
let auth;
let unsub;
let initialized;
$: if(auth) {
auth.initialized.subscribe(i => {
initialized = i;
});
};
$: user = (initialized) ? auth.buildUser() : null;
let roles;
let actualRoles = roles.split(",");
let manual = false;
export {
roles,
manual
}
onMount(() => {
unsub = authStore.subscribe((value) => {
auth = value;
});
});
</script>
{#if user}
{#if user.hasRole(actualRoles) && manual}
<slot name="role"></slot>
{:else if !user.hasRole(actualRoles) && manual}
<slot name="no_role"></slot>
{:else if user.hasRole(actualRoles) && !manual}
<slot></slot>
{/if}
{/if}
Let's take look on another example:
<RoleGuard roles=user>
<h2>You have user role!</h2>
</RoleGuard>
<RoleGuard roles=user,admin>
<h2>You have user and admin roles!</h2>
</RoleGuard>
<RoleGuard manual=true roles=user>
<h2 slot=role>You have user role!</h2>
<h2 slot=no_role>You don't have user role!</h2>
</RoleGuard>
Now we have our two main components. In next section we will take look on stores.js and setting up our Keycloak JS adapter.
5. Using our classes and components
At this point we have all components, Auth class and User class. What is left thou is stores.js and _layout.svelte. So let's take look at them now.
stores.js
import { writable } from 'svelte/store';
export const authStore = writable(null);
This store allow us to share one Auth instance between all of our components.
_layout.svelte
<script lang="ts">
import { onMount } from "svelte";
import { Auth } from "../components/Auth.class";
import { authStore } from "../components/stores";
import Nav from "../components/Nav.svelte";
onMount(() => {
authStore.set(
new Auth({
realm: "{realm_name}",
"auth-server-url": "{your_server_url/auth}",
"ssl-required": "external",
resource: "{resource}",
clientId: "{client_id}",
"public-client": true,
"confidential-port": 0,
})
);
});
export let segment: string;
</script>
<style>
main {
position: relative;
max-width: 56em;
background-color: white;
padding: 2em;
margin: 0 auto;
box-sizing: border-box;
}
</style>
<Nav {segment} />
<main>
<slot />
</main>
Here's what it all connects together. Layout allows us to define one Auth instance and distribute it through our other components. We can now specify our realm and client we'v created at beginning.
If you want to know other attributes you can specify take look at official documentation.
Now let me show you example of a login page.
<script>
import { onMount } from 'svelte';
import { authStore } from '../components/stores';
import AuthGuard from '../components/AuthGuard.svelte';
let unsub;
let auth;
onMount(() => {
unsub = authStore.subscribe(
(a) => {
auth = a;
}
);
});
$: if(auth){
auth.checkParams();
};
function login(){
if(auth){
auth.login();
}
}
function logout(){
if(auth){
auth.logout();
}
}
</script>
{#if auth}
<AuthGuard manual="true">
<button slot="not_authed" on:click={login}>Login</button>
<button slot="authed" on:click={logout}>Logout</button>
</AuthGuard>
{/if}
It's pretty simple, just calling methods we'v defined in Auth class.
After successful login you can try to go to developer tools, take your access token and paste it to JWT.IO, where you can then see your token parsed.
If you want to see whole implementation take look at my Github.
6. Conclusion
In this post we'v taken a look at simple way of securing Svelte/Sapper applications. This is my first post here so I hope it was at least helpful.
Thank you very much for reading. Feel free to ask me any question or give me some suggestion.
Top comments (8)
The latest polka version starting from 1.0.0-next.12 causes typescript build warnings with the sapper template using 0.28.0 through 0.29.1, see github.com/sveltejs/sapper/issues/.... To resolve the build warnings, set the polka version to
"polka": "1.0.0-next.11"
It would be incredible if you could give a tutorial on how to do this with SVELTE-KIT.
I am desperatly searching for something like this.
I made a new branch in the repository called “updated”. There you can find a working example using Svelte-kit.
Thanks a lot! I tried it, however I encounter a "window is not defined" error which I saw a lot lately when trying things out with SvelteKit and Keycloak. I figure this has to do with SSR. If you can give a hint I am very thankful.
Sorry! My Bad, had a typo in my registry... Example is working! Thank you for this.
The code seems to be broken. TypeError: Cannot read properties of undefined (reading 'init'), because keycloak can't be initilized most of time. It works randomly, sometimes, but quite rare.
Great article! And how to use a silent token update in your algorithm?
Hi, thank you very much.
The method
this.keycloak.updateToken(number)
silently updates your tokens, when the refresh token is still non-expired. Otherwise it makes redirect to your Keycloak Authorization Endpoint and user have to log in.In the latest version of my code, you can see that I've set
this.keycloak.onTokenExpired
to an anonymous lambda function, that calls the update method. TheonTokenExpired
method is called by Keycloak itself, so you don't have to worry about updating your token anymore.