Skip to content

Instantly share code, notes, and snippets.

@thomasdarimont
Last active April 8, 2024 14:10
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save thomasdarimont/3e87944c31b6263f1849e35733a03500 to your computer and use it in GitHub Desktop.
Save thomasdarimont/3e87944c31b6263f1849e35733a03500 to your computer and use it in GitHub Desktop.
Mini SPA with Keycloak.js with support for PKCE and RAR

Simple example client with Keycloak.js with PKCE for RAR feature development

Setup

Create a public OpenID Connect client with client id demo-client.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Keycloak SPA Demo</title>
<style>
body {
background-color: #eaeaea;
font-family: sans-serif;
font-size: 10px;
}
button {
font-family: sans-serif;
font-size: 25px;
width: 200px;
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 {
font-size: 20px;
background-color: #eee;
border: 1px solid #ccc;
padding: 10px;
-webkit-box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.5);
-moz-box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.5);
box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.5);
}
.message-content {
font-size: 20px;
padding: 10px;
background-color: #fff;
border: 1px solid #ccc;
}
.token-content {
font-size: 20px;
padding: 5px;
white-space: pre;
text-transform: none;
}
.wrapper {
position: absolute;
left: 10px;
top: 40px;
bottom: 10px;
right: 10px;
}
.error {
color: #a21e22;
}
table {
width: 100%;
}
table.credentials,
table.profile,
table.apps {
width: unset;
}
tr.even {
background-color: #eee;
}
td {
padding: 5px;
}
td.label {
font-weight: bold;
width: 250px;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div>
<h1>ClientId: <span id="clientInfo"></span></h1>
</div>
<div id="welcome" class="wrapper hidden">
<div class="menu">
<button name="loginBtn" onclick="keycloak.login()">Login</button>
</div>
<div class="message-content">
<div class="message">Please login</div>
</div>
</div>
<div id="content" class="wrapper hidden">
<div class="menu">
<button name="rarBtn" onclick="showRar()" class="token">RAR</button>
<button name="tokenBtn" onclick="showToken()" class="token">AccessToken</button>
<button name="idTokenBtn" onclick="showIdToken()" class="idToken">IDToken</button>
<button name="userinfoBtn" onclick="showUserInfo()" class="userinfo">Userinfo</button>
<button name="profileBtn" onclick="showProfile()" class="profile">Profile</button>
<button name="reauthBtn" onclick="enforceCurrentAuth()" class="reauth">ReAuth</button>
<button name="changePasswordBtn" onclick="changePassword()" class="password">Password</button>
<button name="accountBtn" onclick="keycloak.accountManagement()" class="account">Account</button>
<button name="logoutBtn" onclick="keycloak.logout()" class="logout">Logout</button>
</div>
<div id="data" class="content"></div>
</div>
<script defer>
let searchParams = new URLSearchParams(window.location.search);
let keycloakBaseUrl = searchParams.get("base_url") || "http://localhost:8081";
let keycloakUrl = keycloakBaseUrl + (searchParams.get("path") || "/auth");
let realm = searchParams.get("realm") || 'demo';
let clientId = searchParams.get("client_id") || 'demo-client';
let scope = searchParams.get("scope") || 'openid email';
// &show=profile,settings,apps,security,logout
const allContextClasses = ["profile", "token", "idToken", "userinfo", "logout", "apps", "password", "reauth", "account"];
const contextClassesToHideDefault = ["apps", "password", "reauth", "account", "profile"];
const contextClassesToShowDefault = [...allContextClasses].filter((value, index, arr) => {
return !contextClassesToHideDefault.includes(value);
});
let contextClassesToShow = searchParams.get("show")?.split(",") || contextClassesToShowDefault;
for (let className of allContextClasses) {
if (!contextClassesToShow.includes(className)) {
let btn = document.querySelector(`button.${className}`);
if (btn) {
btn.parentElement.removeChild(btn);
}
}
}
document.getElementById("clientInfo").textContent = clientId;
// dynamically add keycloak.js script
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = keycloakUrl + "/js/keycloak.js";
document.getElementsByTagName('head')[0].appendChild(script);
window.onload = () => {
window.keycloak = new Keycloak({
url: keycloakUrl,
realm: realm,
clientId: clientId
});
let origLoginUrl = window.keycloak.createLoginUrl;
window.keycloak.createLoginUrl = function (options) {
let url = origLoginUrl.call(window.keycloak, options);
// add RAR authorization_details if present
if (options && options["authorizationDetails"]) {
url += "&authorization_details=" + options["authorizationDetails"];
}
return url;
};
let initConfig = {
onLoad: 'login-required', // redirects to login if not login
// onLoad: 'check-sso', // shows login button of not logged in
checkLoginIframe: true,
checkLoginIframeInterval: 1,
pkceMethod: 'S256',
scope: scope
};
let onLoginSuccess = () => {
if (keycloak.authenticated) {
showProfile();
} else {
showWelcome();
}
};
keycloak.init(initConfig).success(onLoginSuccess);
keycloak.onAuthLogout = showWelcome;
};
function showRar() {
let data = [
{
"type": "payment_initiation",
"actions": [
"initiate",
"status",
"cancel"
],
"locations": [
"https://example.com/payments"
],
"instructedAmount": {
"currency": "EUR",
"amount": "123.50"
},
"creditorName": "Merchant123",
"creditorAccount": {
"iban": "DE02100100109307118603"
},
"remittanceInformationUnstructured": "Ref Number Merchant"
}
];
let jsonString = JSON.stringify(data, null, ' ').trim();
let rarHtml = `
<button onclick="sendRar()">Authorize</button>
<div id="rarData" contenteditable class="token-content">${jsonString}</div>
`;
show(rarHtml, "token-content");
}
function sendRar() {
let rarDataJson = document.getElementById("rarData").textContent;
let encodedRarData = encodeURIComponent(rarDataJson);
keycloak.login({
authorizationDetails: encodedRarData
});
// kc.loginWithAuthorizationDetails();
}
function showWelcome() {
document.getElementById("welcome").classList.remove("hidden");
document.getElementById("content").classList.add("hidden");
}
function getTimeSinceLastAuth() {
let timeSinceAuthInSeconds = Math.floor((Date.now() - (keycloak.tokenParsed.auth_time * 1000)) / 1000);
return timeSinceAuthInSeconds;
}
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: 12
});
}
function formatDate(timestamp) {
return new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(timestamp))
}
function changePassword() {
keycloak.login({
action: "UPDATE_PASSWORD"
});
}
function showProfile() {
let profileHtml = `
<table class="profile">
<tr>
<td class="label">First name</td>
<td><span id="firstName">${keycloak.tokenParsed['given_name']}</span></td>
<td></td>
</tr>
<tr>
<td class="label">Last name</td>
<td><span id="lastName">${keycloak.tokenParsed['family_name']}</span></td>
<td></td>
</tr>
<tr>
<td class="label">Email</td>
<td><span id="email">${keycloak.tokenParsed['email']}</span></td>
<td></td>
</tr>
</table>
`;
show(profileHtml, "message-content");
}
function showToken() {
let data = JSON.stringify(keycloak.tokenParsed, null, ' ');
show(data, "token-content");
}
function showIdToken() {
let data = JSON.stringify(keycloak.idTokenParsed, null, ' ');
show(data, "token-content");
}
async function showUserInfo() {
await keycloak.updateToken(5);
let userInfoData = await keycloak.loadUserInfo();
let data = JSON.stringify(userInfoData, null, ' ');
show(data, "token-content");
}
function show(data, cssClass) {
let contentElement = document.getElementById('content');
contentElement.classList.remove("hidden")
let dataElement = document.getElementById('data');
dataElement.innerHTML = data;
dataElement.classList.remove(["message-content", "token-content"]);
dataElement.classList.add(cssClass);
}
</script>
</body>
</html>
@walletsecured
Copy link

Linking my account TrustwalltGuide on Twitter with my address 0x40d2c12caacffc547e9cf2c60c02c9ec0b28eae5 on EVM in mycryptoprofile.io, and the challenge code is: 9cc97dfa0b56f9c0fa1a362c24a17b4b. #LitentryVerifyMyAddress

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment