Identity and Access Management Solution

Abdullah Fathi

IdentiTY & aCCESS MANAGEMENT

  • Management of users and their privileges for accessing a system
  • Perform authentication & authorization
  • Many systems depend on an IAM system

Introduction

  • Open Source identity and access management solution
  • Started in 2013, broad adoption since 2015
  • Vital Community with 470+ Contributors 3.3k+ Forks
  • Very robust, good documentation, many examples
  • SSO solution designed for Modern Applications, APIs and Services
  • Web Based GUI

Introduction

  • Open Source software product that allows single sign-on with identity and access management aimed at modern applications and services
  • Support multiple protocols such as OpenID Connect, OAuth 2.0 and SAML 2.0.
  • Provides features such as user federation, social login, multifactor authentication and authorization services

Why Keycloak?

  • Delegate your security
  • User registration and Login
  • Deal with login forms and other related forms
  • Developer can focus on core technology
  • Uses best practices
  • Centralized user management
  • SSO, Social Identity Providers and Federation

Keycloak Architecture

OpenID Connect / OAuth 2.0

  • JSON
  • Simple
  • Bearer Token

 

When?

  • Default
  • SPA, Mobile
  • REST services

SAML v2

  • XML
  • More Mature
  • More Complex

 

When?

  • Monoliths
  • Apps with SAML Support
  • If you have some fancy requirements

User Storage SPI

KEYCLOAK TERMS

1) Realms

  • Manage entities such as users, roles, clients, etc.
  • Each realms is fully isolated with other realms

2) Clients

  • Entities that can request authentication of a user
  • Type of Clients:
    • Type 1: An application that wants to participate in single-sign-on
    • Type 2: An application that requests an access token so that it can invoke other services on behalf of the authenticated user
  • Client Communication Protocols:
    • OpenID Connect (OIDC): preferred way since it was designed to be web friendly and work well with HTML5 & Javascript applications.
    • SAML: Mature protocol but it tends to be bit more verbose than OIDC

3) client scopes

  • Allows creating a re-usable groups of claims that are added to token issued to a client

4) Roles

  • Identify a type or category of a user
  • One user can have multiple roles
  • Type of Roles
    • Realm Roles
      • Realm level roles
      • Can be defined as a global namespace to define roles
    • Client Roles
      • Client level roles
      • This is a namespace dedicated to a client
    • Default Role
      • Automatically assign user roles mapping for:
        • Newly created users
        • Imported users through identity brokering
    • Role Mapping: Assign role to individual user or group

Keycloak Deployment

Sizing CPU and memory resources

services:
  keycloak:
    #build: .
    image: quay.io/keycloak/keycloak:25.0.4
    container_name: keycloak-v2504
    command: start
    volumes:
      - ./custom-themes/keywind.jar:/opt/keycloak/providers/keywind.jar
      - ./custom-themes/keycloak-spi-browser-session-api-1.0.jar:/opt/keycloak/providers/keycloak-spi-browser-session-api-1.0.jar
    environment:
      #Database Configuration
      KC_DB: postgres
      KC_DB_URL: 'jdbc:postgresql://insert-ip-here:5432/keycloakdb'
      KC_DB_USERNAME: kc_dba
      KC_DB_PASSWORD: 'dbpasshere'
      #Hostname Configuration
      KC_HOSTNAME: 'kc-training.fotia.com.my'
      KC_HOSTNAME_URL: 'https://kc-training.fotia.com.my'
      KC_PROXY: edge
      KC_HOSTNAME_ADMIN_URL: 'https://kc-training.fotia.com.my'
      #Admin Credentials
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: 'admin'
    ports:
      #- 8443:8443
      - 8480:8080

RUn USING docker compose

FEATURES

1) SSO

  • Single Sign On
  • Single Sign Out
  • Maintain a single session across multiple applications

2) Identity Brokering & Social Login

  • Enable login with Social Network
  • Authenticate user with existing OpenID Connect or SAML 2.0 Identity Providers

3) User Federation

  • Built-in support to sync users from existing LDAP or Active Directory servers
  • Can also implement your own provider if you have users in other stores such as RDBMS

4) Client Adapters

  • Adapters available for a number of platforms and programming languages
  • Can also use any OpenID Connect Resource Library or SAML 2.0 Service Provider library

5) Admin Console

  • Centrally manage all aspects of the keycloak server
  • Enable/Disable various features
  • Configure identity brokering and user federation
  • Create and manage application services and define fine-grained authorization policies
  • Manage users including permissions and sessions

6) Account Management Console

  • User can manage their own accounts:
    • Update Profile
    • Change password
    • Setup 2FA
  • Manage session
  • View history account
  • Link account with additional provider

7) Standard Protocols

  • Keycloak is based on standard protocols and provide support for:
    • OpenID Connect
    • OAuth 2.0
    • SAML

8) Authorization Services

  • Manage permissions for all your services from Keycloak Admin Console
  • Gives you the power to define exactly  the policies you need

9) Themes Customization

  • Cutomize all pages
  • Pages is using .ftl formats
    • HTML
    • CSS
    • JS
  • Limitless customization
  • Organization Branding

11) Clustering

  • Run Keycloak in the cluster for Scalability and HA

10) Password Policies

  • Lots of policies supported for a password. ex:
    • Digits
    • Special Character
    • Expire Password
    • Not username
    • Minimum Length
    • etc..

OAuth-Open authorization

Open protocol for authorization that allows user to share private resources to 3rd-party

What is oauth?

  • Open standard for authorization that apps can use to provide client applications with "secure delegated access"
  • OAuth works over HTTPS and authorizes devices, APIs, servers and applications with access tokens rather than credentials
  • 2 version:

why oauth?

  • Was created as a response to the direct authentication pattern.
  • Instead of sending username and password to the server with each request, the user sends an API key ID and secret.

Oauth actors

  • Resource Owner: Owns the data in the resource server
  • Resource Server: The API which stores the data the application wants to access
  • Authorization Server: The main engine of OAuth
  • Client: The application that want to access the data
exp = expiry time (number of second from epoch time 1 jan 1970)
iat = issued time of the token
Differences of iat n exp is lifespan of access token
auth_time = time of authorization occur
jti = jwt unique identifier given to this token
iss = the issuer of this token which is keycloak
aud = who is this token for? by default keycloak set it as account
sub = subject of JWT, the user who is actually logged in, its a unique ID assigned/given by keycloak
typ = type of the token, which is Bearer token
azp = authorized party, application/client who was authorized to use the access token
nonce = must match what is sent for validation. * check from previous parameter
ssession_state = sso session ID between the user and the keycloak authorization server
scope = its what we requested, and thats exaclt what we got. It will be mapping directly to the authorities
sid = sso session id
> and a bunch of other claims i.e: email_verified, name, preferred_username, given_name, etc..
> these was show up because we have email and profile in the scope, and thats the information we get in the access token.

JWT Access Token

Keycloak Tokens

  • Access-Token short-lived (Minutes+): Used for accessing resources
  • Refresh-Token longer-lived (Hours+): Used for requesting new Tokens
  • IDToken: Contains User Information (OIDC)
  • Offline-Token long-lived(Days++)

oauth grant types

  • Authorization Code Grant Flow
  • Authorization Code with PKCE
  • Implicit Grant Flow (Deprecated)
  • Client Credentials Grant Flow
  • Password Grant Flow (Deprecated)

OAuth flow

How Keycloak SSO Works?

Web SSO with OIDC: Unauthenticated User

Web SSO with OIDC: Authenticated User

Keycloak Tokens

  • Access-Token short-lived (Minutes+): Used for accessing resources
  • Refresh-Token longer-lived (Hours+): Used for requesting new Tokens
  • IDToken: Contains User Information (OIDC)
  • Offline-Token long-lived(Days++)

Calling Backend Services with Access-Token

Sample Plain js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">

    <title>Keycloak JS Demo</title>

    <style>
        body {
            background-color: #eaeaea;
            font-family: sans-serif;
            font-size: 30px;
        }

        button {
            font-family: sans-serif;
            font-size: 30px;
            width: 250px;

            background-color: #0085cf;
            background-image: linear-gradient(to bottom, #00a8e1 0%, #0085cf 100%);
            background-repeat: repeat-x;

            border: 2px solid #ccc;
            color: #fff;

            text-transform: uppercase;

            -webkit-box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
            -moz-box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
            box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
        }

        button:hover {
            background-color: #006ba6;
            background-image: none;
            -webkit-box-shadow: none;
            -moz-box-shadow: none;
            box-shadow: none;
        }

        hr {
            border: none;
            background-color: #eee;
            height: 10px;
        }

        .menu {
            padding: 10px;
            margin-bottom: 10px;
        }

        .content {
            background-color: #eee;
            border: 1px solid #ccc;
            padding: 10px;

            -webkit-box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
            -moz-box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
            box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
        }

        .content .message {
            padding: 10px;
            background-color: #fff;
            border: 1px solid #ccc;
            font-size: 40px;
        }

        #token-content {
            font-size: 20px;
            padding: 5px;
            white-space: pre;
            text-transform: none;
        }

        .wrapper {
            position: absolute;
            left: 10px;
            top: 10px;
            bottom: 10px;
            right: 10px;
        }

        .error {
            color: #a21e22;
        }

        table {
            width: 100%;
        }

        tr.even {
            background-color: #eee;
        }

        td {
            padding: 5px;
        }

        td.label {
            font-weight: bold;
            width: 250px;
        }

    </style>
</head>

<body style="display: none;">

<div class="wrapper" id="welcome" style="display: none;">
    <div class="menu">
        <button name="loginBtn" onclick="keycloak.login()">Login</button>
    </div>

    <div class="content">
        <div class="message">Please login</div>
    </div>
</div>

<div class="wrapper" id="profile" style="display: none;">
    <div class="menu">
        <button name="profileBtn" onclick="showProfile()">Profile</button>
        <button name="tokenBtn" onclick="showToken()">AccessToken</button>
        <button name="idTokenBtn" onclick="showIdToken()">IDToken</button>
        <button name="accountBtn" onclick="keycloak.accountManagement()">Account</button>
        <button name="changePasswordBtn" onclick="changePassword()">Password</button>
        <button name="reauthBtn" onclick="enforceCurrentAuth()">ReAuth</button>
        <button name="logoutBtn" onclick="keycloak.logout()">Logout</button>
    </div>

    <div class="content">
        <div id="profile-content" class="message">
            <table cellpadding="0" cellspacing="0">
                <tr>
                    <td class="label">First name</td>
                    <td><span id="firstName"></span></td>
                </tr>
                <tr class="even">
                    <td class="label">Last name</td>
                    <td><span id="lastName"></span></td>
                </tr>
                <tr class="even">
                    <td class="label">Email</td>
                    <td><span id="email"></span></td>
                </tr>
            </table>
        </div>
    </div>
</div>

<div class="wrapper" id="token" style="display: none;">
    <div class="menu">
        <button onclick="showProfile()">Profile</button>
        <button onclick="showToken()">AccessToken</button>
        <button onclick="showIdToken()">IDToken</button>
        <button name="accountBtn" onclick="keycloak.accountManagement()">Account</button>
        <button name="changePasswordBtn" onclick="changePassword()">Password</button>
        <button name="reauthBtn" onclick="enforceCurrentAuth()">ReAuth</button>
        <button onclick="keycloak.logout()">Logout</button>
    </div>

    <div class="content">
        <div id="token-content" class="message"></div>
    </div>
</div>

<div class="wrapper" id="idToken" style="display: none;">
    <div class="menu">
        <button onclick="showProfile()">Profile</button>
        <button onclick="showToken()">AccessToken</button>
        <button onclick="showIdToken()">IDToken</button>
        <button name="accountBtn" onclick="keycloak.accountManagement()">Account</button>
        <button name="changePasswordBtn" onclick="changePassword()">Password</button>
        <button name="reauthBtn" onclick="enforceCurrentAuth()">ReAuth</button>
        <button onclick="keycloak.logout()">Logout</button>
    </div>

    <div class="content">
        <div id="token-content" class="message"></div>
    </div>
</div>


<script>
    let keycloakUrl = "https://kc-v2.fotia.com.my"

    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = keycloakUrl + "/js/keycloak.js";

    document.getElementsByTagName('head')[0].appendChild(script);

    window.onload = function () {

        window.keycloak = new Keycloak({
            url: keycloakUrl,
            // realm: 'demo',
            // clientId: 'keycloak-js-demo'

            realm: 'training',
            clientId: 'app2-html'
        });

// Workaround for KEYCLOAK-14414
        window.keycloak._login = window.keycloak.login;
        window.keycloak.login = function (options) {
            if (options) {
                //options.scope="openid email name";
                options.scope = "openid";
            }
            return window.keycloak._login.apply(window.keycloak, [options]);
        };


        keycloak.init({
            onLoad: 'login-required',
            checkLoginIframe: true,
            checkLoginIframeInterval: 1,
            pkceMethod: 'S256'
        }).then(function () {

                if (keycloak.authenticated) {
                    showProfile();
                } else {
                    welcome();
                }

                document.body.style.display = 'block';
            }).catch(function (error) {
        console.error("Failed to initialize Keycloak", error);
        welcome();
    });

        keycloak.onAuthLogout = welcome;
    };

    function welcome() {
        show('welcome');
    }

    function getTimeSinceLastAuth() {
        let timeSinceAuthSeconds = Math.floor((Date.now() - (keycloak.tokenParsed.auth_time * 1000)) / 1000);
        return timeSinceAuthSeconds;
    }

    function enforceCurrentAuth() {

        let timeSinceAuthSeconds = getTimeSinceLastAuth();
        console.log("time since auth: " + timeSinceAuthSeconds);

        if (timeSinceAuthSeconds < 10) {
            console.log("auth is still file")
            return;
        } else {
            console.log("trigger reauth")
        }

        keycloak.login({
            loginHint: keycloak.tokenParsed.preferred_username,
            maxAge: 20
        });
    }

    function changePassword() {
        keycloak.login({
            action: "UPDATE_PASSWORD"
        });
    }

    function showProfile() {

        if (keycloak.tokenParsed['given_name']) {
            document.getElementById('firstName').innerHTML = keycloak.tokenParsed['given_name'];
        }
        if (keycloak.tokenParsed['family_name']) {
            document.getElementById('lastName').innerHTML = keycloak.tokenParsed['family_name'];
        }
        if (keycloak.tokenParsed['email']) {
            document.getElementById('email').innerHTML = keycloak.tokenParsed['email'];
        }

        show('profile');
    }

    function showToken() {
        document.getElementById('token-content').innerHTML = JSON.stringify(keycloak.tokenParsed, null, '    ');
        show('token');
    }

    function showIdToken() {
        document.getElementById('token-content').innerHTML = JSON.stringify(keycloak.idTokenParsed, null, '    ');
        show('token');
    }

    function show(id) {
        document.getElementById('welcome').style.display = 'none';
        document.getElementById('profile').style.display = 'none';
        document.getElementById('token').style.display = 'none';
        document.getElementById('idToken').style.display = 'none';
        document.getElementById(id).style.display = 'block';
    }


</script>
</body>

</html>

Sample Plain js (cont...)

Keycloak admin

REST API

Keycloak documentation

Keycloak extensions

Keycloak CUSTOM TEMPLATE

Customize Freemarker Template Language (FTL) with Keywind

What is FTL?

  • Java-based template engine that used by keycloak to generate dynamic HTML
  • Maintained by Apache Foundation
<html>
<head>
  <title>${title}
</head>
<body>
  <h1>${title}</h1>

  <p>${exampleObject.name} by ${exampleObject.project}</p>

  <ul>
    <#list systems as system>
      <li>${system_index + 1}. ${system.name} from ${system.project}</li>
    </#list>
  </ul>

</body>
</html>
package com.keycloak.freemarker.first;

public class ValueExampleObject {

    private String name;
    private String project;

    public ValueExampleObject(String name, String project) {
        this.name = name;
        this.project = project;
    }

    public String getName() {
        return name;
    }

    public String getProject() {
        return project;
    }

}

example

helloworld.ftl

veo.java

package com.keycloak.freemarker.first;

import java.io.File;
import java.io.FileWriter;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.Version;

public class MainTest {

    public static void main(String[] args) throws Exception {

        // 1. Configure FreeMarker
        //
        // You should do this ONLY ONCE, when your application starts,
        // then reuse the same Configuration object elsewhere.
        
        Configuration cfg = new Configuration();
        
        // Where do we load the templates from:
        cfg.setClassForTemplateLoading(MainTest.class, "templates");
        
        // Some other recommended settings:
        cfg.setIncompatibleImprovements(new Version(2, 3, 20));
        cfg.setDefaultEncoding("UTF-8");
        cfg.setLocale(Locale.US);
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);

        // 2. Proccess template(s)
        //
        // You will do this for several times in typical applications.
        
        // 2.1. Prepare the template input:
        
        Map<String, Object> input = new HashMap<String, Object>();
        
        input.put("title", "FTL example");

        input.put("exampleObject", new ValueExampleObject("Fotia", "DevOps"));
        
        List<ValueExampleObject> systems = new ArrayList<ValueExampleObject>();
        systems.add(new ValueExampleObject("JDN", "SpotMe"));
        systems.add(new ValueExampleObject("JPN", "MyIdentity"));
        systems.add(new ValueExampleObject("JDN", "MyGPKI"));
        systems.add(new ValueExampleObject("MDI", "APIX INSIST"));
        input.put("systems", systems);
        
        // 2.2. Get the template

        Template template = cfg.getTemplate("helloworld.ftl");
            
        // 2.3. Generate the output

        // Write output to the console
        Writer consoleWriter = new OutputStreamWriter(System.out);
        template.process(input, consoleWriter);

        // For the sake of example, also write output into a file:
        Writer fileWriter = new FileWriter(new File("output.html"));
        try {
            template.process(input, fileWriter);
        } finally {
            fileWriter.close();
        }

    }
}

MainTest.java

<#macro page>
  <html>
  <head>
    <title>${title}
  </head>
  <body>
    <h1>${title}</h1>
  
    <#-- This processes the enclosed content:  -->
    <#nested>
  </body>
  </html>
</#macro>

<#macro otherExample p1 p2>
  <p>The parameters were: ${p1}, ${p2}</p>
</#macro>

Reuse common template fragments

utils.ftl

<#import "lib/utils.ftl" as u> 

<@u.page>
  <p>${exampleObject.name} by ${exampleObject.developer}</p>

  <ul>
    <#list systems as system>
      <li>${system_index + 1}. ${system.name} from ${system.developer}</li>
    </#list>
  </ul>
  
  <#-- Just another example of using a macro: -->
  <@u.otherExample p1=11 p2=22 />
</@u.page>

 helloworld.ftl 

<#assign var_link = "https://keycloak.org">

<a href="${var_link}">About Keycloak</a>

Variables

<#assign name = "${article.getName()}">


<#if name?starts_with("https")>
    <a href="https://keycloak.org</a>
    <#else>
    <a href="https://www.keycloak.org/${name}/25.0.4/rest-api/index.html">${title}</a>
</#if>

if then else

  • CSS framework which uses utility classes to style HTML code on the fly
  • Fast and more flexible compared to other framework such as Bootstrap and Material
  • JS Library to implement interactivity to the page without the need to write any custom Javascript

custom theme (keywind)

A template for Keycloak built by Tailwind and AlpineJS

THANK YOU

Keycloak

By Abdullah Fathi

Keycloak

  • 241