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
- Automatically assign user roles mapping for:
- Role Mapping: Assign role to individual user or group
- Realm Roles
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