Аутентификация на developer.spotify.com

Аутентификация на developer.spotify.com

Andrey Kuznetsov

Если мы пользовались Spotify в браузере, то зайдя на портал, можем сразу обнаружить себя аутентифицированными.

Первичный authorization request выполняется не с помощью редиректа, а через встройку "невидимого" iframe с подготовленным URL authorization request:

[full image]

[full image]

Сами authorization request и response при этом выглядят так:

[full image]

Можно увидеть, что используется response_mode=web_message, в результате которого authorization response не возвращается в параметре для redirect_uri, как для response_mode=query, а вместо этого authorization server возвращает HTML-страницу, на которой реализован код для отправки authorization response родительскому окну через postMessage. При этом видно, что выполняется статическая проверка origin, чтобы не отправить такое сообщение окну с неподходящим origin.

Соответственно родительское по отношению ко фрейму окно получит подобное сообщение:

[full image]

Использование prompt=none позволяет не выполнять редирект в iframe в случае, если пользователь неаутентифицирован на стороне authorization server, а получить сразу ошибку вместо этого:

[full image]

Чтобы cross-origin содержимое фрейма могло быть загружено, причем не где попало, а только на нужном ресурсе, на стороне authorization server (accounts.spotify.com) используется директива CSP frame-ancestors https://developer.spotify.com;

При этом на стороне developer.spotify.com дополняющей директивы frame-src, разрешающий фрейминг содержимого только с определенных origin, не наблюдается.

Далее с той же клиентской части выполняется и token request:

[full image]

Здесь также видим, что:

  1. Используется public client, при этом получение refresh token все равно предусмотрено
  2. Токен доступа живет час

Можно проверить, что полученный токен доступа действительно используется в запросах к API.

[full image]

Однако интересно теперь, где токен хранится на клиенте. В локальных хранилищах пусто, в куках тоже, зарегистрированных service workers нет. Где еще может храниться? Логично, что, в памяти. Проверить это можно, используя snapshot памяти в Chrome DevTools:

[full image]

Примечательно, что при этом значение refresh-токена найти не удалось. Зато можно проверить, используется ли ротация refresh-токенов, попробовав руками получить дважды новый access token с ним. При повторной попытке видим, что получение невозможно:

[full image]


В целом подход имеет место быть, однако имеет и свои потенциальные риски, актуальные для случая, когда злоумышленник имеет возможность выполнения своего JS-кода в приложении. Что для данного приложения может быть релевантно в меньшей степени, например.

Хранение токенов в памяти выглядит красиво, но все равно не делает токены полностью недоступными с клиентской части. Для примера, пропатчим метод window.fetch, добавив в него console.log('intercepted request:', args):

const originalFetch = window.fetch;

window.fetch = function(...args) {
  console.log('intercepted request:', args);
  return originalFetch(...args);
};

И теперь при отправке любого запроса со страницы имеем возможность получить отправляемое значение токена доступа из его заголовков:

[full image]

Authorization request в iframe, реализуемый незаметно для пользователя, в сочетании с использованием postMessage позволяет атакующему инициировать новый OAuth flow и получить таким образом authorization code. Главное - иметь возможность выполнения кода на том же origin, тогда authorization response можно будет успешно получить в postMessage-сообщении.

[full image]

Report Page