В веб-разработке безопасность и управление идентификацией имеют решающее значение. 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». Эта статья служит практическим руководством для тех, кто интересуется безопасностью приложений и управлением идентификацией.