В веб-разработке безопасность и управление идентификацией имеют решающее значение. Keycloak часто занимает центральное место в этом контексте. Хотя существует множество документации, ничто не сравнится с практическим опытом. В этой статье рассказывается о моем исследовании Keycloak с помощью Vanilla JavaScript.
Примечание. Это экспериментальное исследование, а не готовое к использованию руководство.
Настройка HTML
Начните с простой страницы HTML5 и элемента div с идентификатором «root» для отображения информации о аутентифицированном пользователе.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VANILLA JS AUTH</title>
</head>
<body>
<div id="root" />
<script src="./main.js"></script>
</body>
</html>
Инициализация
Создайте файл main.js и инициализируйте такие переменные, как baseUrl, область и client_id.
const baseUrl = "KEYCLOAK_URL";
const realm = "KEYCLOAK_REALM";
const client_id = "KEYCLOAK_CLIENT_ID";
const redirect_uri = `${location.protocol}//${location.host}/`;
Генерация случайного состояния и одноразового номера
Генерация случайного состояния и одноразового номера необходима для безопасности OAuth 2.0 и OpenID Connect. Мы используемgenerRandomState() для создания этих значений.
const generateRandomState=()=> {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
Перенаправление на Keycloak
Неаутентифицированные пользователи перенаправляются на Keycloak с помощью redirectToKeycloak().
const redirectToKeycloak = () => {
const state = generateRandomState();
const nonce = generateRandomState();
const urlParams = {
client_id,
redirect_uri,
response_mode: "fragment",
response_type: "code",
scope: "openid",
nonce,
state,
};
const conectionURI = new URL(`${baseUrl}/realms/${realm}/protocol/openid-connect/auth`);
for(const key of Object.keys(urlParams)){
conectionURI.searchParams.append(key, urlParams[key]);
}
window.location.href = conectionURI;
}
В этой функции мы создаем новый URL-адрес, используя baseurl и область. Затем мы добавляем различные параметры, такие как client_id, redirect_uri, nonce и состояние. Для параметра response_type установлено значение «code», что сигнализирует Keycloak о том, что мы запрашиваем код авторизации. Этот код будет использоваться позже для получения токена. Наконец, мы перенаправляем пользователя на этот URL-адрес Keycloak.
Получение токена
После успешной аутентификации Keycloak перенаправит вас обратно с помощью кода. Используйте getToken(code) для получения токена.
const getToken = async (code) => {
const tokenUri = new URL(`${baseUrl}/realms/${realm}/protocol/openid-connect/token`);
var body = new URLSearchParams();
body.append("client_id", client_id);
body.append("redirect_uri", redirect_uri);
body.append("grant_type", "authorization_code");
body.append("code", code);
const data = await fetch(tokenUri, {
method: 'post',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body,
});
const jsonData = await data.json();
sessionStorage.setItem("token", jsonData.access_token);
sessionStorage.setItem("id_token", jsonData.id_token);
sessionStorage.setItem("refresh_token", jsonData.refresh_token);
sessionStorage.setItem("session_state", jsonData.session_state);
history.replaceState({}, '', '/');
location.reload();
}
Отображение данных пользователя
Извлеките токен из sessionStorage и отобразите сведения о пользователе.
const decodeJWT = (token) => {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Invalid JWT');
const decoded = atob(parts[1]);
const payload = JSON.parse(decoded);
return payload;
}
const generateUserProfile = () => {
const JWT = sessionStorage.getItem("token");
const payload = decodeJWT(JWT);
console.log(payload);
const div = document.createElement("div");
const userName = document.createElement("label");
userName.innerText = `--------${payload.given_name} ${payload.family_name} (${payload.email})--------`;
const logoutButton = document.createElement("button");
logoutButton.innerText = "Logout";
logoutButton.addEventListener("click", ()=>{
logout();
});
div.appendChild(userName);
div.appendChild(logoutButton);
return div;
}
Выход
Разрешите выход пользователя из системы, отправив сохраненный id_token.
const logout = () => {
const logoutURI = new URL(`${baseUrl}/realms/${realm}/protocol/openid-connect/logout`);
const id_token_hint = sessionStorage.getItem("id_token");
const urlParams = {
client_id,
post_logout_redirect_uri: redirect_uri,
id_token_hint,
};
for(const key of Object.keys(urlParams)){
logoutURI.searchParams.append(key, urlParams[key]);
}
sessionStorage.clear();
window.location.href = logoutURI;
}
Обработка событий
Наконец, мы обрабатываем загрузку окна и различные сценарии, такие как наличие токена или необходимость его создания.
window.addEventListener("load", ()=>{
const token = sessionStorage.getItem("token");
if(token!==null){
const __rootElement = document.querySelector("#root");
const __content = generateUserProfile();
__rootElement.appendChild(__content);
return;
}
const params = new URLSearchParams(window.location.hash.split("#")[1]);
const code = params.get("code");
if(code){
getToken(code);
return;
}
redirectToKeycloak();
});
Заключительные мысли
Этот эксперимент Vanilla JS-Keycloak дает базовое понимание процесса аутентификации, закладывая основу для более сложных приложений. Обратите внимание, что в реальном сценарии необходимы дополнительные меры безопасности, такие как проверка «состояния» и «nonce». Эта статья служит практическим руководством для тех, кто интересуется безопасностью приложений и управлением идентификацией.