Решение проблем

Ошибка 401 Unauthorized: что требует сервер и почему не доходят учётные данные

Ошибка 401 Unauthorized (HTTP 401): сервер ждёт действительные учётные данные и не получил их. Как прочитать WWW-Authenticate и починить auth_basic и токены.

Опубликовано ~8 мин чтения
На этой странице

Коротко: ошибка 401 Unauthorized, то есть код HTTP 401 («не авторизован»), значит, что сервер хочет, чтобы вы прошли аутентификацию, а в запросе не было действительных учётных данных: их не было совсем, они не того вида, истекли или были отвергнуты. Заголовок ответа WWW-Authenticate называет схему, которую ждёт сервер, например Basic или Bearer. Войдите заново или отправьте свежий токен в этом формате. Если вы уже аутентифицированы и всё равно получаете отказ, перед вами 403, а это другая проблема.

что вы отправили и чего хочет сервер
curl -v https://example.com/admin/ 2>&1 | grep -iE '^> authorization|^< HTTP|^< www-authenticate'
# < HTTP/1.1 401 Unauthorized
# < WWW-Authenticate: Basic realm="Staff only"
#
# В выводе нет строки "> Authorization:"? Значит, учётные данные вообще не ушли с вашей машины.

Чем 401 отличается от 403?

Несмотря на название, 401 касается аутентификации (кто вы), а 403 авторизации (что вам можно). RFC 9110 требует, чтобы каждый ответ 401 содержал заголовок WWW-Authenticate хотя бы с одним вызовом (challenge), а если в запросе были учётные данные, 401 значит, что их отвергли. 403 значит, что сервер понял запрос и отказывает в нём; повтор с теми же учётными данными ответа не изменит.

Вопрос401 Unauthorized403 Forbidden
Что говорит серверКто вы? Действительных учётных данных нетЯ знаю, кто вы, или это неважно: нет
Обязательный заголовокWWW-Authenticate с ожидаемой схемойНет
Поможет ли войти заново?Да, с действительными учётными даннымиНет: нужны другие права, другая учётная запись или изменение на сервере
Типичная причина в nginxauth_basic без правильных пользователя и пароляПравила deny, права на файлы, нет индексного файла
API с OAuth Bearer (RFC 6750)invalid_token: токен истёк, отозван или искажёнinsufficient_scope: токен действителен, но прав не хватает

407 Proxy Authentication Required означает то же самое для прокси между вами и сервером, с заголовком Proxy-Authenticate. Если у вас 403, эту сторону разбирает статья о 403 Forbidden в nginx.

Что говорит заголовок WWW-Authenticate?

частые варианты
WWW-Authenticate: Basic realm="Staff only"
WWW-Authenticate: Bearer realm="api"
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The access token expired"
WWW-Authenticate: Negotiate
  • Basic: имя пользователя и пароль в base64, в Authorization: Basic …. Именно эта схема заставляет браузер показать окно входа.
  • Bearer без ошибки: вы вообще не отправили токен. RFC 6750 велит серверам в этом случае не сообщать подробностей.
  • Bearer с error="invalid_token": токен истёк, отозван или искажён. Получите новый. Ошибка insufficient_scope относится к 403, а invalid_request к 400.
  • Negotiate или NTLM: встроенная аутентификация Windows, как у IIS в домене. Первая 401, за которой следует повтор с токеном, — это нормальное рукопожатие, а не сбой.
  • Заголовка нет вовсе: сервер нарушает спецификацию, как делают многие API. Прочитайте тело ответа: обычно там написано, что не так.

Почему возникает ошибка 401? Причины от частых к редким

  1. 1Пригодные учётные данные не дошли до сервера

    В выводе curl -v найдите строку > Authorization:. Если её нет или она выглядит неправильно, виноват запрос, а не сервер:

    учётные данные в curl
    echo "${#TOKEN}"     # 0 значит, что $TOKEN в этой оболочке пуст: уходит "Bearer " и больше ничего
    
    curl -v -H "Authorization: Bearer $TOKEN" https://api.example.com/me
    curl -v --oauth2-bearer "$TOKEN" https://api.example.com/me     # тот же заголовок
    curl -v -u 'alice:pa$$word' https://example.com/admin/          # одинарные кавычки оставляют $ как есть

    Обычные промахи: не то слово схемы (Bearer, Token и Bot не взаимозаменяемы; токены ботов Discord идут в Authorization: Bot …), вставленные в токен кавычки или перенос строки, а также пароль с $ внутри двойных кавычек, который раскрывает оболочка. Собирать заголовок Basic через echo тоже нельзя: перевод строки становится частью пароля:

    заголовок Basic вручную
    echo 'alice:secret' | base64             # неверно: кодирует перевод строки в конце, 401
    printf '%s' 'alice:secret' | base64      # верно
    curl -u 'alice:secret' https://example.com/admin/    # проще всего: пусть curl соберёт его сам
  2. 2Токен истёк, отозван или выдан для другого места

    У ключей API и токенов есть срок жизни, после утечки их меняют, и часто они привязаны к одному окружению: тестовый ключ, отправленный в боевой API, или ключ одного региона либо аккаунта, предъявленный другому, получает 401. JWT несёт собственный срок действия, и прочитать его можно локально. Полезная нагрузка лишь закодирована в base64url, а не зашифрована, так что вставлять боевой токен на какой-либо сайт незачем:

    Debian · Ubuntu
    python3 -c 'import sys,json,base64; p=sys.argv[1].split(".")[1]; p+="="*(-len(p)%4); print(json.dumps(json.loads(base64.urlsafe_b64decode(p)), indent=2))' "$TOKEN"
    
    date -u -d @1799999999      # перевести число из "exp" или "nbf" в дату
    timedatectl                 # ищите "System clock synchronized: yes"

    Часы важны с обеих сторон. Сервер, у которого время сбито на несколько минут, считает свежие токены ещё не действительными (nbf) или уже истёкшими (exp), поэтому 401, которую получает только одна машина, часто означает, что на ней сломана синхронизация времени.

  3. 3Учётные данные потерялись по дороге
    • Их отбросил редирект. curl отправляет -u, --oauth2-bearer и вручную заданный заголовок Authorization только на тот origin, к которому вы обратились; редирект, который меняет хост, схему или порт, их отбрасывает. Поэтому curl -L -u alice:… http://example.com/api, перенаправленный на https://, приходит без учётных данных и получает 401. fetch() в браузерах тоже отбрасывает Authorization при редиректах на другой origin. Обращайтесь сразу к конечному адресу https://, а --location-trusted используйте, только если доверяете цели редиректа.
    • nginx отбросил заголовок с подчёркиванием. Нестандартный заголовок вроде X_API_KEY по умолчанию выбрасывается (underscores_in_headers off), так что приложение его не видит и отвечает 401. Переименуйте его в X-API-Key.
    • Apache не передал его в PHP. С PHP-FPM или CGI Apache не показывает скриптам заголовок Authorization, пока вы не зададите CGIPassAuth On (Apache 2.4.13 и новее) в блоке <Directory> или в .htaccess.
    • Два слоя претендуют на один и тот же заголовок. В запросе только один заголовок Authorization. Если перед приложением, которое ждёт собственный Bearer-токен, стоит nginx с auth_basic, этот заголовок удовлетворит кого-то одного, но никогда обоих. Защитите приложение списком разрешённых IP или перенесите одни из учётных данных в другой заголовок.

    Кроме того, nginx по умолчанию передаёт учётные данные Basic бэкенду. Если приложение не должно видеть пароль сотрудников, очистите заголовок в этом location:

    nginx · не передавать учётные данные Basic
    location /internal/ {
        auth_basic "Staff only";
        auth_basic_user_file /etc/nginx/.htpasswd;
        proxy_set_header Authorization "";   # пустое значение удаляет заголовок
        proxy_pass http://127.0.0.1:3000;
    }

    proxy_set_header в location заменяет все такие директивы, которые иначе унаследовались бы из блока server, поэтому повторите там свои строки Host и X-Forwarded-*.

  4. 4nginx auth_basic отвергает пользователя

    Лог ошибок называет причину каждого отказа во входе:

    /var/log/nginx/error.log
    user "alice": password mismatch
    user "carol" was not found in "/etc/nginx/.htpasswd"
    no user/password was provided for basic authentication     (уровень info)

    Последняя строка соответствует первой 401, которую получает любой браузер перед тем, как показать окно входа, так что это нормально. Файл паролей создаётся так:

    Debian · Ubuntu
    sudo apt install -y apache2-utils
    sudo htpasswd -c -B /etc/nginx/.htpasswd alice   # -c только для ПЕРВОГО пользователя: он опустошает существующий файл
    sudo htpasswd -B /etc/nginx/.htpasswd bob        # каждый следующий пользователь, без -c
    sudo chown root:www-data /etc/nginx/.htpasswd
    sudo chmod 640 /etc/nginx/.htpasswd
    
    # без apache2-utils: хеш SHA-512 crypt, его nginx тоже принимает
    sudo -v
    printf 'carol:%s\n' "$(openssl passwd -6)" | sudo tee -a /etc/nginx/.htpasswd

    -B сохраняет хеш bcrypt. nginx проверяет такие хеши системной функцией crypt(), а в Debian 12 она понимает и bcrypt, и SHA-512. Мы проверили это на Debian 12 (libcrypt1 4.4.33), вызвав ту же функцию из Python 3.11.2; сам nginx при этом не запускали. Если ваша система bcrypt не умеет, уберите -B, и htpasswd запишет свой стандартный хеш на основе MD5. Затем закройте сайт паролем и откройте без пароля пути, до которых машины должны добираться сами:

    в вашем блоке server
    auth_basic "Staff only";
    auth_basic_user_file /etc/nginx/.htpasswd;
    
    location /.well-known/acme-challenge/ {
        auth_basic off;          # проверка certbot через webroot не умеет вводить пароль
        root /var/www/html;
    }
    применяем
    sudo nginx -t && sudo systemctl reload nginx

    Браузеры кешируют учётные данные Basic на время сессии и после смены пароля продолжают отправлять старые, поэтому проверяйте в приватном окне. Basic-аутентификация лишь кодирует данные в base64, так что используйте её только поверх HTTPS; как это настроить, показывает статья об обратном прокси и SSL (англ.).

  5. 5Браузер сообщает об ошибке CORS, а 401 получил OPTIONS

    Перед кросс-доменным запросом с заголовком Authorization браузер отправляет предварительный запрос OPTIONS (preflight). Preflight никогда не несёт учётных данных и должен получить ответ 2xx. Если ваш сервер требует аутентификацию и для OPTIONS, preflight получает 401, браузер блокирует настоящий запрос, а в консоли видна только ошибка CORS (англ.). Пропускайте OPTIONS без аутентификации:

    nginx · аутентификация для всех методов, кроме OPTIONS
    location /api/ {
        limit_except OPTIONS {
            auth_basic "API";
            auth_basic_user_file /etc/nginx/.htpasswd;
        }
        proxy_pass http://127.0.0.1:3000;    # на preflight приложение отвечает своими заголовками CORS
    }

    В приложении правило то же: middleware CORS должен срабатывать раньше middleware аутентификации.

Что значит 401 от Discord или Telegram?

Для бота 401 почти всегда значит, что токен неверный или был сброшен. На неверный токен бота Telegram отвечает так:

Telegram Bot API
curl -s https://api.telegram.org/bot<TOKEN>/getMe
# {"ok":false,"error_code":401,"description":"Unauthorized"}

Discord ждёт Authorization: Bot <token>, и библиотеки выставляют его за вас. Не давайте боту с мёртвым токеном перезапускаться без паузы, раз за разом: Discord засчитывает ответы 401 в свой лимит в 10 000 недопустимых запросов за 10 минут и затем на время блокирует IP, и один плохой токен превращается в проблему 429 Too Many Requests. Задайте юниту systemd RestartSec=10 или больше и исправьте токен в его файле окружения. Тарифы, на которых бот работает такой службой, перечислены на странице хостинга Discord-ботов.

Как сделать, чтобы ошибки 401 не возвращались?

  • Обращайтесь сразу к конечному адресу https://; никогда не рассчитывайте, что учётные данные донесёт редирект.
  • Храните токены в файле окружения, который может читать только пользователь службы (chmod 600), а не в коде или репозитории.
  • Обновляйте токены до истечения срока и держите часы синхронизированными на каждой машине.
  • В именах собственных заголовков используйте дефисы, никогда не подчёркивания.
  • В скриптах считайте 401 сигналом остановиться и исправить. Повтор с теми же учётными данными лишь добавляет неудачных попыток.
  • Блокируйте адреса, которые подбирают пароли, стандартным jail nginx-http-auth из fail2ban: он читает те же строки лога ошибок, что показаны выше.

Чаще всего auth_basic ставят паролем перед инструментом администрирования, например перед phpMyAdmin (англ.), и все команды из этой статьи без изменений работают на Debian VPS или Ubuntu VPS.

Частые вопросы

Что значит ошибка 401 Unauthorized?

Код HTTP 401 значит, что сервер требует аутентификацию, а запрос пришёл без действительных учётных данных: их не было совсем, они не того вида, истекли или были отвергнуты. В заголовке ответа WWW-Authenticate сервер называет схему, которую ждёт, например Basic или Bearer. Войдите заново или отправьте свежий токен в этом формате.

Чем ошибка 401 отличается от 403?

401 значит, что сервер не знает, кто вы: учётных данных нет, они истекли или неверны, и в заголовке WWW-Authenticate он обязан сказать, как пройти аутентификацию. 403 значит, что сервер понял запрос и отказывает в нём, поэтому повторный вход с той же учётной записью не поможет: нужны другие права или изменение на сервере.

Как узнать, почему запрос получает 401?

Выполните запрос с curl -v. Строки, которые начинаются с «>», покажут, ушёл ли заголовок Authorization на самом деле, а строка «< WWW-Authenticate» покажет, какую схему ждёт сервер, и для токенов Bearer часто ошибку вроде invalid_token. На сервере nginx с auth_basic лог ошибок называет пользователя и пишет «password mismatch» или «was not found».

Читайте также

Все статьи раздела