Перейти к основному содержимому

Интеграции — Basic, Bearer и mTLS на практике

Разработчику Инженеру Архитектору

Теория схем — в Basic, Bearer и mTLS. Здесь — что реально крутят в интеграциях: где проверяют учётные данные, какие заголовки прокидывают внутрь, как ротируют секреты и сертификаты и где чаще всего ломается безопасность.

Три типовых контура

Basic — legacy B2B, внутренние шины, «быстрый» партнёрский доступ за reverse proxy. Bearer — публичные и корпоративные API, OAuth 2.0, JWT access token. mTLS — регулируемый B2B, банки, гос. контуры; часто вместе с Bearer, а не вместо него.


Где проверяют — общая топология

СлойBasicBearermTLS
Ingress / Nginx / Kongauth_basic, плагин basic-authJWT plugin, auth_request к introspectionssl_verify_client, auth-tls в K8s Ingress
Service meshредкоpolicy по SPIFFE + JWTIstio/Linkerd mTLS между pod
Приложениедублировать только если нет gatewayaud, iss, scope, IDORмаппинг CN/SAN → partnerId

Правило: тяжёлую проверку выносят на gateway; backend получает уже нормализованный контекст (X-Partner-Id, X-Token-Sub), а не сырой Authorization из интернета.


Basic Auth — практическая настройка

Когда оставляют

  • Старый ERP/1С/скрипт партнёра умеет только логин/пароль.
  • Внутренний контур за VPN, срок жизни интеграции короткий, миграция на OAuth запланирована.
  • Dev/stage для ручных проверок (curl -u).

Что подкручивают в продакшене

НастройкаЗачем
Отдельный technical user на интеграциюНе LDAP-логин сотрудника; отзыв = отключить одну запись
Минимальные праваРоль «только POST /v1/orders/import», не admin
Только HTTPS + HSTSBase64 не шифрует пароль
Rate limit по IP + по userЗащита от перебора
Запрет Basic в queryПароль попадёт в access-log и Referer
Ротация пароля по календарю90 дней или при увольнении интегратора у партнёра

Nginx на краю

Проверка до попадания в приложение — партнёр не знает внутреннюю топологию:

location /integr/erp/ {
auth_basic "ERP partner zone";
auth_basic_user_file /etc/nginx/secrets/integr-erp.htpasswd;
limit_req zone=partner_erp burst=20 nodelay;

proxy_pass http://orders-backend;
proxy_set_header X-Integration-Id erp_legacy_01;
proxy_set_header Authorization "";
}

Разбор кода (Nginx, Basic на краю):

  • location /integr/erp/ — префикс URL, на который действуют директивы ниже (только интеграционный путь ERP).
  • auth_basic "ERP partner zone" — включает Basic Auth; строка в кавычках — текст realm в ответе 401 и в диалоге браузера.
  • auth_basic_user_file — путь к файлу htpasswd (логин + хеш пароля); генерируют offline, монтируют из secret store, не кладут в git.
  • limit_req zone=partner_erp burst=20 nodelay — ограничение частоты запросов по зоне partner_erp; burst — краткий всплеск, nodelay — без искусственной задержки в очереди.
  • proxy_pass http://orders-backend — проксирование на внутренний upstream после успешной аутентификации.
  • proxy_set_header X-Integration-Id erp_legacy_01 — служебный заголовок для backend: какая интеграция прошла проверку (аудит, rate limit по партнёру).
  • proxy_set_header Authorization ""обнуление внешнего Basic перед приложением; внутри кластера доверяют mTLS или internal JWT, пароль не утекает в логи сервиса.

Учётная запись только для машины

В Keycloak / AD заводят svc_erp_partner@integr.local с одной ролью integr.erp.push. Пароль хранит партнёр в своём vault; у вас — только хеш в htpasswd или bind к LDAP с фильтром «только этот DN».

Типичные ошибки

  • Один общий api:api на всех партнёров — при утечке страдают все.
  • Логирование заголовка Authorization в SIEM «для отладки».
  • Basic на публичном URL без WAF и без lockout.
  • «Временно» отключили TLS на staging — конфиг уехал в prod (устаревшие подходы).

Миграция: выдать партнёру client_id / client_secret → Client Credentials → Bearer; Basic оставить на переходный период с разными URL (/v1 vs /v1-legacy).


Bearer — практическая настройка

Поток machine-to-machine (самый частый)

Что подкручивают

ПараметрПрактичное значениеКомментарий
exp access token5–15 мин (M2M), до 1 ч (редко)Короткий срок снижает ущерб утечки
scopeorders:read, не *Проверять на каждом эндпоинте
aud / issжёстко в middlewareИначе токен с другого API принимается
JWKS cache5–15 мин + fallbackНе бить auth-сервер на каждый запрос
client_secretтолько в Vault/K8s SecretРотация без передеплоя приложения
Логимаскировать BearerВ 128 — утечка токена = полный доступ

Запрос токена (партнёр)

POST /realms/corp/protocol/openid-connect/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=partner_acme_erp
&client_secret=***
&scope=orders:import

Разбор кода (запрос токена):

  • POST — метод обмена учётных данных клиента на токен (тело в запросе, не в URL).
  • /realms/corp/protocol/openid-connect/token — endpoint token у Keycloak (OIDC); у других AS путь свой (/oauth/token).
  • Content-Type: application/x-www-form-urlencoded — параметры grant передаются как поля формы, не JSON.
  • grant_type=client_credentials — поток M2M без участия пользователя; только client_id + client_secret.
  • client_id — публичный идентификатор приложения-партнёра в реестре AS.
  • client_secret — секрет (в примере ***); только HTTPS и vault, не в логах.
  • scope=orders:import — запрашиваемые права; AS может выдать меньше, но не больше разрешённого для клиента.

Ответ:

{
"access_token": "eyJhbGciOiJSUzI1NiIs…",
"token_type": "Bearer",
"expires_in": 900,
"scope": "orders:import"
}

Разбор кода (ответ AS):

  • access_token — строка JWT или opaque token; её кладут в Authorization: Bearer ….
  • token_type: "Bearer" — подсказка клиенту формата заголовка (RFC 6750).
  • expires_in: 900 — срок жизни в секундах (15 мин); кэшировать до expires_in - 60, затем запросить новый.
  • scope — фактически выданные права; backend должен сверять с требуемой операцией.

Партнёр кэширует токен до expires_in - 60 секунд и шлёт:

GET /v1/orders/PO-1042 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs…
Accept: application/json

Разбор кода (вызов API с Bearer):

  • GET /v1/orders/PO-1042 — бизнес-запрос к resource API (идемпотентное чтение по id заказа).
  • Host: api.example.com — виртуальный хост gateway или балансировщика.
  • Authorization: Bearer eyJ… — access token из предыдущего шага; схема Bearer обязательна перед значением.
  • Accept: application/json — ожидаемый формат ответа; не влияет на авторизацию, но фиксирует контракт.

Gateway vs приложение

На gateway (Kong JWT, NGINX auth_jwt, Envoy JWT filter):

  • проверка подписи и exp;
  • отсечение запросов без токена до backend;
  • rate limit по sub / client_id.

В приложении (обязательно дополнительно):

  • scope на операцию (POST /orders требует orders:import);
  • IDOR: sub или claim tenant_id совпадает с владельцем ресурса — см. 128.

API-ключ в формате Bearer sk_live_… настраивают так же на краю, но проверка идёт lookup в Redis/БД, не JWKS — не смешивать middleware для JWT и для ключей в одной «слепой» проверке подписи.

Нормализация на ingress (псевдоконфиг)

После валидации JWT gateway подменяет контекст:

# Kong / аналог: после jwt плагина
headers:
X-Auth-Sub: "{{ jwt.sub }}"
X-Auth-Scopes: "{{ jwt.scope }}"
X-Auth-Client: "{{ jwt.azp }}"

Разбор кода (нормализация на gateway):

  • headers — блок подстановки исходящих заголовков к upstream после успешной JWT-проверки.
  • X-Auth-Sub: "{{ jwt.sub }}" — subject из payload (кто или какой сервис вызвал API).
  • X-Auth-Scopes: "{{ jwt.scope }}" — выданные scopes одной строкой или списком — backend проверяет операцию без повторного разбора JWT.
  • X-Auth-Client: "{{ jwt.azp }}" — authorized party / client_id приложения; удобно для аудита и лимитов по партнёру.
  • {{ jwt.* }} — шаблон плагина (Kong, Envoy и др.); синтаксис зависит от продукта.

Backend не парсит внешний Bearer повторно — доверяет только внутренней сети и заголовкам от gateway (или своему internal JWT).

Типичные ошибки

  • Принимать alg: none или смену алгоритма HS256/RS256 без whitelist.
  • Хранить access token в браузерном localStorage для «интеграционной» SPA (утечка XSS).
  • Один scope на все методы API.
  • Не отзывать client_id при расторжении договора — только смена пароля у людей.

mTLS — практическая настройка

Когда включают

  • Договор: «доступ только с сертификата организации X».
  • Закрытый контур без публичного OAuth (гос., финтех).
  • Service mesh: автоматический mTLS между сервисами (сертификация и TLS).

Сквозной сценарий с webhook и outbox — 1172.

Терминация на Ingress (Nginx)

server {
listen 443 ssl;
server_name api.payments.example;

ssl_certificate /etc/nginx/tls/server.crt;
ssl_certificate_key /etc/nginx/tls/server.key;

ssl_client_certificate /etc/nginx/tls/partners-ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;

location /v1/ {
if ($ssl_client_s_dn = "") { return 403; }
proxy_set_header X-Client-Cert-Subject $ssl_client_s_dn;
proxy_pass http://payments-api;
}
}

Разбор кода (Nginx, mTLS):

  • listen 443 ssl — HTTPS; mTLS работает поверх TLS-рукопожатия.
  • ssl_certificate / ssl_certificate_key — серверный cert и ключ (партнёр проверяет вашу сторону).
  • ssl_client_certificate — PEM доверенного CA, которым подписаны клиентские сертификаты партнёров.
  • ssl_verify_client on — требовать и проверять client cert; без cert — handshake не завершится успешно для API.
  • ssl_verify_depth 2 — глубина цепочки CA при проверке client cert (корневой → промежуточный → leaf).
  • if ($ssl_client_s_dn = "") { return 403; } — запрет запросов без валидного client cert (subject DN пустой).
  • proxy_set_header X-Client-Cert-Subject $ssl_client_s_dn — DN клиента в заголовке для маппинга на partnerId в приложении.
  • proxy_pass http://payments-api — проксирование только после успешного mTLS.

Приложение читает X-Client-Cert-Subject, сопоставляет с таблицей partnerId (как в 1172). PEM в handler не разбирают — это уже сделал ingress.

Kubernetes Ingress (nginx-controller)

Аннотации (смысл, не полный манифест):

metadata:
annotations:
nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
nginx.ingress.kubernetes.io/auth-tls-secret: "prod/partners-ca"
nginx.ingress.kubernetes.io/auth-tls-verify-depth: "2"

Разбор кода (Ingress, auth-tls):

  • metadata.annotations — способ передать настройки nginx-ingress без своего ConfigMap.
  • auth-tls-verify-client: "on" — аналог ssl_verify_client on на Ingress.
  • auth-tls-secret: "prod/partners-ca" — Secret в namespace с CA для проверки client cert (формат namespace/name).
  • auth-tls-verify-depth: "2" — максимальная глубина цепочки client cert.

CA для sandbox и prod разные Secret — партнёр не может случайно бить prod cert'ом из теста.

Service mesh (Istio / Linkerd)

  • Sidecar поднимает mTLS между pod без правок кода.
  • Внешний партнёр всё равно терминируется на Ingress Gateway с client cert.
  • Миграция: сначала PERMISSIVE, затем STRICT после обновления всех клиентов.

Ротация сертификатов

ШагДействие
T−90Партнёр получает новый cert, fingerprint в документации
T−30Оба cert принимаются (overlap)
TАлерт по notAfter за 14 дней
T+Отзыв старого в CA / удаление из trust store

Партнёру отдают не только PEM, но и SHA-256 fingerprint — сверка перед cutover.

mTLS + Bearer (два слоя)

  • mTLS — «это инфраструктура банка X».
  • Bearer — «этому client_id разрешён scope payouts:read».

Отключение только Bearer при живом mTLS оставляет риск украденного cert; только mTLS без scope — риск избыточных операций при компрометации cert.


Клиентский код по языкам

Ниже — исходящие вызовы партнёрского API (ваш сервис — клиент). Для приёма запросов на сервере см. колонку «Сервер (проверка)».

Сводка — библиотеки и ключевые типы

ЯзыкBasic (клиент)Bearer / OAuth2 Client CredentialsmTLS (клиент)Сервер (проверка)
C#AuthenticationHeaderValue, NetworkCredential + HttpClientHandler.CredentialsIdentityModelClientCredentialsTokenRequest, TokenClient; ASP.NET CoreAddJwtBearer, JwtBearerHandlerHttpClientHandler.ClientCertificates, X509Certificate2AddAuthentication().AddJwtBearer(), middleware, Kestrel client cert
JavaHttpRequest + Base64, Spring BasicAuthenticationInterceptorSpring Security OAuth2AuthorizedClientManager; java.net.http + JSON token; Nimbus JOSE JWTSSLContext, KeyManagerFactory, OkHttp HandshakeCertificatesSpring Security oauth2ResourceServer().jwt(), servlet filters
Pythonrequests / httpxHTTPBasicAuth, auth=(user, pass)requests-oauthlib OAuth2Session; authlib OAuth2Client; заголовок вручнуюcert=(client.pem, key.pem), verify=ca.pemFastAPI HTTPBearer, OAuth2PasswordBearer; Django DRF authentication_classes
TypeScriptaxios auth: { username, password }; fetch + btoaaxios headers.Authorization; oauth4webapi / openid-clientnode:https Agent({ cert, key, ca }) + axios/fetchExpress middleware; NestJS @nestjs/jwt, PassportStrategy
Goreq.SetBasicAuth(user, pass)golang.org/x/oauth2/clientcredentials + oauth2.NewClient; заголовок вручнуюtls.Config{ Certificates: []tls.Certificate{…} } в http.Transportgin / echo middleware JWT; tls.RequireAndVerifyClientCert
PHPGuzzle 'auth' => [user, pass]Guzzle заголовок Authorization; league/oauth2-client ClientCredentialsGuzzle 'cert' => [pem, key], 'verify' => ca.pemLaravel auth:api, Sanctum; Symfony access_token firewall

Секреты (client_secret, пароли, ключи PEM) — только из env / Vault, не из репозитория. Подробнее про HTTP-клиенты в экосистемах — C# интеграции, Python requests, Java HttpClient.


Basic Auth

C#

using System.Net.Http.Headers;
using System.Text;

var credentials = Convert.ToBase64String(
Encoding.UTF8.GetBytes("svc_erp:secret_from_vault"));
var client = httpClientFactory.CreateClient("ErpPartner");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", credentials);

// Альтернатива — встроенный handler (удобно для IIS/legacy)
var handler = new HttpClientHandler
{
Credentials = new NetworkCredential("svc_erp", "secret_from_vault")
};

Разбор кода (C#, Basic):

  • AuthenticationHeaderValue — тип заголовка Authorization в System.Net.Http.Headers.
  • Convert.ToBase64String + Encoding.UTF8.GetBytes("login:password") — кодирование пары логин:пароль в Base64 по RFC 7617 (без префикса Basic — его добавляет конструктор).
  • httpClientFactory.CreateClient("ErpPartner") — именованный клиент из DI с base address и политиками Polly.
  • DefaultRequestHeaders.Authorization — заголовок на все запросы этого экземпляра HttpClient.
  • HttpClientHandler + NetworkCredential — альтернатива: handler сам формирует Basic и обрабатывает 401 challenge (удобно для legacy IIS).

Java

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.util.Base64;

String basic = Base64.getEncoder()
.encodeToString("svc_erp:secret".getBytes(StandardCharsets.UTF_8));

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/v1/orders"))
.header("Authorization", "Basic " + basic)
.GET()
.build();

Разбор кода (Java, Basic):

  • Base64.getEncoder().encodeToString(...) — Base64 для строки login:password в UTF-8.
  • HttpRequest.newBuilder() — fluent-сборка immutable-запроса (Java 11+ HTTP Client API).
  • .uri(URI.create(...)) — абсолютный HTTPS URL ресурса.
  • .header("Authorization", "Basic " + basic) — схема Basic + пробел + payload (не только Base64).
  • .GET() — HTTP-метод; .build() — финализация HttpRequest.

Spring — new BasicAuthenticationInterceptor("svc_erp", secret) в RestTemplate / RestClient.

Python

import requests
from requests.auth import HTTPBasicAuth

response = requests.get(
"https://api.example.com/v1/orders",
auth=HTTPBasicAuth("svc_erp", os.environ["ERP_PASSWORD"]),
timeout=30,
)

Разбор кода (Python, Basic):

  • requests.get — синхронный HTTP GET; возвращает Response с .status_code, .json().
  • auth=HTTPBasicAuth(...) — библиотека сама кодирует логин/пароль и ставит заголовок Authorization.
  • os.environ["ERP_PASSWORD"] — секрет из окружения, не из кода.
  • timeout=30 — обрыв зависшего соединения через 30 с (интеграции без таймаута опасны).

httpxhttpx.BasicAuth("svc_erp", password) или auth=(user, pass).

TypeScript (Node)

import axios from 'axios';

const client = axios.create({
baseURL: 'https://api.example.com',
auth: { username: 'svc_erp', password: process.env.ERP_PASSWORD! },
});

Разбор кода (TypeScript, Basic):

  • axios.create — фабрика клиента с общими настройками (base URL, auth, interceptors).
  • baseURL — префикс для относительных путей (/v1/orders → полный URL).
  • auth: { username, password } — axios формирует Basic Auth на каждый запрос.
  • process.env.ERP_PASSWORD! — non-null assertion TypeScript; значение должно быть задано при старте процесса.

fetch`Authorization: Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`.

Go

req, _ := http.NewRequest(http.MethodGet, "https://api.example.com/v1/orders", nil)
req.SetBasicAuth("svc_erp", os.Getenv("ERP_PASSWORD"))
resp, err := http.DefaultClient.Do(req)

Разбор кода (Go, Basic):

  • http.NewRequest — создание запроса; первый аргумент — метод (http.MethodGet).
  • SetBasicAuth(user, pass) — установка заголовка Authorization: Basic … (кодирование внутри stdlib).
  • os.Getenv("ERP_PASSWORD") — чтение секрета из окружения.
  • http.DefaultClient.Do(req) — выполнение; err — сеть/TLS; статус смотрят в resp.StatusCode.

PHP (Guzzle)

use GuzzleHttp\Client;

$client = new Client([
'base_uri' => 'https://api.example.com',
'auth' => ['svc_erp', getenv('ERP_PASSWORD')],
]);
$client->get('/v1/orders');

Разбор кода (PHP, Basic):

  • GuzzleHttp\Client — HTTP-клиент с пулом соединений и middleware.
  • 'base_uri' — базовый URL; путь в get('/v1/orders') дописывается к нему.
  • 'auth' => [user, pass] — короткая форма Basic Auth в Guzzle (массив из двух строк).
  • getenv('ERP_PASSWORD') — пароль из env; в production предпочтительнее $_ENV через dotenv с кешем.

Bearer (OAuth 2.0 Client Credentials)

Общий шаблон — POST /token → кэш access_tokenAuthorization: Bearer … до expires_in.

C#

Пакет Duende.IdentityModel / IdentityModel:

using Duende.IdentityModel.Client;

var tokenResponse = await httpClient.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = "https://auth.example.com/realms/corp/protocol/openid-connect/token",
ClientId = "partner_acme_erp",
ClientSecret = configuration["OAuth:ClientSecret"],
Scope = "orders:import",
});

client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);

Разбор кода (C#, Bearer):

  • RequestClientCredentialsTokenAsync — обёртка IdentityModel над POST /token с grant_type=client_credentials.
  • ClientCredentialsTokenRequest — DTO: Address (token endpoint), ClientId, ClientSecret, Scope.
  • configuration["OAuth:ClientSecret"] — секрет из конфигурации/vault, не литерал в коде.
  • tokenResponse.AccessToken — строка access token из JSON-ответа AS.
  • AuthenticationHeaderValue("Bearer", …) — заголовок для последующих вызовов API.

На resource serverservices.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(...), проверка iss, aud, scope в policy.

Java

Spring Boot 3OAuth2AuthorizedClientManager + @RegisteredClient; для чистого JDK:

// После POST token — парсинг JSON (Jackson / Gson)
HttpRequest apiCall = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/v1/orders/PO-1"))
.header("Authorization", "Bearer " + accessToken)
.GET()
.build();

Разбор кода (Java, Bearer):

  • Комментарий «После POST token» — отдельный шаг: HttpClient к AS, парсинг JSON → переменная accessToken.
  • .header("Authorization", "Bearer " + accessToken) — схема Bearer; пробел после Bearer обязателен.
  • Остальное — тот же HttpRequest.newBuilder() / .uri / .GET() / .build(), что и для Basic.

Проверка входящих JWT — spring-boot-starter-oauth2-resource-server, класс конфигурации с JwtDecoder / JwtAuthenticationConverter.

Python

from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import BackendApplicationClient

client = BackendApplicationClient(client_id=os.environ["CLIENT_ID"])
oauth = OAuth2Session(client=client)
token = oauth.fetch_token(
token_url="https://auth.example.com/oauth/token",
client_id=os.environ["CLIENT_ID"],
client_secret=os.environ["CLIENT_SECRET"],
scope=["orders:import"],
)
r = oauth.get("https://api.example.com/v1/orders/PO-1")

Разбор кода (Python, Bearer):

  • BackendApplicationClient — oauthlib-клиент только для grant client_credentials (без браузера).
  • OAuth2Session(client=client) — сессия requests с автоматической подстановкой токена после fetch_token.
  • fetch_token(token_url=..., client_secret=..., scope=[...]) — POST на AS; возвращает dict с access_token, expires_in.
  • oauth.get(url) — GET с уже прикреплённым Authorization: Bearer ….

FastAPI (сервер) — OAuth2PasswordBearer, Depends(get_current_user); см. 3432.

TypeScript

import axios from 'axios';

async function getAccessToken(): Promise<string> {
const { data } = await axios.post(
'https://auth.example.com/oauth/token',
new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
scope: 'orders:import',
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
);
return data.access_token;
}

const api = axios.create({ baseURL: 'https://api.example.com' });
api.interceptors.request.use(async (config) => {
config.headers.Authorization = `Bearer ${await getAccessToken()}`;
return config;
});

Разбор кода (TypeScript, Bearer):

  • getAccessToken — отдельная функция получения токена; её результат кэшируют в памяти с учётом expires_in.
  • axios.post + URLSearchParams — тело application/x-www-form-urlencoded для OAuth token endpoint.
  • grant_type: 'client_credentials' — M2M-поток; client_id / client_secret из process.env.
  • data.access_token — поле ответа AS.
  • api.interceptors.request.use — middleware axios: перед каждым запросом подставляет свежий Bearer (нужен кэш токена, иначе лишние POST /token).

NestJS@nestjs/passport, JwtStrategy, AuthGuard('jwt').

Go

import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)

cfg := clientcredentials.Config{
ClientID: os.Getenv("CLIENT_ID"),
ClientSecret: os.Getenv("CLIENT_SECRET"),
TokenURL: "https://auth.example.com/oauth/token",
Scopes: []string{"orders:import"},
}
client := cfg.Client(ctx)
resp, err := client.Get("https://api.example.com/v1/orders/PO-1")

Разбор кода (Go, Bearer):

  • clientcredentials.Config — структура с ClientID, ClientSecret, TokenURL, Scopes.
  • cfg.Client(ctx) — возвращает *http.Client, который перед запросом получает/обновляет token (встроенный кэш oauth2).
  • client.Get(url) — обычный GET; transport сам добавляет Authorization: Bearer ….

PHP

league/oauth2-client:

use League\OAuth2\Client\Provider\GenericProvider;

$provider = new GenericProvider([
'clientId' => getenv('CLIENT_ID'),
'clientSecret' => getenv('CLIENT_SECRET'),
'urlAccessToken' => 'https://auth.example.com/oauth/token',
]);
$token = $provider->getAccessToken('client_credentials', ['scope' => 'orders:import']);

$client = new Client([
'headers' => ['Authorization' => 'Bearer ' . $token->getToken()],
]);

Разбор кода (PHP, Bearer):

  • GenericProvider — универсальный провайдер league/oauth2-client для кастомных AS.
  • 'urlAccessToken' — endpoint выдачи токена.
  • getAccessToken('client_credentials', ['scope' => ...]) — POST grant; возвращает объект AccessToken.
  • $token->getToken() — строка access token для заголовка.
  • Второй Client (Guzzle) — уже с фиксированным Authorization на все запросы.

Laravel (исходящий) — Http::withToken($token); входящий API — Sanctum / Passport.


mTLS

Клиент предъявляет свой сертификат; CA цепочки сервера проверяется отдельно (verify / ServerCertificateCustomValidationCallback только в dev с осторожностью).

C#

var handler = new HttpClientHandler();
handler.ClientCertificates.Add(
new X509Certificate2(
"/secrets/partner-client.pfx",
configuration["Cert:Password"],
X509KeyStorageFlags.MachineKeySet));
handler.ServerCertificateCustomValidationCallback = null; // prod — стандартная проверка

var client = new HttpClient(handler) { BaseAddress = new Uri("https://api.payments.example") };

Разбор кода (C#, mTLS):

  • HttpClientHandler — низкоуровневый handler для TLS, сертификатов и прокси.
  • ClientCertificates.Add — коллекция client cert, отправляемых при handshake.
  • X509Certificate2(path, password, flags) — загрузка PFX/PEM; MachineKeySet — ключ в хранилище машины (типично для Windows-сервисов).
  • ServerCertificateCustomValidationCallback = null — в prod стандартная проверка сервера (не отключать проверку в бою).
  • BaseAddress — базовый URL API; относительные пути в запросах дописываются к нему.

Java

KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new FileInputStream("client.p12"), password);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password);

SSLContext ssl = SSLContext.getInstance("TLS");
ssl.init(kmf.getKeyManagers(), null, null);

HttpClient client = HttpClient.newBuilder().sslContext(ssl).build();

Разбор кода (Java, mTLS):

  • KeyStore.getInstance("PKCS12") — хранилище в формате .p12 с client cert и ключом.
  • ks.load(stream, password) — загрузка keystore из файла.
  • KeyManagerFactory — фабрика ключей для исходящего client cert (кто мы для сервера).
  • SSLContext.getInstance("TLS") + ssl.init(kmf.getKeyManagers(), null, null) — контекст только с client auth; trust store сервера — по умолчанию JVM cacerts (для кастомного CA нужен TrustManager).
  • .sslContext(ssl) на HttpClient.Builder — все запросы этого клиента используют mTLS.

OkHttpHandshakeCertificates с heldCertificate + addTrustedCertificate.

Python

response = requests.get(
"https://api.payments.example/v1/payouts/po_1",
cert=("/secrets/client.crt", "/secrets/client.key"),
verify="/secrets/partners-ca.pem",
)

Разбор кода (Python, mTLS):

  • cert=(client.crt, client.key) — кортеж: публичный cert и приватный ключ клиента для TLS handshake.
  • verify="/secrets/partners-ca.pem" — доверенный CA (или цепочка) для проверки серверного cert; без этого возможна MITM.
  • requests.get — один запрос; для серии вызовов лучше requests.Session() с теми же cert и verify.

httpx — те же параметры cert= и verify=.

TypeScript (Node)

import https from 'node:https';
import fs from 'node:fs';
import axios from 'axios';

const agent = new https.Agent({
cert: fs.readFileSync('/secrets/client.crt'),
key: fs.readFileSync('/secrets/client.key'),
ca: fs.readFileSync('/secrets/partners-ca.pem'),
});

const client = axios.create({
baseURL: 'https://api.payments.example',
httpsAgent: agent,
});

Разбор кода (TypeScript, mTLS):

  • https.Agent — низкоуровневый TLS-агент Node.js для https и axios.
  • cert / key — буферы client cert и приватного ключа (readFileSync).
  • ca — буфер CA для проверки сервера API.
  • axios.create({ httpsAgent: agent }) — все HTTPS-запросы клиента идут через этот TLS-контекст (не путать с HTTP-прокси).

Go

cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
transport := &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
RootCAs: caCertPool, // загрузить partners-ca.pem
},
}
client := &http.Client{Transport: transport}

Разбор кода (Go, mTLS):

  • tls.LoadX509KeyPair — загрузка client cert + key из PEM-файлов в tls.Certificate.
  • http.Transport — настраиваемый транспорт; подменяет TLS у http.Client.
  • TLSClientConfig.Certificates — client cert для handshake.
  • MinVersion: tls.VersionTLS12 — запрет устаревших TLS 1.0/1.1.
  • RootCAs: caCertPool — пул доверенных CA для проверки сервера (загружают из partners-ca.pem).

Сервер — tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: pool } на http.Server.

PHP (Guzzle)

$client = new Client([
'base_uri' => 'https://api.payments.example',
'cert' => ['/secrets/client.crt', '/secrets/client.key'],
'verify' => '/secrets/partners-ca.pem',
]);

Разбор кода (PHP, mTLS):

  • 'cert' => [crt, key] — массив из двух путей: client certificate и private key (аналог Python cert=).
  • 'verify' => ca.pem — путь к CA для проверки сервера; true — системные CA, falseне использовать в prod (отключает проверку).
  • base_uri — как в Basic-примере; mTLS и Bearer можно комбинировать в одном Client + заголовок Authorization.

Basic + Bearer + mTLS в одном клиенте

Типичная связка для банковского API — mTLS на транспорте и Bearer в заголовке:

  1. Собрать HttpClient / requests.Session / Guzzle с client cert.
  2. Получить access token (Client Credentials).
  3. Добавить Authorization: Bearer … к каждому бизнес-запросу.

В C# удобно разделить — DelegatingHandler, который подставляет Bearer поверх HttpClient с mTLS-handler (интеграции C#).

Разбор логики (три слоя в одном клиенте):

  • Шаг 1 — transport с client cert (mTLS): «подключение разрешено только с нашей инфраструктуры».
  • Шаг 2 — OAuth Client Credentials: короткоживущий access_token с scope.
  • Шаг 3 — каждый бизнес-запрос несёт Authorization: Bearer … поверх уже установленного TLS-соединения.
  • DelegatingHandler в .NET — цепочка: внешний handler добавляет Bearer, внутренний handler держит mTLS; порядок регистрации важен.

Сводный чек-лист перед продом

Basic

  • Отдельная учётка/файл htpasswd на интеграцию
  • HTTPS, HSTS, rate limit
  • Authorization не логируется
  • План миграции на Bearer/OAuth с датой

Bearer

  • iss, aud, exp, scope проверяются
  • JWKS/introspection на gateway
  • Секреты в vault, ротация client_secret
  • IDOR-тесты с двумя токенами разных клиентов

mTLS

  • Trust store и SAN/CN согласованы с реестром партнёров
  • Overlap при ротации
  • 403 без cert, единый формат ошибки
  • При необходимости — второй слой (scope/JWS)

Наблюдаемость и инциденты

СобытиеЧто логироватьЧего избегать
Неуспешный Basicintegration_id, IP, счётчикпароль, base64 целиком
Invalid JWTiss, kid, errorполный токен
mTLS failssl_client_verify, subject DNprivate key
Успешный вызовpartner_id, scope, correlation-idизбыточные PII

Алерты: всплеск 401/403 с одного IP, истечение cert за 14 дней, рост invalid_token после деплоя JWKS.


Связанные материалы


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.