Аутентификация на developer.spotify.com
Andrey KuznetsovЕсли мы пользовались Spotify в браузере, то зайдя на портал, можем сразу обнаружить себя аутентифицированными.
Первичный authorization request выполняется не с помощью редиректа, а через встройку "невидимого" iframe с подготовленным URL authorization request:


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

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

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

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

Здесь также видим, что:
- Используется public client, при этом получение refresh token все равно предусмотрено
- Токен доступа живет час
Можно проверить, что полученный токен доступа действительно используется в запросах к API.

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

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

В целом подход имеет место быть, однако имеет и свои потенциальные риски, актуальные для случая, когда злоумышленник имеет возможность выполнения своего JS-кода в приложении. Что для данного приложения может быть релевантно в меньшей степени, например.
Хранение токенов в памяти выглядит красиво, но все равно не делает токены полностью недоступными с клиентской части. Для примера, пропатчим метод window.fetch, добавив в него console.log('intercepted request:', args):
const originalFetch = window.fetch;
window.fetch = function(...args) {
console.log('intercepted request:', args);
return originalFetch(...args);
};
И теперь при отправке любого запроса со страницы имеем возможность получить отправляемое значение токена доступа из его заголовков:

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