Делаем два метода аутентификации:
Resource Owner Password Credentials Grant и
Refresh token grant.
SPA отправляет логин/пароль юзера на первый endpoint. В ответ получаем
access_token (например, JWT),
refresh_token и
expires_in. Сохраняем все это добро куда-нибудь, например, в Local Storage. Время жизни JWT-токена лучше ставить небольшое (например, 1 час), потому что отозвать его нельзя. Далее SPA при каждом запросе к API проверяет время жизни токена
expires_in из Local Storage, и когда оно истекает, отправляет запрос на обновление токена (
refresh_token). Все это прозрачно для юзера.
Stateless, по-моему, и проще, и универсальнее. Если потом делать, например, мобильное приложение, API переписывать не придется.
Вся фишка JWT по сути только в том, что не нужно дергать БД при каждом запросе к API. Делать это придется, например, только раз в час при refresh'е токена. Больше никаких существенных преимуществ перед традиционными токенами, хранящимися в БД, нет.
Советую курить именно
официальный RFC по oAuth2, а не всякие блогпосты а-ля "OAuth2 простыми словами". Сам через это прошел. RFC - самый понятный и доходчивый источник знаний.