Данная статья будет полезна разработчикам браузерных (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 приложение отобразит сообщение об успешной аутентификации: