Данная статья будет полезна разработчикам браузерных (SPA) приложений, которые хотят настроить аутентификацию пользователей. Для аутентификации будет использоваться OAuth2/OIDC протокол c PKCE. В качестве сервера аутентификации будет использоваться сервер управления аутентификации с открытым исходным кодом OpenAM.

Настройка OpenAM

Установка OpenAM

Пусть OpenAM располагается на хосте openam.example.org. Если у вас уже установлен OpenAM, можете пропустить этот шаг. Самым простым способом развернуть OpenAM можно в Docker контейнере. Перед запуском, добавьте имя хоста и IP адрес в файл hosts, например 127.0.0.1 openam.example.org .

В Windows системах файл hosts находится по адресу C:\Windows\System32\drivers\etc\hosts , в Linux и Mac находится по адресу /etc/hosts

После этого запустите Docker контейнер OpenAM Выполните следующую команду:

docker run -h openam.example.org -p 8080:8080 --name openam openidentityplatform/openam

После того, как сервер запустится, запустите начальную конфигурацию OpenAM. Выполните следующую команду:

docker exec -w '/usr/openam/ssoconfiguratortools' openam bash -c \
'echo "ACCEPT_LICENSES=true
SERVER_URL=http://openam.example.org:8080
DEPLOYMENT_URI=/$OPENAM_PATH
BASE_DIR=$OPENAM_DATA_DIR
locale=en_US
PLATFORM_LOCALE=en_US
AM_ENC_KEY=
ADMIN_PWD=passw0rd
AMLDAPUSERPASSWD=p@passw0rd
COOKIE_DOMAIN=openam.example.org
ACCEPT_LICENSES=true
DATA_STORE=embedded
DIRECTORY_SSL=SIMPLE
DIRECTORY_SERVER=openam.example.org
DIRECTORY_PORT=50389
DIRECTORY_ADMIN_PORT=4444
DIRECTORY_JMX_PORT=1689
ROOT_SUFFIX=dc=openam,dc=example,dc=org
DS_DIRMGRDN=cn=Directory Manager
DS_DIRMGRPASSWD=passw0rd" > conf.file && java -jar openam-configurator-tool*.jar --file conf.file'

После успешной конфигурации OpenAM можно приступить к дальнейшей настройке.

Настройка OAuth2/OIDC провайдера

Зайдите в консоль администратора по ссылке

http://openam.example.org:8080/openam/XUI/#login/

В поле логин введите значение amadmin, поле пароль введите значение из параметра ADMIN_PWD команды установки, в данном случае passw0rd

Настройка OAuth2/OIDC

Выберите требуемый realm. В разделе Dashboard кликните на элементе Configure OAuth Provider

Затем Configure OpenID Connect

В открывшейся форме оставьте все настройки без изменений и нажмите кнопку Create

Теперь создадим OAuth2/OIDC клиент, который будет использовать SPA приложение для аутентификации.

Зайдите в консоль администратора, выберите требуемый realm, в меню слева выберите пункт Applications и далее OAuth 2.0

В таблице Agents нажмите кнопку New

  • Введите Name (client_id) test_client_id и Password (client_secret) changeit нового приложения

  • Откройте настройки приложения

  • Установите Client type в Public

  • Добавьте в список Redirection URIs URI вашего SPA приложения. В нашем случае это будет http://localhost:5173/

  • В список scope добавьте значение openid, это нужно, чтобы сразу получить идентификатор пользователя из возвращаемого объекта id_token.

  • Token Endpoint Authentication Method установите client_secret_post

Настройка CORS

SPA для получения access_token и id_token выполняет кросс-доменные запросы. Для того, чтобы данные запросы не блокировал браузер, нужно включить поддержку CORS в OpenAM.

Откройте консоль администратора. В верхнем меню выберите пункт Configure → Global Services.

Далее перейдите в CORS Settings и включите поддержку CORS

Нажмите `Save Changes`

Пример SPA приложения на React

В качестве примера будем использовать приложение, написанное на React. Для упрощения не будем проверять валидность параметра state, корректность подписи возвращаемого id_token и т.д. В продуктивном окружении настоятельно рекомендуем это сделать.

Создайте нового приложение, выполнив в консоле команду:

npm create vite@latest react-openam-example -- --template react

Добавьте в зависимости библиотеку CryptoJS. Она будет нужна для генерации code_challenge.

cd react-openam-example
npm install crypto-js

Замените содержимое файла react-openam-example/src/App.jsx следующим кодом:

import { useEffect, useState } from 'react'

import CryptoJS from 'crypto-js';

import './App.css'

const OPENAM_URL = "http://openam.example.org:8080/openam";
const OAUTH2_ENDPOINT = OPENAM_URL + "/oauth2";
const OAUTH2_AUTHORIZE_ENDPOINT = OAUTH2_ENDPOINT + "/authorize";
const OAUTH2_TOKEN_ENDPOINT = OAUTH2_ENDPOINT + "/access_token";
const CLIENT_ID = "test_client";
const SCOPE = "openid";

function App() {

  const [user, setUser] = useState("");

  //TODO should be randomly generated, saved and then restored in production evironment
  const codeVerifier = "a116cb8c-5a1e-4918-a164-255ae3d8f1b1"; 

  useEffect(() => {
    const params = new URLSearchParams(window.location.search)
    const code = params.get('code')
    if(!code) {
      return;
    }
    getToken(code)
  }, [])

  const getToken = async (code) => {
    const resp = await fetch(OAUTH2_TOKEN_ENDPOINT, {
      method: "POST",
      mode: "cors",
      cache: "no-cache", 
      credentials: "include", 
      headers: {'content-type': 'application/x-www-form-urlencoded'},
      redirect: "follow", 
      referrerPolicy: "no-referrer", 
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: CLIENT_ID,
        code_verifier: codeVerifier,
        code: code,
        redirect_uri: window.location.origin
      }),
    });
    if(resp.ok) {
      const accessToken = await resp.json()
      //TODO verify id_token signature
      const idToken = accessToken['id_token'];
      const parts = idToken.split('.')
      const payload = parts[1];
      const jsonPayload = JSON.parse(atob(payload));
      const sub = jsonPayload["sub"]
      setUser(sub)
      console.log(sub, "authenticated")
    } else {
      console.log(resp.status)
    }
  }
  

  const authOpenAM = () => {
    const state = "state";
    const codeChallenge = CryptoJS.SHA256(codeVerifier).toString(CryptoJS.enc.Base64url);
    console.log(codeChallenge);
    const queryString = "?redirect_uri=" + encodeURIComponent(window.location.origin) +
    "&client_id=" + CLIENT_ID +
    "&response_type=code" +
    "&state=" + state +
    "&scope=" + encodeURIComponent(SCOPE) +
    "&code_challenge=" + codeChallenge +
    "&code_challenge_method=S256";
    window.location = OAUTH2_AUTHORIZE_ENDPOINT + queryString;
  }

  const getComponent = () => {
    if (!user) {
      return <>
      <div>
        <h1>Not authenticated</h1>
      </div>
      <button onClick={authOpenAM}>Login</button>
    </>
    } else {
      return <h1>User {user} authenticated</h1>
    }
  }
  return getComponent()
}

export default App

Проверка решения

Запустите SPA командой

npm run dev

Откройте приложение в браузере перейдя по URL http://localhost:5173/

Нажмите кнопку Login. Вас перенаправит на аутентификацию в OpenAM. Введите логин пользователя demo и пароль changeit.

Подтвердите согласие на доступ к данным

После этого браузер перенаправит обратно в приложение и успешно аутентифицирует пользователя: Если все настроено корректно, SPA приложение отобразит сообщение об успешной аутентификации:

Комментарии (0)