DEV Community

loading...

Secure Svelte with Keycloak

matejbucek profile image Matěj Bucek ・7 min read

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.

GitHub logo 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
  1. Setting up Keycloak Realm and Client
  2. Preparing new Sapper project
  3. Creating the Auth and the User classes
  4. Creating AuthGuard and RoleGuard components
  5. Using our classes and components
  6. 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.

Let's start with Realm.
Creating new Realm

Setting Realm's name

After we set up a Realm, we also have to create new Client, so we can then use this authorization server.
Creating new Client

Setting Client's name and Root URL

Now we will create new default role and new user.
Creating new Role

Making the Role default

Creating 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
Enter fullscreen mode Exit fullscreen mode

Now let's convert our project to TypeScript.

node scripts/setupTypeScript.js
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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%
...
Enter fullscreen mode Exit fullscreen mode

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 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());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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.

Resources

Keycloak Docs

Discussion (1)

pic
Editor guide
Collapse
stilet profile image
stilet

Great article! And how to use a silent token update in your algorithm?