Назад к блогу

Путь API-токена: от выдачи до аннулирования

Путь API-токена: от выдачи до аннулирования

Разбираем жизненный цикл OAuth 2.0-токена по спецификациям — от момента выдачи до отзыва и проверки на ресурсном сервере. Отдельный акцент на асимметрии этих процессов: выдача всегда идёт через сервер авторизации, тогда как валидация токена может обходиться без обращения к нему. Полезно тем, кто хочет понять, что на самом деле стоит за словами «токен отозван» и почему access- и refresh-токены живут по разным правилам.

Разбираем, как устроен жизненный цикл OAuth 2.0-токена по спецификациям: кто и при каких условиях его выдаёт, как он передаётся в запросе, как проверяется на стороне ресурсного сервера, как обновляется и что на самом деле означает его отзыв. Нетривиальность в том, что «выдать токен» и «отозвать токен» — это не симметричные операции: выдача всегда проходит через сервер авторизации, а проверка на ресурсном сервере может происходить вообще без обращения к нему.

Роли и появление токена у клиента

В потоке участвуют четыре роли. resource owner — тот, чьи данные защищаются. resource server — тот, кто в итоге обслуживает запросы. client — тот, кто получает токен. authorization server — тот, кто токен выпускает.

Порядок шагов такой. Сначала клиент запрашивает авторизацию у resource owner — напрямую или, что предпочтительнее, косвенно через authorization server как посредника. В ответ клиент получает authorization grant. Затем клиент запрашивает access token, аутентифицируясь на authorization server и предъявляя этот grant: именно grant подтверждает, что владелец ресурса дал согласие, поэтому без него сервер не выпустит токен. Сервер аутентифицирует клиента, проверяет grant и, если тот действителен, выдаёт access token.

Токен появляется у клиента в теле HTTP-ответа со статусом 200 (OK). Обязательных параметров два: access_token (сам токен) и token_type (тип токена, значение регистронезависимо). Параметр expires_in помечен как RECOMMENDED — это время жизни токена в секундах. refresh_token и scope помечены как OPTIONAL. Все параметры передаются в теле ответа в формате application/json.

Срок жизни и различие access/refresh

expires_in — это время жизни токена доступа в секундах. Значение «3600» означает, что токен истечёт через один час с момента формирования ответа. Если параметр опущен, сервер авторизации должен сообщить время истечения иным способом или задокументировать значение по умолчанию. В примерах успешного ответа он передаётся как "expires_in":3600 в теле JSON.

Access token и refresh token различаются по назначению. Access token — учётные данные для доступа к защищённым ресурсам: строка, представляющая выданную клиенту авторизацию. Refresh token — учётные данные для получения access token: он выдаётся клиенту сервером авторизации и используется, когда текущий access token стал недействительным или истёк, либо для получения дополнительных access token с той же или более узкой областью.

Ключевое различие в том, кому их предъявляют. Access token клиент предъявляет resource server при запросе защищённого ресурса. Refresh token, в отличие от него, предназначен только для использования с authorization server и никогда не отправляется resource server. Кроме того, refresh token привязан к клиенту, которому был выдан, и authorization server обязан проверять эту привязку.

Ошибки при запросе токена

При неудачном запросе токена сервер авторизации отвечает кодом HTTP 400 (Bad Request), если не указано иное, и включает обязательный параметр error — одиночный ASCII-код. Возможные значения:

  • invalid_request — запрос неполный, содержит неподдерживаемое значение параметра, повторяет параметр, включает несколько учётных данных, использует более одного механизма аутентификации клиента или иным образом искажён;
  • invalid_client — аутентификация клиента не удалась;
  • invalid_grant — предоставленное разрешение авторизации или refresh-токен недействителен, истёк, отозван, не совпадает с redirect URI или выдан другому клиенту;
  • unauthorized_client — аутентифицированный клиент не имеет права использовать этот тип разрешения;
  • unsupported_grant_type — тип разрешения не поддерживается сервером;
  • invalid_scope — запрошенная область недействительна, неизвестна, искажена или превышает предоставленную владельцем ресурса.

Дополнительно могут присутствовать необязательные error_description (читаемый ASCII-текст с пояснением) и error_uri (URI веб-страницы с информацией об ошибке). Параметры включаются в тело ответа с медиатипом application/json, например:

{ "error":"invalid_request" }

Передача токена в запросе

Bearer-токен — это bearer-токен. Клиент передаёт его в заголовке запроса Authorization, используя схему аутентификации Bearer:

Authorization: Bearer mF_9.B5f-4.1JqM

Синтаксис заголовка для этой схемы следует использованию схемы Basic, а учётные данные задаются как credentials = "Bearer" 1*SP b64token, где b64token состоит из одного или более символов ALPHA, DIGIT, -, ., _, ~, +, / и завершается нулём или более символов =. Клиенты SHOULD делать аутентифицированные запросы с bearer-токеном через этот заголовок, а серверы ресурсов MUST поддерживать этот метод. Имена и значения параметров протокола чувствительны к регистру, если не указано иное.

Есть два альтернативных способа. Первый — form-encoded тело запроса: клиент обязан использовать Content-Type: application/x-www-form-urlencoded, тело должно быть single-part и состоять только из ASCII-символов, а метод запроса должен иметь определённую семантику тела — в частности, GET использовать нельзя. Этот способ следует применять только там, где браузеры не имеют доступа к заголовку Authorization, и серверы могут его поддерживать по желанию. Второй — URI query parameter access_token, но это не рекомендуется: браузеры, веб-серверы и другое ПО могут недостаточно защищать URL в истории, логах и других структурах, откуда злоумышленник может украсть токен. В обоих случаях клиент не должен использовать более одного способа передачи токена в одном запросе.

Почему bearer-токен требует TLS

Bearer-токен — это строка, представляющая выданное клиенту право доступа, и любой, кто ею владеет, может получить доступ к защищённым ресурсам. Поэтому для защиты от раскрытия токена должна применяться конфиденциальность через TLS с набором шифров, обеспечивающим конфиденциальность и целостность. Это касается как взаимодействия клиента с сервером авторизации, так и взаимодействия клиента с ресурсным сервером. TLS объявлен обязательным к реализации и использованию, а клиенты должны всегда применять TLS (https) или эквивалентную транспортную защиту при запросах с bearer-токенами.

Если клиент не может наблюдать содержимое токена, то в дополнение к TLS должно применяться шифрование токена. Если TLS-соединение завершается на фронтенд-сервере до бэкенда, должны применяться достаточные меры для конфиденциальности токена между фронтендом и бэкендом — шифрование токена одна из таких мер. Клиент обязан проверять цепочку сертификатов TLS при запросах к защищённым ресурсам, включая проверку списка отзыва сертификатов (CRL).

Проверка токена на ресурсном сервере

Ресурсный сервер валидирует токен и, если он валиден, обслуживает запрос. Что означает «валиден», прямо определяется через понятие активности при интроспекции: значение true для свойства active обычно означает, что токен выдан этим сервером авторизации, не отозван владельцем ресурса и находится в пределах временного окна действия — после выдачи и до истечения. Временное окно задаётся тремя метками: exp (когда токен истечёт), iat (когда он был выдан) и nbf (когда его нельзя использовать раньше).

Отзыв выполняется через запрос к эндпоинту отзыва токенов — отдельной точке на сервере авторизации, куда клиент отправляет токен, чтобы сделать его недействительным; в отличие от эндпоинта интроспекции, который лишь сообщает состояние токена, эндпоинт отзыва изменяет это состояние. Запрос на отзыв делает недействительным сам токен и, если применимо, другие токены на основе того же authorization grant и сам grant. Scope — это список разделённых пробелами регистрозависимых строк, определяющих диапазон доступа.

Интроспекция

Запрос к эндпоинту интроспекции выполняется методом HTTP POST с параметрами в формате application/x-www-form-urlencoded. Обязательный параметр token содержит строковое значение проверяемого токена. Необязательный token_type_hint подсказывает тип токена для оптимизации поиска; если сервер не находит токен по подсказке, он должен расширить поиск на все поддерживаемые типы.

Ответ возвращается как JSON-объект в формате application/json. Обязательное поле active — булев признак того, что токен сейчас действителен. Остальные поля необязательны: scope (области доступа через пробел), client_id (идентификатор клиента, запросившего токен), username (читаемый идентификатор владельца ресурса), token_type, exp/iat/nbf (метки времени в секундах), sub (субъект токена), aud (аудитория), iss (издатель), jti (идентификатор токена).

Если вызов авторизован, но токен неактивен, не существует или не разрешён к интроспекции, сервер обязан вернуть ответ с active=false и не должен включать дополнительную информацию о неактивном токене. Реализации могут добавлять собственные поля верхнего уровня, а сервер может по-разному отвечать разным защищённым ресурсам, например ограничивая набор возвращаемых scope.

Что означает active=false

active — обязательный булев признак того, является ли предъявленный токен в данный момент действующим; значение false означает, что токен не активен. Решение о том, что считать активным, принимает сервер авторизации, который обязан выполнить все применимые проверки состояния токена: истечение, начало срока действия, отзыв, подпись, допустимость использования на данном ресурсном сервере. Поскольку ресурсный сервер полагается на ответ интроспекции при принятии решений об авторизации, при active=false он не должен считать токен действительным. Ответ для неактивного токена не должен содержать дополнительных claims помимо обязательного active со значением false.

Ошибки интроспекции

При недействительных учётных данных клиента эндпоинт интроспекции возвращает HTTP 401 (Unauthorized) в соответствии с разделом 5.2 OAuth 2.0. Там описаны invalid_request (запрос с отсутствующим обязательным параметром, неподдерживаемым значением, повторяющимся параметром, множественными учётными данными или иным образом искажённый) и invalid_client (сбой аутентификации клиента). Формат ответа — параметры с обязательным error, содержащим один ASCII-код. Код insufficient_scope определён для ответов ресурсного сервера, а не для эндпоинта интроспекции.

Что видит клиент при ошибке авторизации

Если запрос к защищённому ресурсу не содержит учётных данных или токена доступа, дающего доступ, ресурсный сервер обязан включить в ответ заголовок WWW-Authenticate; он может включать его и при других условиях. Все задачи используют схему Bearer, за которой следует один или несколько параметров. Атрибут realm может быть включён для указания области защиты и не должен появляться более одного раза. Атрибут scope — разделённый пробелами список регистрозависимых значений, указывающий требуемую область действия токена.

При ошибке сервер отвечает подходящим HTTP-статусом (обычно 400, 401, 403 или 405) и включает один из кодов:

  • invalid_request — запрос с недостающим, неподдерживаемым или повторяющимся параметром, либо использующий более одного способа передачи токена; обычно HTTP 400;
  • invalid_token — токен истёк, отозван, искажён или иным образом недействителен; обычно HTTP 401, и клиент может запросить новый access token и повторить запрос;
  • insufficient_scope — требуются более высокие привилегии, чем даёт токен; обычно HTTP 403, при этом может включаться атрибут scope.

Если запрос вообще не содержит сведений аутентификации, сервер не должен включать код ошибки или иную информацию об ошибке. Пример ответа с истёкшим токеном:

WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"

Обновление токена

При получении insufficient_scope клиент, увидев в ответе необходимый scope, может запросить новый токен доступа с достаточной областью. Для обновления клиент делает запрос на token endpoint методом POST с телом в формате application/x-www-form-urlencoded и кодировкой UTF-8. Обязательный grant_type должен иметь значение refresh_token; обязательный refresh_token — это выданный клиенту refresh-токен; необязательный scope задаёт запрашиваемую область. Запрашиваемый scope не должен включать области, изначально не предоставленные владельцем ресурса, а при отсутствии трактуется как равный изначально предоставленной области. Если клиент конфиденциальный или ему выданы клиентские учётные данные, он должен аутентифицироваться на сервере авторизации.

В ответе возвращается JSON с обязательными access_token и token_type, рекомендованным expires_in и необязательным refresh_token. Если выданный scope отличается от запрошенного, сервер авторизации обязан включить параметр ответа scope, чтобы сообщить клиенту фактически предоставленную область. Сервер обязан включать заголовки Cache-Control: no-store и Pragma: no-cache в ответы, содержащие токены, а клиент должен игнорировать нераспознанные имена значений в ответе.

При ошибке обновления сервер отвечает HTTP 400 и включает параметр error. invalid_client означает, что аутентификация клиента не удалась; при этом сервер может вернуть HTTP 401, а если клиент пытался аутентифицироваться через заголовок Authorization, сервер обязан ответить HTTP 401 и добавить заголовок WWW-Authenticate. invalid_grant означает, что предоставленный грант или refresh-токен недействителен, истёк, отозван, не совпадает с redirect URI или выдан другому клиенту. invalid_scope означает, что запрошенная область недействительна, неизвестна, имеет неверный формат или превышает предоставленную владельцем ресурса.

Аннулирование токена

Клиент формирует запрос на отзыв, помещая параметры в тело HTTP-запроса в формате application/x-www-form-urlencoded. Обязательный token — это сам токен, который клиент хочет отозвать; необязательный token_type_hint подсказывает тип токена и принимает значения access_token или refresh_token. Клиент также включает свои учётные данные для аутентификации, например через заголовок Authorization: Basic.

Сервер сначала проверяет учётные данные клиента (для конфиденциального клиента) и что токен был выдан именно этому клиенту, затем немедленно делает токен недействительным. При успешном отзыве или если клиент прислал недействительный токен сервер отвечает кодом HTTP 200, а тело ответа клиентом игнорируется.

Почему 200 даже для недействительного токена

RFC 7009 предписывает отвечать HTTP 200, если токен был успешно отозван или если клиент прислал недействительный токен, потому что клиент не может разумно обработать такую ошибку, а цель запроса — сделать конкретный токен недействительным — уже достигнута. Если же проверка учётных данных клиента или принадлежности токена не проходит, запрос отклоняется и клиент уведомляется об ошибке.

Определён код unsupported_token_type: сервер не поддерживает отзыв предъявленного типа токена — то есть клиент пытался отозвать access token на сервере, не поддерживающем эту возможность. Если сервер отвечает HTTP 503, клиент должен считать, что токен всё ещё существует, и может повторить запрос после разумной задержки; сервер может включить заголовок Retry-After. Неверное значение token_type_hint игнорируется сервером и не влияет на ответ об отзыве.

Что происходит после отзыва

Запрос на отзыв делает недействительным сам токен, а при наличии — и другие токены на том же гранте, и сам грант. Что происходит с последующими запросами, зависит от вида access-токена. Если токен — это handle, ресурсный сервер при каждом предъявлении обращается к серверу авторизации, и тот может отозвать ранее выданный токен. Если токен самодостаточный, для немедленного отзыва может использоваться нестандартизированное backend-взаимодействие между сервером авторизации и ресурсным сервером, либо применяются короткоживущие токены.

При запросе к защищённому ресурсу с отозванным токеном ресурсный сервер отвечает ошибкой invalid_token и статусом HTTP 401. Ответ интроспекции после отзыва должен вернуть active=false, поскольку отозванный токен не является активным. При интроспекции сервер авторизации обязан проверять, не был ли токен отозван.

Безопасность и компромиссы

RFC 6750 перечисляет две угрозы. token redirect и token replay.

Для защиты от раскрытия токена через канал связи требуется TLS с набором шифров, обеспечивающим конфиденциальность и целостность, а клиент обязан проверять цепочку сертификатов TLS, включая CRL. Bearer-токены запрещено хранить в cookie, которые могут передаваться в открытом виде, поскольку cookie обычно передаются в открытом виде и содержимое в них подвержено раскрытию. Токены не следует передавать в URL страниц, так как браузеры, веб-серверы и другое ПО могут недостаточно защищать URL в истории, журналах и других структурах, откуда злоумышленники могут их украсть.

Против захвата и повторного использования токена рекомендуется ограничивать время его жизни, например помещая поле времени действия в защищённую часть токена; короткоживущие токены (один час или меньше) снижают последствия утечки. Также рекомендуется выдавать токены с ограничением области применения.

Что из этого следует на практике

Отзыв токена не «стирает» уже утёкшее значение: отзыв — это запрос к серверу авторизации, а не удаление строки у всех, кто её видел. Если access token самодостаточный и нет backend-взаимодействия между сервером авторизации и ресурсным сервером, отзыв не даёт немедленного эффекта — приходится полагаться на короткое время жизни токена или на дополнительные меры. Альтернатива — короткоживущие access tokens, обновляемые refresh-токенами, что позволяет серверу авторизации ограничить время, в течение которого отозванные access tokens ещё могут использоваться.

Из этого вытекает несколько практических следствий. Реакция на инцидент ограничена архитектурой проверки: если ресурсный сервер валидирует токен без обращения к серверу авторизации, немедленный отзыв невозможен без нестандартизированного взаимодействия. Refresh token нельзя предъявлять ресурсному серверу — он предназначен только для сервера авторизации, и сервер авторизации обязан проверять его привязку к клиенту. Запрашивая новый токен с расширенным scope, нельзя выйти за пределы изначально предоставленной области. И наконец, любой способ передачи токена, кроме заголовка Authorization со схемой Bearer, несёт дополнительные риски: form-encoded тело ограничено по методу и кодировке, а URI query parameter прямо не рекомендуется из-за утечек через историю и логи.

Источники

Похожее