Índice
- Source - Backend
- 1 - Backend - Conhecendo o Backend
- 2 - Backend - Endpoint de Login
- 3 - Backend - Endpoint de Refresh
- 4 - Backend - Endpoint de Logout
- 5 - Frontend - Entendendo as Vulnerabilidades
- 6 - Frontend - Protegendo um App SPA
- 7 - Frontend - Protegendo um App SSR
1 - Backend - Conhecendo o Backend
Antes de começamos a proteger a nossa aplicação, precisamos conhecer o Backend com o qual iremos conectar, assim como descobrir quais escolhas o mesmo adotou, uma vez que existe diferentes formas de criar e gerenciar os refresh tokens.
Para este artigo, estarei usando como base uma API feita com NestJS que usa um esquema de Refresh definido por mim, porém que cobre a maioria dos modelos e/ou estrategias de refresh token existentes.
O código fonte da API por ser encontrado no seguinte repositorio.:
https://github.com/TobyMosque/ws-auth-samples-backend/tree/refresh-after-spa
porém, não se preocupe com o código em si, mas com o comportamento da API, primeiro devemos analisar todos os endpoints utilizados para a autenticação.
2 - Backend - Endpoint de Login
O primeiro endpoint que iremos visitar é o de login:
2.1 - Parâmetros do Login
Note que além do body
com o username
e o password
, temos dois parâmetros que são passados na query string
, estes parâmetros são utilizados para instruir o backend de como iremos gerenciar o refresh token.
rotation: valor boleano (true/false) que define se iremos usar rotação de tokens ou não
- true: o
refresh_token
poderá ser usado apenas uma vez, então a cada/refresh
a API irá retornar um novorefresh_token
além de umaccess_token
renovado. - false: o
refresh_token
poderá ser utilizado multiplas vezes.
flow: enumerador que define quem irá gerenciar o refresh_token
- server: a API não irá devolver o
refresh_token
ao cliente, ficando a API responsável pela segurança dorefresh_token
, nesta caso orefresh_token
é persistido em umcookie
seguro. - client: a API irá devolver o
refresh_token
ao cliente, neste caso a API não irá persistir e/ou gerenciar orefresh_token
, ficando a cargo do cliente a segurança do mesmo.
2.2 - Analisando o Login
Agora vamos fazer algumas chamadas a API para que possamos observar os diferentes comportamentos.
Lembrando que pode ser usada qual quer combinação de flow e rotation, porém irei utilizar apenas duas combinações nos exemplos.
2.2.1 - Primeiro Cenário - flow=server e rotation=false
Veja, que a API retornou a seguinte resposta:
{ "accessToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTgwY2E2My0yYjUyLTRlYjAtOTJlNi1mMWNlMzYxM2IxNjYiLCJuYW1lIjoiVG9iaWFzIE1lc3F1aXRhIiwiZW1haWwiOiJtZUB0b2JpYXNtZXNxdWl0YS5kZXYiLCJyb2xlcyI6WyJkZXZlbG9wZXIiLCJhZG1pbiIsInVzZXIiXSwiaWF0IjoxNjc4MDQ4NjQ0LCJleHAiOjE2NzgwNDg2NzQsImF1ZCI6Imh0dHBzOi8vand0LXNhbXBsZS50b2JpYXNtZXNxdWl0YS5kZXYiLCJpc3MiOiJodHRwczovL2p3dC1zYW1wbGUudG9iaWFzbWVzcXVpdGEuZGV2IiwianRpIjoiODBmMDQxMDctMGE4MS00ZWNhLWE1ODItNWM3NzViNDQ4MWJmIn0.GIAeo5sfrA5ksKp5msAJCAxLo4ivHxoHrTz6j9JBWyVgg3BmHN0veF24V27PG_K8yqjMiCKRONuNwkdZHENdQg" }
então, podemos fazer o decode do token em https://jwt.io
[{ "alg": "HS512", "typ": "JWT" }, { "sub": "0180ca63-2b52-4eb0-92e6-f1ce3613b166", "name": "Tobias Mesquita", "email": "me@tobiasmesquita.dev", "roles": [ "developer", "admin", "user" ], "iat": 1678048644, "exp": 1678048674, "aud": "https://jwt-sample.tobiasmesquita.dev", "iss": "https://jwt-sample.tobiasmesquita.dev", "jti": "80f04107-0a81-4eca-a582-5c775b4481bf" }]
Se analisamos o iat e o exp, veremos que o token tem validade de 30 segundos, ou seja, se trata de um token de curta duração:
{ iat: '05/03/2023 17:37:24' exp: '05/03/2023 17:37:54' }
Também podemos verificar que a API não retornou o refresh_token
, porém o salvou como um cookie
.
Set-Cookie: REFRESH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NzgwNDg2NDQsImV4cCI6MTY3ODEzNTA0NCwiYXVkIjoiaHR0cHM6Ly9qd3Qtc2FtcGxlLnRvYmlhc21lc3F1aXRhLmRldiIsImlzcyI6Imh0dHBzOi8vand0LXNhbXBsZS50b2JpYXNtZXNxdWl0YS5kZXYiLCJqdGkiOiI4MGYwNDEwNy0wYTgxLTRlY2EtYTU4Mi01Yzc3NWI0NDgxYmYifQ.GW3o37wLXY6eg4_ns9xuDKb8UOwlw1SF2CHdtWaXdXg; Path=api/auth/refresh; Expires=Mon, 06 Mar 2023 20:37:24 GMT; HttpOnly; Secure; SameSite=Lax
E no caso desta API, o refresh_token
também é token assinado, assim como o access_token
, então também podemos fazer o decode do mesmo em https://jwt.io.
Apesar de termos usado um token assinado para o
refresh_token
, é bastante comum a utilização de objetos, sendo que estes objetos podem mudar drasticamente de uma API para outra, porém a finalidade e a utilização deles continua a mesma.Segue um exemplo de
refresh_token
como objeto{ grant_type: 'refresh_token', refresh_token: `${refreshToken}`, client_id: `${clientId}`, client_secret: `${clientSecret}`, scope: '', }
[{ "alg": "HS256", "typ": "JWT" }, { "iat": 1678048644, "exp": 1678135044, "aud": "https://jwt-sample.tobiasmesquita.dev", "iss": "https://jwt-sample.tobiasmesquita.dev", "jti": "80f04107-0a81-4eca-a582-5c775b4481bf" }]
E novamente poderemos analisar as datas de criação e expiração, e veremos que se trata de um token de longa duração, com validade de 24 horas:
{ iat: '05/03/2023 17:37:24' exp: '06/03/2023 17:37:24' }
2.2.2 - Segundo Cenario - flow=client e rotation=true
Veja, que a API retornou a seguinte resposta:
{ "accessToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTgwY2E2My0yYjUyLTRlYjAtOTJlNi1mMWNlMzYxM2IxNjYiLCJuYW1lIjoiVG9iaWFzIE1lc3F1aXRhIiwiZW1haWwiOiJtZUB0b2JpYXNtZXNxdWl0YS5kZXYiLCJyb2xlcyI6WyJkZXZlbG9wZXIiLCJhZG1pbiIsInVzZXIiXSwiaWF0IjoxNjc4MDQ5MDcwLCJleHAiOjE2NzgwNDkxMDAsImF1ZCI6Imh0dHBzOi8vand0LXNhbXBsZS50b2JpYXNtZXNxdWl0YS5kZXYiLCJpc3MiOiJodHRwczovL2p3dC1zYW1wbGUudG9iaWFzbWVzcXVpdGEuZGV2IiwianRpIjoiMTViYzJlMjctMjkxYy00MzE5LTgxNWYtYTc1NDJkNWZmZTAyIn0.Xz--Ep6eTUGl_YG6VejwJFqaR-WKCPzFDywvT57KnU9eZ3lj9Con6GDZK5EEjFDTdBNvcfc3xsI5xtnuITLIOg", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZWZyZXNoIjoiZjQzZmE0MDMtNTE3Yi00YmEzLTljNTktNDRjYzAxZGMzOWVjIiwiaWF0IjoxNjc4MDQ5MDcwLCJleHAiOjE2NzgxMzU0NzAsImF1ZCI6Imh0dHBzOi8vand0LXNhbXBsZS50b2JpYXNtZXNxdWl0YS5kZXYiLCJpc3MiOiJodHRwczovL2p3dC1zYW1wbGUudG9iaWFzbWVzcXVpdGEuZGV2IiwianRpIjoiMTViYzJlMjctMjkxYy00MzE5LTgxNWYtYTc1NDJkNWZmZTAyIn0.WtFmPuhQMrDvxkxoyc6rAFtr4wXErnMhnqRa4_VZ6Ts" }
Desta vez, a API retornou o refresh_token
como parte da resposta (body) e não como um cookie
, ficando a critério da API decidir qual é a melhor estratégia para proteger o refresh_token
.
Como o access_token
, terá o mesmo formato independente do flow
e do rotation
que forem passados, não é necessário decompor ele novamente, porém o refresh_token
terá uma claim a mais.
[{ "alg": "HS256", "typ": "JWT" }, { "refresh": "f43fa403-517b-4ba3-9c59-44cc01dc39ec", "iat": 1678049070, "exp": 1678135470, "aud": "https://jwt-sample.tobiasmesquita.dev", "iss": "https://jwt-sample.tobiasmesquita.dev", "jti": "15bc2e27-291c-4319-815f-a7542d5ffe02" }]
A API irá utilizar a claim refresh
para identificar que o refresh_token
só pode ser utilizado uma vez, e se o mesmo já foi utilizado.
3 - Backend - Endpoint de Refresh
Agora que o login
já foi bem dissecado, porém voltar a nossa atenção para o refresh
3.1 - Parâmetros do Refresh
Assim como no login, temos que passar alguns argumentos na query string
, porém devemos passar apenas o flow, que deve ser igual ao que passamos durante o login
.
flow: enumerador que define como o refresh_token
será passado para a API.
- server: a API escreveu um cookie, então a API deve ler deste cookie.
- client: a aplicação Cliente deve passar o
refresh_token
no headerRefreshToken
.
3.2 - Analisando o Refresh
Agora, vamos dá continuidade aos cenários apresentados no login.
3.2.1 - Primeiro Cenário - flow=server e rotation=false
Neste caso, o refresh_token
será lido do cookie
, como não instruímos a API para utilizar o rotation
do refresh_token
, um novo refresh_token
não foi gerado, e o cookie
não será sobescrito.
3.2.2 - Segundo Cenário - flow=server e rotation=false
Neste exemplo, como o refresh_token
foi criado usando o flow=client
e o rotation=false
, ele precisou ser enviado no header
, assim como ele foi invalidado e um novo refresh_token
, este estando disponível da resposta (body) da requisição feita à API.
Lembrando que, o token antigo foi invalidado, não podendo ser utilizado novamente.
4 - Backend - Endpoint de Logout
E por fim temos o endpoint /logout
:
Neste caso só há uma coisa a ser dita, caso o flow=server
, estamos instruindo a API a apagar o cookie gerenciado pela API.
Lembrando que o objeto desta API é disponibilizar a maior quantidade de fluxos e configurações possíveis, para que possamos aplicar os mesmos ao nosso frontend.
5 - Frontend - Entendendo as Vulnerabilidades
Quando falamos em proteger uma aplicação, temos de pensar nas principais vulnerabilidades que podem ocorrer ao se optar por utilizar tokens JWT para realizar a Autenticação e/ou Autorização.
Neste ponto, as duas principais vulnerabilidades são a XSS (Cross-Site Scripting) ou a CSRF (Cross-Site Request Forgery).
- XSS: O ataque se dá pela injeção de um script que será capaz de ler o
LocalStorage
,SessionStorage
,Cookies
não seguros, ou qual quer outra informação que esteja disponível globalmente. Evitamos este problema ao não disponibilizar dados sensiveis globalmente. - CSRF: O cookie com o Token JWT é enviado para outro servidor, podendo assim ser lido por uma aplicação maliciosa. Evitamos este problema ao marcar o nosso cookie com
HttpOnly; Secure; SameSite=Lax
.
6 - Frontend - Protegendo um App SPA
Agora vamos falar sobre como proteger uma Aplicação SPA, porém isto também se aplica à aplicações PWA, Cordova, Capacitor e Electron. Ou seja, aplicações cujo o build possui apenas arquivos estáticos, não contando com a possibilidade de possuir um servidor/backend proprio.
Neste caso, para mantemos a aplicação segura, teremos de contar com uma API/Backend que disponibilize o flow=server
ou o rotation=true
.
6.1 - Realizando o Refresh
Mas vale ressaltar que independente do fluxo que iremos adotar, o boot do axios irá ser o mesmo, sendo que todos os ajustes serão feitos apenas no composable responsável pela autenticação.
No boot do Axios/API
iremos injetar o Access Token
no header e realizar o refresh
caso o Access Token
expire.
src/boot/axios.ts
import { boot } from 'quasar/wrappers'; import axios from 'axios'; import { InjectionKey } from 'vue'; import { useAuth } from 'src/composables/auth'; export const apiKey: InjectionKey<AxiosInstance> = Symbol('api-key'); export default boot(async ({ app, store, router }) => { const url = process.env.API_URL; const api = axios.create({ baseURL: url }); const { token, refresh } = useAuth(api); api.interceptors.request.use( function (config) { if (token.value) { config.headers ||= {} config.headers.Authorization = `Bearer ${token.value}`; } return config; }, function (error) { return Promise.reject(error); } ); api.interceptors.response.use( function (response) { return response; }, async function (error) { const res = error.response; switch (res.status) { case 401: if (token.value) { await refresh() if (token.value) { return api.request(error.config); } } break; } return Promise.reject(error); } ); await refresh(); })
Agora algumas explicações, como podemos ver nos destaques na imagem abaixo, o token é injetado no request
, e caso haja um erro do tipo 401
no response
, o refresh
é acionado e a requisição é refeita.
6.2 - Integrando a uma API com flow=server
Ao optamos por confiamos a segurança do Access Token
à API, e caso a API persista o Refresh Token
em um Cookie Seguro
, a aplicação estará completamente imune a ataques do tipo XSS
e/ou CSRF
.
Agora irei mostrar um exemplo de implementação com o flow=server
src/composables/auth.ts
import { ref, computed } from 'vue' import jwtDecode from 'jwt-decode'; import type { AxiosInstance } from 'axios' import type { JwtPayload } from 'jwt-decode'; type Payload = JwtPayload & { roles: string[] } interface LoginRequest { username: string; password: string; } const token = ref('') function useAuth (api: AxiosInstance) { const payload = computed(() => { if (!token.value) { return null; } return jwtDecode(token.value) as Payload; }) async function login (req: LoginRequest) { const { data } = await api.post('/auth/login?flow=server'); token.value = data.accessToken; } async function refresh (req: LoginRequest) { try { const { data } = await api.get('/auth/refresh?flow=server'); token.value = data.accessToken; } catch (error) { const res = error.response; if (res.status === 401) { token.value = ''; } } } async function logout (req: LoginRequest) { await api.delete('/auth/logout?flow=server'); token.value = ''; } return { token, payload, login, refresh, logout } }
Note que no código acima estamos armazenando o token em memoria, assim como disponibilizamos os métodos de login, refresh e logout. Vale salientar que refresh_token
não é accessível pelo frontend.
6.3 - Integrando a uma API com rotation=true
No caso da API não se responsabilizar pelo Refresh Token
, ou de termos algo que esteja a bloquear o Cookie Seguro
da API, teremos de persistir o Refresh Token
no Local Storage, o que irá abrir a nossa aplicação à um ataque do tipo XSS
.
Para mitigamos este problema, devemos adotar o rotation=true
, de forma que o token será invalidado a cada uso, ou seja, em 99% dos casos, o Refresh Token
será invalidado junto ao Access Token
, o que limita o Ataque do tipo XSS
e qual quer dano que venha a ser gerado por ele.
Agora iremos abordar o fluxo com flow=client com rotation=true
src/composables/auth.ts
import { ref, computed } from 'vue' import { useLocalStorage } from '@vueuse/core' import jwtDecode from 'jwt-decode'; import type { AxiosInstance } from 'axios' import type { JwtPayload } from 'jwt-decode'; type Payload = JwtPayload & { roles: string[] } interface LoginRequest { username: string; password: string; } const token = ref('') const refresh = useLocalStorage('refresh-token', '') function useAuth (api: AxiosInstance) { async function login (req: LoginRequest) { const { data } = await api.post('/auth/login?flow=client&rotation=true'); token.value = data.accessToken; refresh.value = data.refreshToken; } async function refresh (req: LoginRequest) { try { const { data } = await api.get('/auth/refresh?flow=client', { headers: { RefreshToken: refresh.value } }); token.value = data.accessToken; refresh.value = data.refreshToken; } catch (error) { const res = error.response; if (res.status === 401) { token.value = ''; refresh.value = ''; } } } async function logout (req: LoginRequest) { await api.delete('/auth/logout?flow=client'); token.value = ''; refresh.value = ''; } const payload = computed(() => { if (!token.value) { return null; } return jwtDecode(token.value) as Payload; }) return { token, payload, login, refresh, logout } }
Neste caso, o refresh token é gerenciado pelo frontend, e por isto podemos ver que o mesmo é lido, escrito e persistido.
7 - Frontend - Protegendo um App SSR
Agora que concluímos as explicações sobre aplicações SPA, vamos falar sobre SSR, e neste caso o refresh_token
deve ser compartilhado entre o server-side
e o client-side
, e por isto não podemos usar o flow=server
e seremos forçados a armazenar o Refresh Token
em um Cookie
gerenciado pelo nosso Frontend
.
De toda forma, temos duas opções, uma usando rotation=true
, onde o armazenando no refresh_token
será no client-side
. Caso tenhamos que usar o rotation=false
, teremos que armazenar no server-side
e usar um ssrMiddleware
para criar uma API
intermediária entre o server-side
e o client-side
.
7.1 - Integrando a uma API com rotation=true
No caso do rotation=true
a implementação é a mesma que usamos no SPA
mode, mas iremos usar um CookieStorage
ao invés do LocalStorage
.
src/composables/auth.ts
import { ref, computed, Ref } from 'vue' import { useStorage, StorageLike } from '@vueuse/core' import jwtDecode from 'jwt-decode'; import { Cookies } from 'quasar'; import type { AxiosInstance } from 'axios' import type { JwtPayload } from 'jwt-decode'; type Payload = JwtPayload & { roles: string[] } interface LoginRequest { username: string; password: string; } const token = ref('') let refresh: Ref<string>; function useAuth (api: AxiosInstance, cookies: Cookies) { if (!refresh) { const opts = { path: '/', sameSite: 'Lax', secure: true } const storage: StorageLike = { getItem(key: string) { return JSON.stringify(cookies.get(key)); }, setItem(key: string, value: string) { const obj = JSON.parse(value); cookies.set(key, obj, opts); }, removeItem: function (key: string): void { cookies.remove(key, opts); } } refresh = useStorage('refresh-token', '', storage) } async function login (req: LoginRequest) { const { data } = await api.post('/auth/login?flow=client&rotation=true'); token.value = data.accessToken; refresh.value = data.refreshToken; } async function refresh (req: LoginRequest) { try { const { data } = await api.get('/auth/refresh?flow=client', { headers: { RefreshToken: refresh.value } }); token.value = data.accessToken; refresh.value = data.refreshToken; } catch (error) { const res = error.response; if (res.status === 401) { token.value = ''; refresh.value = ''; } } } async function logout (req: LoginRequest) { await api.delete('/auth/logout?flow=client'); token.value = ''; refresh.value = ''; } const payload = computed(() => { if (!token.value) { return null; } return jwtDecode(token.value) as Payload; }) return { token, payload, login, refresh, logout } }
E na imagem abaixo, esta em destaque as alterações no composable:
E claro, como o composable agora está esperando os Cookies
como argumento, nós precisamos fazer uma modificação no boot.
src/boot/axios.ts
import { boot } from 'quasar/wrappers'; import axios from 'axios'; import { InjectionKey } from 'vue'; import { useAuth } from 'src/composables/auth'; import { Cookies } from 'quasar'; export const apiKey: InjectionKey<AxiosInstance> = Symbol('api-key'); export default boot(async ({ app, store, router, ssrContext }) => { const cookies = process.env.SERVER ? Cookies.parseSSR(ssrContext) : Cookies; const url = process.env.API_URL; const api = axios.create({ baseURL: url }); const { token, refresh } = useAuth(api, cookies); api.interceptors.request.use( function (config) { if (token.value) { config.headers ||= {} config.headers.Authorization = `Bearer ${token.value}`; } return config; }, function (error) { return Promise.reject(error); } ); api.interceptors.response.use( function (response) { return response; }, async function (error) { const res = error.response; switch (res.status) { case 401: if (token.value) { await refresh() if (token.value) { return api.request(error.config); } } break; } return Promise.reject(error); } ); await refresh(); })
E mais uma vez, estou colocando em destaque as alterações relevantes.
7.2 - Integrando a uma API com rotation=false
Agora iremos falar sobre como utilizar o rotation=false
com o modo SSR
.
7.2.1 - Preparando o Server Side
No Server Side, teremos de extender a instancia do webserver que serve a aplicação SSR com uma mini API, podemos utilizar um SSRMiddleware para esta tarefa.:
quasar new ssrmiddleware auth
importante: O
ssrmiddleware
deve ser adicionado aoquasar.config
Então iremos adicionar os endpoints
/login
, /refresh
, /logout
no nosso middleware
src/utils/index.ts
export const cookieName = 'REFRESH_TOKEN'; export function cookieOptions() { const expires = new Date(); expires.setDate(expires.getDate() + 1); return { path: ['api', 'auth', 'refresh'].join('/'), secure: true, sameSite: 'lax', httpOnly: true, expires: expires, }; }
src-ssr/middlewares/auth.ts
import { ssrMiddleware } from 'quasar/wrappers' import { Router } from 'express'; import axios from 'axios'; import { cookieName, cookieOptions } from '../src/utils'; export default ssrMiddleware(async ({ app /*, resolveUrlPath, publicPath, render */ }) => { const url = process.env.API_URL; const api = axios.create({ baseURL: url }); const router = Router() router.post('login', async function (req, res) { const { data } = await api.post('/auth/login?flow=client&rotation=false'); res.cookie(cookieName, data.refreshToken, cookieOptions()); res.json({ accessToken: data.accessToken }); }) router.get('refresh', async function (req, res) { let accessToken = '' const refresh = req.cookies[cookieName]; if (refresh) { const { data } = await api.get('/auth/refresh?flow=client'); accessToken = data.value; } return { accessToken } }) router.delete('logout', async function (req, res) { await api.get('/auth/logout?flow=client'); res.clearCookie(cookieName) }) app.use('/api/auth', router) })
Note que estamos usando o flow=client
no nosso backend
, mas na API interna do frontend
estamos escrevendo um cookie
, e este cookie estará disponível apenas no nosso server-side
.
7.2.2 - Comunicação entre o Server Side e o Client Side
Agora vamos ao nosso composable, ele vai permanecer praticamente o mesmo, mas quando estivemos no client-side, ao invés de chamar a API externa, ele irá chamar a API interna.
src/composables/auth.ts
import { ref, computed, Ref } from 'vue' import { useStorage, StorageLike } from '@vueuse/core' import jwtDecode from 'jwt-decode'; import { Cookies } from 'quasar'; import axios from 'axios'; import type { AxiosInstance } from 'axios' import type { JwtPayload } from 'jwt-decode'; import { cookieName, cookieOptions } from 'src/utils'; type Payload = JwtPayload & { roles: string[] } interface LoginRequest { username: string; password: string; } const token = ref(''); const internal = axios.create({ baseURL: '/api/auth/' }); function useAuth (api: AxiosInstance, cookies: Cookies) { async function login (req: LoginRequest) { const { data } = await internal.post('login'); token.value = data.accessToken; } async function refresh (req: LoginRequest) { if (process.env.CLIENT) { const { data } = await internal.get('refresh'); token.value = data.accessToken; } if (process.env.SERVER) { var refreshToken = cookies.get('REFRESH_TOKEN') try { const { data } = await api.get('/auth/refresh?flow=client', { headers: { RefreshToken: refreshToken } }); token.value = data.accessToken; cookies.set(cookieName, data.refreshToken, cookieOptions()); } catch (error) { const res = error.response; if (res.status === 401) { token.value = ''; cookies.remove(cookieName, cookieOptions()); } } } } async function logout (req: LoginRequest) { await internal.delete('logout'); token.value = ''; } const payload = computed(() => { if (!token.value) { return null; } return jwtDecode(token.value) as Payload; }) return { token, payload, login, refresh, logout } }
E por fim os pontos de interrese:
Note que estamos sempre a chamar a API interna, com exceção do refresh
, que caso seja executado do server-side
irá chamar a API externa diretamente e atualizar o Cookie
.
Top comments (1)
Excelente Tobias!