Keycloak으로 Svelte 보안

안녕하세요, 이 게시물에서는 Svelte 앱에서 구현한 Keycloak JavaScript Adapter를 보여드리고자 합니다.

목차를 자유롭게 사용하십시오.

Github에서 코드를 찾을 수 있습니다.


마테부첵 / Secure_Svelte_With_Keycloak


Keycloak으로 Svelte 앱을 보호하는 예




처음부터 시작합시다. 가장 먼저 필요한 것은 Keycloak 서버가 실행되는 것입니다. 최신 버전here을 다운로드하거나 예를 들어 docker를 사용할 수 있습니다.


목차
  • 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. Keycloak 영역 및 클라이언트 설정

    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.




    Realm을 설정한 후에는 새 클라이언트도 생성해야 이 인증 서버를 사용할 수 있습니다.




    이제 새 기본 역할과 새 사용자를 생성합니다.






    이제 우리는 다음 단계로 넘어가기에 좋습니다.

    2. 새로운 Sapper 프로젝트 준비

    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. 인증 및 사용자 클래스 만들기

    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 - 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 - 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. AuthGuard 및 RoleGuard 구성 요소 생성

    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. 클래스와 컴포넌트 사용하기

    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 그러면 토큰이 구문 분석된 것을 볼 수 있습니다.

    전체 구현을 보려면take look at my Github .

    6. 결론

    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.

    자원



    * Keycloak Docs

    좋은 웹페이지 즐겨찾기