План статьи
Введение
Привет, Хабр! Мы продолжаем цикл статей, в которых описываем нововведения СУБД Tantor Postgres 17.5.0. В прошлый раз мы писали о сценариях, когда сбор статистики с pg_stat_statements может чересчур замедлять работу системы, разбирали, как устроено сэмплирование и в каких случаях его применение позволяет снизить накладные расходы.
Теперь поговорим об авторизации через OAuth 2.0. Поддержка авторизации через OAuth 2.0 Device Authorization Flow — современный и безопасный способ предоставления доступа, представленный в PostgreSQL 18. Этот способ авторизации позволяет приложениям запрашивать доступ к PostgreSQL от имени пользователя через внешнего провайдера идентификации и управления доступом, например Keycloak, что особенно удобно для облачных сред и микросервисных архитектур. Поддержка этой функциональности в Tantor Postgres реализована начиная с представленной в июне 2025 г. версии 17.5.0.
В отличие от аутентификации по паролю (password, md5, SCRAM), OAuth позволяет централизовать управление учетными записями и политиками безопасности в едином провайдере идентификации и управления доступом. Device Authorization Flow идеально подходит для сценариев, где клиентская часть ограничена или отсутствует (например, терминальные приложения, автоматизированные сервисы), — пользователь подтверждает доступ на отдельном устройстве через браузер или мобильное приложение. Это делает процедуру авторизации более безопасной, так как нельзя перехватить пароль на клиентской части.
В этой статье мы пошагово разберём настройку OAuth-авторизации в PostgreSQL с использованием Keycloak: настроим Keycloak, подготовим PostgreSQL, напишем валидатор токенов OAuth в PostgreSQL и проверим успешную авторизацию через psql с использованием Device Flow. Иначе говоря, пройдём представленную ниже схему настройки OAuth-соединения в PostgreSQL:

Настройка Keycloak инженером по безопасности
Keycloak — это система идентификации и управления доступом с открытым исходным кодом (open source), которая позволяет управлять идентификацией пользователей, контролировать доступ к приложениям и данным, обеспечивая единую точку входа (SSO). Keycloak упрощает настройку доступа, восстановление пароля, редактирование профиля и рассылки одноразовых паролей, избавляя разработчиков от необходимости создавать дополнительные формы входа. С Keycloak эти процессы могут быть интегрированы всего несколькими щелчками мыши.
Запуск Keycloak
Запустим Keycloak с помощью образа Docker. Также откроем 8080 порт и создадим начального пользователя admin с именем пользователя admin и паролем admin. Опцией --name зададим имя создаваемого контейнера как keycloak.
docker run --name keycloak -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.2.1 start-dev
Далее введём в браузере адрес http://localhost:8080, откроется админ панель Keycloak, запросит логин-пароль. Введём admin, admin.
Права доступа пользователей в Keycloak инженер по безопасности настраивает в таком порядке:
Создание Realm
Создание Пользователей (Users)
Создание Client scope
Создание Клиентов (Clients)
Рассмотрим подробнее каждый этап.
Создание Realm
Realm — это область настройки безопасности, которая включает в себя учетные записи пользователей, роли, группы и настройки авторизации. Чтобы её создать, нужно кликнуть Manage realms в левом верхнем углу.

Вы увидите страницу Manage realms:

Далее нажимаем на Create realm, в появившемся диалоговом окне в поле Realm name вводим "postgres-realm".

После нажатия на Create наш realm создастся и станет текущим.

Создание пользователей (Users)
Пользователи (Users) – это субъекты, которые могут входить в систему. Они могут иметь связанные атрибуты, такие как электронная почта, имя пользователя, адрес, номер телефона и день рождения. Чтобы вызвать окно создания пользователей, нажмите на вкладке Users в левой панели:

Далее нажимаем Create new user, появится окно с вводом данных для нового пользователя. В поле Username вводим имя "alice", заполняем поля Email, First name и Last name.

Нажимаем кнопку Create, появится окно нового пользователя. ID понадобится администратору БД для сопоставления пользователя Keycloak и PostgreSQL в файле pg_ident.conf.

Переключаемся на вкладку Credentials и нажимаем на Set password для установки пароля:

Вводим пароль "alice". Переключатель Temporary (временный пароль) устанавливаем в выключенное положение, иначе при первом логине система потребует у пользователя установить новый пароль.

Нажимаем Save, затем в появившемся диалоговом окне нажимаем Save password:

Пароль установлен:

Создание областей действия (Client scopes)
Область действия (Client scope) — это способ ограничить права доступа, которые объявляются в токенах. Он позволяет клиенту запрашивать только роли, которые ему нужны, что делает токены более безопасными и управляемыми.
Переключимся на вкладку Client scopes:

Нажимаем на кнопку Create client scope, откроектся окно создания области действия. Вводим в поле Name значение "postgres", выбираем тип Default, активируем Include in token scope и сохраняем нажатием Save.

Создание клиента (Client)
Клиенты (Clients) – это приложения и сервисы, которые могут запрашивать авторизацию пользователя. Чтобы создать клиента – переходим на вкладку Clients и нажимаем Create client.

В появившемся окне General settings в поле Client ID вводим "postgres-client". Далее нажимаем Next.

В появившемся окне Capability config:
Включаем Client authentification (положение On);
Отключаем Standard flow, поскольку мы Authorization Code Flow не используем;
Включаем OAuth 2.0 Device Authorization Grant;
Нажимаем Next.

В окне Login settings не меняем ничего, просто нажимаем Save.

Открывается созданный нами клиент:

Далее переходим во вкладку Client scopes и проверяем, что:
присутствует postgres (на рисунке в самом низу) и для него выставлен тип Default;
для basic также выставлен тип Default.
Остальные scopes для нашего примера не важны, можно оставить все как есть.

На вкладке Credentials находится Client Secret, который нам надо будет вводить в терминале при логине в PostgreSQL (см. далее в секции «Авторизация через psql»)

Настройка PostgreSQL администратором баз данных
Возможность работы OAuth предполагает соответствующее конфигурирование PostgreSQL:
Создание пользователя в PostgreSQL;
Настройка параметров в файле postgresql.conf;
Настройка параметров в файле pg_ident.conf в случае сопоставления через него пользователей между Keycloak и PostgreSQL. Если сопоставление происходит в валидаторе, который написал разработчик, настраивать его не нужно;
Настройка параметров в файле pg_hba.conf.
Создание ролей
Роль — это сущность, которая может владеть объектами и иметь в базе определённые права. Роль может представлять пользователя, группу или и то, и другое в зависимости от варианта использования.
Создадим роль и дадим ей право подключения к базе данных:
CREATE ROLE alice;
ALTER ROLE alice WITH LOGIN;
Настройка файла postgresql.conf
В параметре oauth_validator_libraries зададим имя валидатора, который будет проверять токен (см. секцию «Написание валидатора разработчиком»).
oauth_validator_libraries = 'oauth_validator'
Если предоставлена только одна библиотека проверки, она будет использоваться по умолчанию для любых подключений OAuth; в противном случае все записи oauth HBA должны явно указывать средство проверки, выбранное из этого списка. Если задано значение пустой строки (по умолчанию), в подключениях OAuth будет отказано.
Сопоставление пользователей между Keycloak и PostgreSQL
Сопоставлять пользователей можно двумя способами:
через файл pg_ident.conf — это настраивает администратор баз данных;
с помощью валидатора – в валидаторе это сопоставление добавляет разработчик.
Сопоставление пользователей через файл pg_ident.conf
Настроим отображение id пользователей Keycloak и базы данных:
# MAPNAME SYSTEM-USERNAME PG-USERNAME
oauthmap "0fc72b6f-6221-4ed8-a916-069e7a081d14" "alice"
В первой колонке указывается имя сопоставления, во второй – ID пользователя из Keycloak (см. «Создание пользователей»), в третьей – имя роли в PostgreSQL.
Сопоставление пользователей через валидатор
Описано ниже в секции «Написание валидатора разработчиком».
Настройка файла pg_hba.conf
Настроим вход клиента в базу данных:
# TYPE DATABASE USER ADDRESS METHOD
local all all oauth issuer="http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-configuration" scope="openid postgres" map="oauthmap"
В четвертом поле следует указать oauth и далее его параметры. В параметре issuer указываем URL дискавери-сервиса "http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-configuration. В параметре Scope указываем области доступа, которые будут запрашиваться у Keycloak для клиента.
Далее нужно указать алгоритм сопоставления пользователей между Keycloak и PostgreSQL (см. «Сопоставление пользователей между Keycloak и PostgreSQL»).
Сопоставление пользователей через pg_ident.conf
Добавляем параметр map, в котором указываем map id из файла pg_ident.conf, у нас это "oauthmap":
# TYPE DATABASE USER ADDRESS METHOD
local all all oauth issuer="http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-configuration" scope="openid postgres" map="oauthmap"
Сопоставление пользователей через валидатор
В этом случае вместро параметра map следует задать параметр delegate_ident_mapping=1.
# TYPE DATABASE USER ADDRESS METHOD
local all all oauth issuer="http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-configuration" scope="openid postgres" delegate_ident_mapping=1
У параметра delegate_ident_mapping приоритет выше, чем у map, поэтому если с delegate_ident_mapping=1 будет также указан параметр map, то он будет проигнорирован, и сопоставление пользователей будет проходить через валидатор.
Написание валидатора разработчиком
Модули проверки подлинности OAuth реализуют свою функциональность, определяя набор обратных вызовов. Сервер будет вызывать их по мере необходимости для обработки запроса авторизации от пользователя.
Реализация валидатора токена
При сопоставлении пользователей между Keycloak и PostgreSQL реализация через pg_ident.conf отличается от сопоставления через валидатор только реализацией функции get_user. В примере ниже сопоставление реализовано с использованием pg_ident.conf. Проверка токена заключается в проверке того, что указанные в pg_hba.conf требуемые scope присутствуют в токене, полученом от сервера, в поле Scope. При успешной проверке идентификатор пользователя из поля sub токена присваивается res->authn_id.
Основная логика проверки реализована в функции validate_token. Общая схема ее работы:
1. Разбор содержимого токена: исходная строка токена анализируется для извлечения его содержимого (payload). Если токен имеет неправильный формат или содержимое не может быть извлечено, проверка завершается неудачей.
2. Извлечение из JWT-токена полей sub и scope. Содержимое токена должно включать оба поля:
sub (Subject) - идентификатор пользователя
scope – список разрешений (Scopes), предоставленных токеном, разделённых пробелами
Если любое из этих полей отсутствует, проверка считается непройденной.
3. Назначение идентификатора. Значение sub присваивается полю res->authn_id, которое PostgreSQL использует для идентификации пользователя. Это значение затем сопоставляется с записями в pg_ident.conf для определения фактической роли в базе данных, которую может использовать пользователь.
4. Сравнение разрешений (scopes). Разрешения, предоставленные токеном, сравниваются с теми, которые требуются соответствующей записью в pg_hba.conf (из oauth_scope). Если все требуемые разрешения присутствуют в токене, проверка считается успешной.
5. Установка результата авторизации. Флаг res->authorized устанавливается в true, если разрешения совпадают, иначе – false.
6. Сопоставление идентификатора. Значение sub затем сопоставляется (вне этого модуля) с записями в pg_ident.conf для определения фактической роли в базе данных, которую может использовать пользователь.
Приступим к написанию нашего учебного валидатора (его исходники также выложены на GitHub). Сперва создадим папку oauth_validator, а в ней создадим файл oauth_validator.c
oauth_validator.c
#include <string.h>
#include "postgres.h"
#include "token_utils.h"
#include "fmgr.h"
#include "libpq/oauth.h"
#include "miscadmin.h"
#include "nodes/pg_list.h"
#include "utils/builtins.h"
PG_MODULE_MAGIC;
/*
* Объявления внутренних функций модуля.
*/
static void validator_startup(ValidatorModuleState *state);
static void validator_shutdown(ValidatorModuleState *state);
static bool validate_token(const ValidatorModuleState *state,
const char *token,
const char *role,
ValidatorModuleResult *result);
/*
* Структура с указателями на функции обратных вызовов валидатора токенов OAuth.
* PostgreSQL вызывает их в определённые моменты жизненного цикла модуля.
*/
static const OAuthValidatorCallbacks validator_callbacks = {
PG_OAUTH_VALIDATOR_MAGIC, /* Магическое число для проверки версии API */
.startup_cb = validator_startup, /* Функция инициализации валидатора */
.shutdown_cb = validator_shutdown, /* Функция завершения работы валидатора */
.validate_cb = validate_token /* Функция проверки токена */
};
/*
* Точка входа в модуль валидатора OAuth.
* PostgreSQL вызывает эту функцию при загрузке модуля.
*/
const OAuthValidatorCallbacks *
_PG_oauth_validator_module_init(void)
{
return &validator_callbacks;
}
/*
* Функция инициализации валидатора.
* Вызывается один раз при старте работы модуля.
*/
static void
validator_startup(ValidatorModuleState *state)
{
/*
* Проверяем, совпадает ли версия сервера с той, с которой был собран модуль.
*/
if (state->sversion != PG_VERSION_NUM)
elog(ERROR, "oauth_validator: некорректная версия сервера: sversion=%d", state->sversion);
}
/*
* Функция завершения работы валидатора.
* Вызывается при выгрузке модуля или завершении работы сервера.
*/
static void
validator_shutdown(ValidatorModuleState *state)
{
/* Пока ничего не делаем, но сюда можно добавить освобождение ресурсов при необходимости. */
}
/*
* Основная функция проверки токена OAuth.
*
* Параметры:
* - state: состояние модуля валидатора (может содержать конфигурацию и т.п.);
* - token: строка с токеном, который нужно проверить;
* - role: имя роли PostgreSQL, от имени которой пытается подключиться клиент;
* - res: структура для возврата результата проверки.
*
* Возвращает true, если токен успешно проверен, иначе false.
*/
static bool
validate_token(const ValidatorModuleState *state,
const char *token, const char *role,
ValidatorModuleResult *res)
{
char *sub = NULL; /* Значение поля "sub" из токена (идентификатор пользователя) */
char *scope = NULL; /* Значение поля "scope" из токена (разрешённые области доступа) */
const char *token_payload = NULL; /* Полезная нагрузка (payload) токена в виде строки JSON */
List *granted_scopes = NIL; /* Список областей доступа, полученных из токена */
List *required_scopes = NIL; /* Список обязательных областей доступа из HBA-конфигурации */
bool matched = false; /* Флаг успешного совпадения необходимых областей доступа */
/* Инициализация результата */
res->authn_id = NULL; /* Идентификатор аутентификации (sub) */
res->authorized = false; /* Флаг авторизации */
/* Извлекаем полезную нагрузку из токена */
token_payload = parse_token_payload(token);
if (token_payload == NULL)
{
elog(LOG, "Неверный токен: отсутствует payload: %s", token);
return false;
}
/* Извлекаем поля 'sub' и 'scope' из полезной нагрузки */
extract_sub_scope_fields(token_payload, &sub, &scope);
if (!sub || !scope)
{
elog(LOG, "Неверный токен: отсутствуют поля sub и/или scope: %s", token);
return false;
}
/* Устанавливаем идентификатор пользователя (sub) в результат */
res->authn_id = pstrdup(sub);
/* Разбиваем список областей доступа из токена на элементы */
granted_scopes = split_scopes(scope);
/* Разбиваем обязательные области доступа из HBA-файла на элементы */
required_scopes = split_scopes(MyProcPort->hba->oauth_scope);
if (!granted_scopes || !required_scopes)
return false;
/* Проверяем, удовлетворяет ли токен обязательным областям доступа */
matched = check_scopes(granted_scopes, required_scopes);
/* Устанавливаем флаг авторизации */
res->authorized = matched;
return true;
}
token_utils.c/.h – функции-утилиты
token_utils.h
#ifndef TOKEN_UTILS_H
#define TOKEN_UTILS_H
#include <stdbool.h>
#include "common/jsonapi.h"
#include "nodes/pg_list.h"
const char* parse_token_payload(const char *token);
void extract_sub_scope_fields(const char *json, char **sub_field, char **scope_field);
const char *decode_base64(const char *b64);
char *base64url_to_base64(const char *b64url);
List *split_scopes(const char *raw);
bool check_scopes(List *granted, List *required);
#endif
token_utils.c
#include "PostgreSQL.h"
#include "token_utils.h"
#include "common/base64.h"
#include "mb/pg_wchar.h"
#define SUB_FIELD 0 /* Индекс для поля 'sub' */
#define SCOPE_FIELD 1 /* Индекс для поля 'scope' */
/*
* Обработчик поля JSON-объекта.
* Отмечает, что текущее обрабатываемое поле 'sub' и 'scope', чтобы сохранить их значения на следующем этапе обработки.
*/
static JsonParseErrorType
token_field_start(void *state, char *fname, bool isnull)
{
char **fields = (char **) state;
if (strcmp(fname, "sub") == 0)
fields[SUB_FIELD] = (char *) 1; /* Отметить, что обрабатываемое поле — это 'sub' */
else if (strcmp(fname, "scope") == 0)
fields[SCOPE_FIELD] = (char *) 1; /* Отметить, что обрабатываемое поле — это 'scope' */
return JSON_SUCCESS;
}
/*
* Обработчик значения JSON.
* Сохраняет значение 'sub' или 'scope', если оно было отмечено ранее.
*/
static JsonParseErrorType
token_scalar(void *state, char *token, JsonTokenType tokentype)
{
char **fields = (char **) state;
if (fields[SUB_FIELD] == (char *) 1)
fields[SUB_FIELD] = pstrdup(token); /* Сохраняем значение 'sub' */
else if (fields[SCOPE_FIELD] == (char *) 1)
fields[SCOPE_FIELD] = pstrdup(token); /* Сохраняем значение 'scope' */
return JSON_SUCCESS;
}
/*
* Извлечение полей 'sub' и 'scope' из JSON строки.
*
* Параметры:
* - json: строка JSON
* - sub_field: возвращает значение поля 'sub'
* - scope_field: возвращает значение поля 'scope'
*/
void
extract_sub_scope_fields(const char *json, char **sub_field, char **scope_field)
{
JsonLexContext lex;
JsonSemAction sem;
char **fields = palloc0(sizeof(char *) * 2); /* Выделяем память для 2 строк ('sub', 'scope') */
*sub_field = NULL;
*scope_field = NULL;
/* Создаём лексический контекст для разбора JSON */
makeJsonLexContextCstringLen(&lex, json, strlen(json), GetDatabaseEncoding(), true);
/* Настраиваем обработчики JSON-парсера */
memset(&sem, 0, sizeof(sem));
sem.semstate = (void *) fields;
sem.object_field_start = token_field_start;
sem.scalar = token_scalar;
/* Запускаем парсинг JSON */
pg_parse_json(&lex, &sem);
/* Возвращаем найденные значения */
*sub_field = fields[SUB_FIELD];
*scope_field = fields[SCOPE_FIELD];
}
/*
* Извлекает payload из токена JWT.
* Возвращает раскодированную строку payload в виде JSON.
*/
const char*
parse_token_payload(const char *token)
{
char *dot1 = NULL;
char *dot2 = NULL;
int payload_len = 0;
char *payload_b64url = NULL;
char *b64 = NULL;
if(!token)
return NULL;
/* Ищем первую и вторую точки в JWT (разделители header.payload.signature) */
dot1 = strchr(token, '.');
dot2 = dot1 ? strchr(dot1 + 1, '.') : NULL;
if (!dot1 || !dot2)
{
elog(LOG, "Неверный формат токена, требуется две точки: %s", token);
return NULL;
}
/* Извлекаем закодированный payload между точками */
payload_len = dot2 - (dot1 + 1);
payload_b64url = pnstrdup(dot1 + 1, payload_len);
/* Преобразуем base64url в обычный base64 */
b64 = base64url_to_base64(payload_b64url);
/* Декодируем base64 в JSON строку */
return decode_base64(b64);
}
/*
* Преобразует строку в формате base64url в формат base64.
* Заменяет символы '-' на '+', '_' на '/' и добавляет паддинг '=' при необходимости.
*/
char *
base64url_to_base64(const char *b64url)
{
int len = strlen(b64url);
int pad = (4 - (len % 4)) % 4; /* Определяем количество символов '=' для паддинга */
char *b64 = palloc(len + pad + 1);
for (int i = 0; i < len; i++)
{
if (b64url[i] == '-')
b64[i] = '+';
else if (b64url[i] == '_')
b64[i] = '/';
else
b64[i] = b64url[i];
}
/* Добавляем паддинг '=' */
for (int i = 0; i < pad; i++)
b64[len + i] = '=';
b64[len + pad] = '\0';
return b64;
}
/*
* Декодирует строку base64 в обычную строку.
* Возвращает раскодированную строку или NULL в случае ошибки.
*/
const char *
decode_base64(const char *b64)
{
int encoded_len = strlen(b64);
int max_decoded_len = pg_b64_dec_len(encoded_len); /* Вычисляем необходимую длину буфера */
char *decoded = palloc(max_decoded_len + 1);
int decoded_len = pg_b64_decode(b64, encoded_len, decoded, max_decoded_len);
if (decoded_len <= 0)
{
elog(LOG, "Неверный формат токена: ошибка декодирования base64");
return NULL;
}
decoded[decoded_len] = '\0';
return decoded;
}
/*
* Разбивает строку с пробелами (например, список scope из токена) на список строк List.
*/
List *
split_scopes(const char *raw)
{
List *result = NIL;
char *str = pstrdup(raw); /* Делаем копию строки, так как strtok её изменяет */
char *tok = strtok(str, " ");
while (tok)
{
result = lappend(result, pstrdup(tok));
tok = strtok(NULL, " ");
}
return result;
}
/*
* Функция сравнения строк для сортировки списка.
*/
static int
list_string_cmp(const ListCell *a, const ListCell *b)
{
const char *sa = (const char *) lfirst(a);
const char *sb = (const char *) lfirst(b);
return strcmp(sa, sb);
}
/*
* Проверяет, содержатся ли все обязательные области доступа (required) в предоставленных (granted).
* Списки предварительно сортируются.
*
* Возвращает true, если все required scopes найдены в granted scopes.
*/
bool
check_scopes(List *granted, List *required)
{
ListCell *gcell;
ListCell *rcell;
/* Сортируем оба списка для упрощения сравнения */
list_sort(granted, list_string_cmp);
list_sort(required, list_string_cmp);
gcell = list_head(granted);
rcell = list_head(required);
while (rcell != NULL && gcell != NULL)
{
char *r = (char *) lfirst(rcell);
char *g = (char *) lfirst(gcell);
int cmp = strcmp(r, g);
if (cmp == 0)
{
/* Найдено совпадение — переходим к следующему элементу required */
rcell = lnext(required, rcell);
gcell = lnext(granted, gcell);
}
else if (cmp > 0)
{
/* granted "отстаёт" — переходим к следующему элементу granted */
gcell = lnext(granted, gcell);
}
else
{
/* required элемент не найден в granted — возвращаем false */
return false;
}
}
/* Если не все элементы required были найдены — ошибка */
if (rcell != NULL)
return false;
return true;
}
Makefile
# contrib/oauth_validator/Makefile
PGFILEDESC = "oauth_validator - OAuth validator"
MODULE_big = oauth_validator
OBJS = \
$(WIN32RES) \
oauth_validator.o \
token_utils.o
PG_CPPFLAGS += -I$(top_srcdir)/src/common
PG_CPPFLAGS += -I$(libpq_srcdir)
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
Обратные вызовы
Обратный вызов startup_cb
Обратный вызов startup_cb выполняется сразу после загрузки модуля. Он может быть использован для настройки локального состояния и выполнения дополнительной инициализации, если требуется. Если валидатор имеет состояние, он может использовать поле state->private_data для его хранения.
typedef void (*ValidatorStartupCB) (ValidatorModuleState *state);
ValidatorStartupCB startup_cb;
Обратный вызов validate_cb
Обратный вызов validate_cb выполняется, когда пользователь пытается пройти авторизацию с помощью OAuth. Любое состояние, установленное в предыдущих вызовах, будет доступно в state->private_data.
typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state,
const char *token, const char *role,
ValidatorModuleResult *result);
ValidatorValidateCB validate_cb;
Аргумент token будет содержать токен-носитель для проверки. PostgreSQL позаботился о том, чтобы синтаксически токен был правильно сформирован, но никакой другой проверки не проводилось. Параметр role содержит роль, от имени которой пользователь запросил вход в систему. Обратный вызов должен задать выходные параметры в результирующей структуре, которая определяется так:
typedef struct ValidatorModuleResult
{
bool authorized;
char *authn_id;
} ValidatorModuleResult;
Соединение будет установлено только в случае если валидатор установит для параметра result->authorized значение true. Для аутентификации пользователя под аутентифицированное имя пользователя (т.е. определенное с помощью токена) должна быть выделена память (с помощью функции palloc), и указатель на этот регион памяти должен быть присвоен полю result->authn_id. В качестве альтернативы параметру result->authn_id может быть присвоено значение NULL, если токен действителен, но связанный с ним идентификатор пользователя не может быть определен.
В случае неудачной проверки токена валидатор должен возвращать значение false – вследствие неправильного формата токена, отсутствия необходимых прав у пользователя или иной ошибки, тогда любые параметры из аргумента result игнорируются и соединение прерывается. В случае же успешной проверки токена валидатор должен возвращать true.
Поведение после возврата из validate_cb зависит от конкретной настройки HBA. Обычно имя пользователя result->authn_id должно точно соответствовать роли, под которой пользователь входит в систему (это поведение может быть изменено с помощью карты пользователя). Но при аутентификации по правилу HBA с включенным delegate_ident_mapping PostgreSQL вообще не будет выполнять никаких проверок значения result->authn_id; в этом случае валидатор должен убедиться, что токен обладает достаточными привилегиями, чтобы пользователь мог войти в систему под указанной ролью.
Обратный вызов shutdown_cb
Обратный вызов shutdown_cb выполняется, когда существует серверный процесс, связанный с подключением. Если валидатор имеет какое-либо выделенное состояние, этот обратный вызов должен освободить его, чтобы избежать утечки ресурсов.
typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
ValidatorShutdownCB shutdown_cb;
Процесс авторизации
Логирование
Начнем с настройки логирования. Чтобы посмотреть, какие запросы отправляет PostgreSQL и какие ответы получает, в postgresql.conf можно включить логирование запросов:
log_connections = on
Примеры логов будут представлены далее, в них начале строк будут встречаться префиксы, означающие следующее:
префикс ">" означает запрос в Keycloak
префикс "<" означает ответ Keycloak
discovery
[libcurl] * Trying 192.168.0.156:8080...
[libcurl] * Connected to 192.168.0.156 (192.168.0.156) port 8080 (#0)
[libcurl] > GET /realms/postgres-realm/.well-known/openid-configuration HTTP/1.1
[libcurl] > Host: 192.168.0.156:8080
[libcurl] >
[libcurl] < HTTP/1.1 200 OK
[libcurl] < content-length: 6638
[libcurl] < Cache-Control: no-cache, must-revalidate, no-transform, no-store
[libcurl] < Content-Type: application/json;charset=UTF-8
[libcurl] < Referrer-Policy: no-referrer
[libcurl] < Strict-Transport-Security: max-age=31536000; includeSubDomains
[libcurl] < X-Content-Type-Options: nosniff
[libcurl] < X-Frame-Options: SAMEORIGIN
[libcurl] <
[libcurl] < {"issuer":"http://192.168.0.156:8080/realms/postgres-realm","authorization_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/auth","token_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/token","introspection_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/token/introspect","userinfo_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/userinfo","end_session_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/logout","frontchannel_logout_session_supported":true,"frontchannel_logout_supported":true,"jwks_uri":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/certs","check_session_iframe":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/login-status-iframe.html","grant_types_supported":["authorization_code","client_credentials","implicit","password","refresh_token","urn:ietf:params:oauth:grant-type:device_code","urn:ietf:params:oauth:grant-type:token-exchange","urn:ietf:params:oauth:grant-type:uma-ticket","urn:openid:params:grant-type:ciba"],"acr_values_supported":["0","1"],"response_types_supported":["code","none","id_token","token","id_token token","code id_token","code token","code id_token token"],"subject_types_supported":["public","pairwise"],"prompt_values_supported":["none","login","consent"],"id_token_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"id_token_encryption_alg_values_supported":["ECDH-ES+A256KW","ECDH-ES+A192KW","ECDH-ES+A128KW","RSA-OAEP","RSA-OAEP-256","RSA1_5","ECDH-ES"],"id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"userinfo_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"userinfo_encryption_alg_values_supported":["ECDH-ES+A256KW","ECDH-ES+A192KW","ECDH-ES+A128KW","RSA-OAEP","RSA-OAEP-256","RSA1_5","ECDH-ES"],"userinfo_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"request_object_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"request_object_encryption_alg_values_supported":["ECDH-ES+A256KW","ECDH-ES+A192KW","ECDH-ES+A128KW","RSA-OAEP","RSA-OAEP-256","RSA1_5","ECDH-ES"],"request_object_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"response_modes_supported":["query","fragment","form_post","query.jwt","fragment.jwt","form_post.jwt","jwt"],"registration_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/clients-registrations/openid-connect","token_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"token_endpoint_auth_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"introspection_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_encryption_alg_values_supported":["ECDH-ES+A256KW","ECDH-ES+A192KW","ECDH-ES+A128KW","RSA-OAEP","RSA-OAEP-256","RSA1_5","ECDH-ES"],"authorization_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"claims_supported":["aud","sub","iss","auth_time","name","given_name","family_name","preferred_username","email","acr"],"claim_types_supported":["normal"],"claims_parameter_supported":true,"scopes_supported":["openid","offline_access","organization","service_account","postgres","address","phone","acr","profile","microprofile-jwt","web-origins","roles","basic","email"],"request_parameter_supported":true,"request_uri_parameter_supported":true,"require_request_uri_registration":true,"code_challenge_methods_supported":["plain","S256"],"tls_client_certificate_bound_access_tokens":true,"revocation_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/revoke","revocation_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"device_authorization_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/auth/device","backchannel_token_delivery_modes_supported":["poll","ping"],"backchannel_authentication_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/ext/ciba/auth","backchannel_authentication_request_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","ES256","RS256","ES512","PS256","PS512","RS512"],"require_pushed_authorization_requests":false,"pushed_authorization_request_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/ext/par/request","mtls_endpoint_aliases":{"token_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/token","revocation_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/revoke","introspection_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/token/introspect","device_authorization_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/auth/device","registration_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/clients-registrations/openid-connect","userinfo_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/userinfo","pushed_authorization_request_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/ext/par/request","backchannel_authentication_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/ext/ciba/auth"},"authorization_response_iss_parameter_supported":true}
auth device
[libcurl] * Connection #0 to host 192.168.0.156 left intact
[libcurl] * Found bundle for host: 0x6124961fd400 [serially]
[libcurl] * Can not multiplex, even if we wanted to
[libcurl] * Re-using existing connection #0 with host 192.168.0.156
[libcurl] * Server auth using Basic with user 'postgres-client'
[libcurl] > POST /realms/postgres-realm/protocol/openid-connect/auth/device HTTP/1.1
[libcurl] > Host: 192.168.0.156:8080
[libcurl] > Authorization: Basic cG9zdGdyZXMtY2xpZW50OmZTY1hYcDFUcFNQY3BVaEZLcWk0eDk4alZ5NTR1Y1RC
[libcurl] > Content-Length: 47
[libcurl] > Content-Type: application/x-www-form-urlencoded
[libcurl] >
[libcurl] > scope=openid+postgres&client_id=postgres-client
[libcurl] < HTTP/1.1 200 OK
[libcurl] < Cache-Control: no-store, must-revalidate, max-age=0
[libcurl] < content-length: 296
[libcurl] < Content-Type: application/json
[libcurl] < Referrer-Policy: no-referrer
[libcurl] < Strict-Transport-Security: max-age=31536000; includeSubDomains
[libcurl] < X-Content-Type-Options: nosniff
[libcurl] < X-Frame-Options: SAMEORIGIN
[libcurl] <
[libcurl] < {"device_code":"tgElqUogevqjEkZy6-Z1i209pgoKW9_CT0t9wwNjafY","user_code":"WXAI-ZNVY","verification_uri":"http://192.168.0.156:8080/realms/postgres-realm/device","verification_uri_complete":"http://192.168.0.156:8080/realms/postgres-realm/device?user_code=WXAI-ZNVY","expires_in":600,"interval":5}
token
Клиент ожидает одобрения запроса авторизации пользователя:
[libcurl] * Connection #0 to host 192.168.0.156 left intact
[libcurl] * Found bundle for host: 0x6124961fd400 [serially]
[libcurl] * Can not multiplex, even if we wanted to
[libcurl] * Re-using existing connection #0 with host 192.168.0.156
[libcurl] * Server auth using Basic with user 'postgres-client'
[libcurl] > POST /realms/postgres-realm/protocol/openid-connect/token HTTP/1.1
[libcurl] > Host: 192.168.0.156:8080
[libcurl] > Authorization: Basic cG9zdGdyZXMtY2xpZW50OmZTY1hYcDFUcFNQY3BVaEZLcWk0eDk4alZ5NTR1Y1RC
[libcurl] > Content-Length: 147
[libcurl] > Content-Type: application/x-www-form-urlencoded
[libcurl] >
[libcurl] > device_code=tgElqUogevqjEkZy6-Z1i209pgoKW9_CT0t9wwNjafY&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&client_id=postgres-client
[libcurl] < HTTP/1.1 400 Bad Request
[libcurl] < Cache-Control: no-store
[libcurl] < Pragma: no-cache
[libcurl] < content-length: 98
[libcurl] < Content-Type: application/json
[libcurl] < Referrer-Policy: no-referrer
[libcurl] < Strict-Transport-Security: max-age=31536000; includeSubDomains
[libcurl] < X-Content-Type-Options: nosniff
[libcurl] < X-Frame-Options: SAMEORIGIN
[libcurl] <
[libcurl] < {"error":"authorization_pending","error_description":"The authorization request is still pending"}
Пользователь одобрен:
[libcurl] * Connection #0 to host 192.168.0.156 left intact
[libcurl] * Found bundle for host: 0x6124961fd400 [serially]
[libcurl] * Can not multiplex, even if we wanted to
[libcurl] * Re-using existing connection #0 with host 192.168.0.156
[libcurl] * Server auth using Basic with user 'postgres-client'
[libcurl] > POST /realms/postgres-realm/protocol/openid-connect/token HTTP/1.1
[libcurl] > Host: 192.168.0.156:8080
[libcurl] > Authorization: Basic cG9zdGdyZXMtY2xpZW50OmZTY1hYcDFUcFNQY3BVaEZLcWk0eDk4alZ5NTR1Y1RC
[libcurl] > Content-Length: 147
[libcurl] > Content-Type: application/x-www-form-urlencoded
[libcurl] >
[libcurl] > device_code=tgElqUogevqjEkZy6-Z1i209pgoKW9_CT0t9wwNjafY&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&client_id=postgres-client
[libcurl] < HTTP/1.1 200 OK
[libcurl] < Cache-Control: no-store
[libcurl] < Pragma: no-cache
[libcurl] < content-length: 3307
[libcurl] < Content-Type: application/json
[libcurl] < Referrer-Policy: no-referrer
[libcurl] < Strict-Transport-Security: max-age=31536000; includeSubDomains
[libcurl] < X-Content-Type-Options: nosniff
[libcurl] < X-Frame-Options: SAMEORIGIN
[libcurl] <
[libcurl] < {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwb1RQSV85LXVIQWJ5Q0t3M2luTm5MTW5hU21hc05nWS1OaDVkTzJ4X0lNIn0.eyJleHAiOjE3NDYwMzEyNTMsImlhdCI6MTc0NjAzMDk1MywiYXV0aF90aW1lIjoxNzQ2MDMwOTUyLCJqdGkiOiJvbnJ0ZGc6MWU3MmRlNGUtNTRhYi00OTZjLWIxYWYtY2FhYmRiYzJlYzExIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMC4xNTY6ODA4MC9yZWFsbXMvcG9zdGdyZXMtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMGZjNzJiNmYtNjIyMS00ZWQ4LWE5MTYtMDY5ZTdhMDgxZDE0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicG9zdGdyZXMtY2xpZW50Iiwic2lkIjoiNjRlMDUzMTMtM2UyMi00MmNjLWE0YmItOTAxODU2YTFhYjMzIiwiYWxsb3dlZC1vcmlnaW5zIjpbIi8qIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsImRlZmF1bHQtcm9sZXMtcG9zdGdyZXMtcmVhbG0iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgcG9zdGdyZXMiLCJuYW1lIjoiYWxpY2UgcG9zdGdyZXMiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhbGljZSIsImdpdmVuX25hbWUiOiJhbGljZSIsImZhbWlseV9uYW1lIjoicG9zdGdyZXMifQ.RXMszI-snIdrXyyTw74U8QXQeDG3zpfV4OvxYuJQvsb86eauXkKHAH35GfEm3XvQbtmpdSdfs1S4i11d69dUjpVTgPpzx6G7IXCXj2NTowzZuyuvdnLxPi1aXdxXqOKNSLSj5PXhGIaZhWsn2sR8dAJ0jjWTUO_lh8qJuJYaDcFulWn_flHVGQYzMZ5PTneRadg8h_1dWp4HSr6yC74NmF94dnOBmytivM4a__Wcq6TkZ3KLn_gafqnn72HpWY0WRwyZdQuzc5o8mE3UUAoKukxMnwDG7Yhxif2YFb_a5aCloMbL9aDghbMypahl3MiJHHx3j50FavSRm0FJa3zK9w","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjOGNiNjY3Ni00OTAxLTRmNjItOTI0OS1kMzY2MWI5Mjg3OTIifQ.eyJleHAiOjE3NDYwMzI3NTMsImlhdCI6MTc0NjAzMDk1MywianRpIjoiZTJkNzkzODUtNjBhZS00MTIwLWIwODAtNjVmYWU4ZmNhYzIzIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMC4xNTY6ODA4MC9yZWFsbXMvcG9zdGdyZXMtcmVhbG0iLCJhdWQiOiJodHRwOi8vMTkyLjE2OC4wLjE1Njo4MDgwL3JlYWxtcy9wb3N0Z3Jlcy1yZWFsbSIsInN1YiI6IjBmYzcyYjZmLTYyMjEtNGVkOC1hOTE2LTA2OWU3YTA4MWQxNCIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJwb3N0Z3Jlcy1jbGllbnQiLCJzaWQiOiI2NGUwNTMxMy0zZTIyLTQyY2MtYTRiYi05MDE4NTZhMWFiMzMiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIHdlYi1vcmlnaW5zIHBvc3RncmVzIHJvbGVzIGJhc2ljIn0.43pRSq4PBO7ZY86jt8dIL7xZJylntY_CZXllcRfwfh41IRCOft6iqIWdJQp7TJv_JIDI-_-QeOSf1EC_wzABNg","token_type":"Bearer","id_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwb1RQSV85LXVIQWJ5Q0t3M2luTm5MTW5hU21hc05nWS1OaDVkTzJ4X0lNIn0.eyJleHAiOjE3NDYwMzEyNTMsImlhdCI6MTc0NjAzMDk1MywiYXV0aF90aW1lIjoxNzQ2MDMwOTUyLCJqdGkiOiIzNWM3ZDAyZC05OGFjLTQzMTgtOTg3NC0zYzY0Mjg5NjFhMjgiLCJpc3MiOiJodHRwOi8vMTkyLjE2OC4wLjE1Njo4MDgwL3JlYWxtcy9wb3N0Z3Jlcy1yZWFsbSIsImF1ZCI6InBvc3RncmVzLWNsaWVudCIsInN1YiI6IjBmYzcyYjZmLTYyMjEtNGVkOC1hOTE2LTA2OWU3YTA4MWQxNCIsInR5cCI6IklEIiwiYXpwIjoicG9zdGdyZXMtY2xpZW50Iiwic2lkIjoiNjRlMDUzMTMtM2UyMi00MmNjLWE0YmItOTAxODU2YTFhYjMzIiwiYXRfaGFzaCI6ImphOXRKZ1E0VkVPTTNBZGc0VWJBVGciLCJuYW1lIjoiYWxpY2UgcG9zdGdyZXMiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhbGljZSIsImdpdmVuX25hbWUiOiJhbGljZSIsImZhbWlseV9uYW1lIjoicG9zdGdyZXMifQ.dH5hM21-ygBiCXoA9NVOou-L3esUVJUFFUmt_1fU0jc9al4Lk7EUN-cqHicHZPD48HhkIWMJK7WZjxnZLDlkG7ORGdDKPGccMU4-sGsRuVu-GDuNFo_5kHAh_NJZsLBXz9UkpNBkd8ROxK3-fbmyYdwsuwNeg6KNhSOj0FxEnxLc0-HrjEE92P7hzq0PD29oY2jhRKcqpbtknMwxFkkMBi8xPgdpuyTmLtJD3-xxYuwMKP7WUGzwzVAFqfrhFm5O5dJxeld5fTFE4Kyl9fR24JcjtfxBeIHVJqLiQkl9Et_KNGiFoXDG4Xwcc7eIUaBnauhY5_froYvKS8NbQxCOUg","not-before-policy":0,"session_state":"64e05313-3e22-42cc-a4bb-901856a1ab33","scope":"openid profile postgres"}
Авторизация через psql
Для целей тестирования разрешим авторизацию по незащищённому протоколу http (при использовании https надо будет настраивать цепочки сертификатов):
export PGOAUTHDEBUG="UNSAFE"
Запустим psql и укажем детали подключения:
psql "user=alice dbname=postgres oauth_issuer=http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-configuration oauth_client_id=postgres-client oauth_client_secret=YYi8LqfzHRMnqUUlptpWVC2k7eWNrqjX"
Мы увидим URL и код, который нужно будет ввести после перехода на этот URL:
Вводим в браузере http://192.168.0.156:8080/realms/postgres-realm/device:

Вводим код и нажимаем Submit.

Далее видим окно логина пользователя:

Вводим логин-пароль (см. «Создание пользователей») и нажимаем на Sign In.

Далее спрашивает, готовы ли мы предоставить (прислать) такую информацию PostgreSQL. Нажимаем Yes.

Успешно залогинились...

... и можем вводить команды в терминале:
psql (18devel)
Type "help" for help.
postgres=>
В итоге мы успешно авторизовались в PostgreSQL.
Заключение
Интеграция OAuth 2.0 Device Authorization Flow, представленная в СУБД PostgreSQL 18 и Tantor Postgres 17.5.0, позволяет применять механизм SSO (single sign-on). Реализация централизованного управления доступом через провайдеров вроде Keycloak повышает уровень защиты, исключая риски перехвата паролей, и оптимизирует администрирование в распределённых средах. В статье представлено пошаговое руководство от настройки Keycloak и конфигурации PostgreSQL до реализации валидатора токенов и успешной авторизации через psql.
shurutov
GSSAPI?! Не, не знаю, да и зачем?
Sleuthhound
GSSAPI с Kerberos кажется сложнее, чем OAuth2
Ну и OAuth2 кажется, что это уже стандарт дефакто и очень радует что в 18-й версии появилась такая возможность.
ivan-kush
GSSAPI - протокол аутентификации, OAuth - протокол авторизации, они в принципе для разных нужд. Если вы принципиально считаете, что OAuth в Postgres не нужен - это можно обсудить с разработчиками Postgres, которые реализуют OAuth в Postgres 18 -
https://www.postgresql.org/message-id/d1b467a78e0e36ed85a09adf979d04cf124a9d4b.camel%40vmware.com
также можно черкануть ребятам из pgAdmin
https://www.pgadmin.org/docs/pgadmin4/development/oauth2.html
shurutov
Я вижу аутентификацию в статье. И никакой авторизации.
ivan-kush
Авторизация происходит в валидаторе. В представленном базовом примере авторизация заключается в сравнении, что scope-ы из pg_hba.conf присутствуют в полученном от сервера токене. Значит, мы можем залогиниться в postgres. На значениях scope-ов можно реализовать ещё какую-то логику.
Также токен может содержать и другие поля, на основе которых в валидаторе может быть реализована какая-то логика получения прав.