---
title: "X-Authorization. Когда Authorization заголовок уже занят"
description: "Паттерн X-Authorization для решения конфликта Basic Auth и JWT в одном заголовке. Passport.js кастомный экстрактор, Nginx конфигурация, и как AI помог решить проблему за 5 промптов."
url: "https://ifonin.ru/blog/x-authorization-pattern/"
date: 2026-03-11
tags: ["vibe-coding","nodejs","authorization"]
---

# X-Authorization. Когда Authorization заголовок уже занят

Ваше приложение принимает JWT Bearer, впереди стоит Nginx с Basic Auth, и оба хотят использовать один и тот же `Authorization` заголовок. RFC 7235 говорит, что можно передать только одно значение, сервер молча перезаписывает одно другим - и ваш JWT теряется где-то в пути. Именно это случилось у меня в geo-monitor.

## Откуда вырастает проблема

В geo-monitor архитектура выглядела просто: Nginx принимает трафик, проверяет Basic Auth, проксирует на Node.js приложение, которое верифицирует JWT. Клиент отправлял Bearer токен, а до приложения доходило `Authorization: Basic dXNlcjpwYXNz`.

Происходит это потому, что RFC 7235 отводит под заголовок `Authorization` единственный слот с единственной схемой. `Basic` и `Bearer` - это две разные схемы, их нельзя передать одновременно в одном заголовке. RFC 7230 раздел 3.2.2 требует, чтобы если отправляются несколько заголовков с одинаковым именем, их значение было списком, разделённым запятыми - либо заголовок был явным исключением (как Set-Cookie). Большинство серверов просто перезаписывают первое значение вторым, без ошибки и без предупреждения. Ошибка проявляется уже на слое аутентификации, когда JWT исчез.

Это не теоретическая проблема. В GitHub issues Kong (#12692), Apache APISIX (#10247, #8521) и Envoy встречается один и тот же сценарий: Basic Auth на уровне прокси конфликтует с JWT в приложении. В Envoy некоторое время поведение было ещё интереснее - он конкатенировал два значения через запятую, и JWT парсер ломался на этой запятой (исправлено в PR #32365).

## Почему очевидные варианты не работают

Первое, что приходит в голову: отправлять два `Authorization` заголовка. RFC запрещает. Большинство серверов ведут себя непредсказуемо, положиться на это нельзя.

Второй вариант: переключить JWT на query-параметр, например `?token=eyJ0...`. Работает, но любой proxy log, WAF или CDN теперь видит токен в URL. Прямая утечка credentials в логи.

Третий вариант: перенастроить Nginx так, чтобы он не трогал `Authorization` и делал Basic Auth другим способом. Теоретически возможно, но если Nginx под чужим управлением или его конфигурацию менять нельзя - не подойдёт.

## Паттерн X-Authorization

Решение, которое сработало в geo-monitor: JWT передаётся в кастомном заголовке `X-Authorization`, Basic Auth остаётся в стандартном `Authorization`. Nginx занимается своим делом с `Authorization: Basic`, до приложения доходит `X-Authorization: Bearer eyJ0...`, и конфликта нет.

Со стороны клиента:

```bash
curl -H "Authorization: Basic dXNlcjpwYXNz" \
     -H "X-Authorization: Bearer eyJ0..." \
     http://api/profile
```

Passport.js делает это удобным через `ExtractJwt.fromExtractors()` - экстракторы пробуются по очереди, возвращается первый успешный. Кастомный экстрактор для geo-monitor:

```javascript
const flexibleJwtExtractor = (req) => {
  // Сначала смотрим в X-Authorization
  if (req.headers['x-authorization']) {
    return req.headers['x-authorization'].replace(/^Bearer\s+/, '');
  }
  // Если не нашли - проверяем стандартный Bearer
  if (req.headers.authorization?.startsWith('Bearer ')) {
    return req.headers.authorization.substring(7);
  }
  return null;
};
```

Регистрируется в конфиге стратегии:

```javascript
passport.use(new JwtStrategy({
  jwtFromRequest: flexibleJwtExtractor,
  secretOrKey: process.env.JWT_SECRET
}, verifyCallback));
```

## Пять этапов уточнения

В geo-monitor я решил эту проблему за пять итераций с Claude - не за одну и не за двадцать.

Первый запрос был размытым: "как обойти конфликт между Basic Auth и JWT в Passport.js?". Получил объяснение механики и вариант с `fromExtractors()` - я не знал, что эта опция вообще существует.

Второй уточнил контекст: "у меня Nginx делает Basic Auth, покажи конфигурацию для proxy и Node.js". Получил конкретные примеры с `proxy_pass_header` и кастомным заголовком.

Третий был про тестирование локально - нужен был способ воспроизвести схему без поднятия реального Nginx. Вышел на эмуляцию заголовков через curl.

Четвёртый - про edge cases: что если клиент отправит оба заголовка, или `x-authorization` придёт без `Bearer` префикса? Здесь поправили экстрактор под реальное поведение.

Пятый - про логирование: добавил `req.authMethod` в middleware. Это оказалось полезным для дебага позже, когда несколько клиентов неправильно собирали заголовки.

Каждый следующий запрос был точнее предыдущего - за счёт накопленного контекста, а не за счёт AI.

## Когда это подходит, а когда нет

X-Authorization хорошо работает в конкретных сценариях:

- Перед приложением стоит proxy, который уже занял `Authorization` для своих нужд
- Нужна обратная совместимость с legacy-клиентами на Basic Auth при поддержке JWT
- Хочется явного разделения: Basic Auth на уровне proxy, JWT на уровне приложения

Когда не стоит идти этим путём: новый API без Legacy-ограничений. RFC 6648 (BCP 178) рекомендует отказаться от `X-` префикса для новых кастомных заголовков - это соглашение считается устаревшим. Для нового API лучше сразу проектировать так, чтобы конфликта не возникало.

| Сценарий | Решение | Главный минус |
|----------|---------|---------------|
| Legacy Nginx + JWT приложение | X-Authorization | Кастомный заголовок, требует договорённости |
| Новый API без proxy | Authorization: Bearer | Нужна архитектурная дисциплина |
| Enterprise с несколькими схемами | Spring SecurityFilterChain | Verbose конфигурация |

## Как протестировать

Воспроизвести проблему локально проще, чем кажется:

```bash
# Имитируем что Nginx перезаписал Authorization
curl -H "Authorization: Basic dXNlcjpwYXNz" http://localhost:3000/api/profile
# → 401, JWT не найден

# Отправляем JWT в X-Authorization
curl -H "Authorization: Basic dXNlcjpwYXNz" \
     -H "X-Authorization: Bearer eyJ0..." \
     http://localhost:3000/api/profile
# → 200, JWT найден в x-authorization
```

Полезно добавить логирование в middleware - писать в консоль какой метод аутентификации сработал. В geo-monitor это помогло поймать несколько случаев, когда клиент неправильно собирал заголовки.

## Что вынес из geo-monitor

Конфликты авторизационных заголовков - это не экзотика. Kong, APISIX, Envoy все наступили на эти грабли в production. X-Authorization паттерн работает, он простой, его можно внедрить за несколько часов без переписывания архитектуры.

Но это временное решение. Если есть возможность спроектировать систему заново - лучше сразу сделать так, чтобы конфликта не было: отдельный endpoint для Basic Auth, отдельный для JWT, или уйти на один механизм.

Если вы сталкивались с похожей проблемой - пишите в комментарии, интересно сравнить сценарии.
