DEV Community

Tobias Mesquita for Quasar Framework Brasil

Posted on • Edited on

Protegendo uma aplicação com Refresh Tokens.

Índice

1 - Backend - Conhecendo o Backend

Voltar ao Topo

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.

Backend Methods

2 - Backend - Endpoint de Login

Voltar ao Topo

O primeiro endpoint que iremos visitar é o de login:

Endpoint Login

2.1 - Parâmetros do Login

Voltar ao Topo

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 novo refresh_token além de um access_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 do refresh_token, nesta caso o refresh_token é persistido em um cookie seguro.
  • client: a API irá devolver o refresh_token ao cliente, neste caso a API não irá persistir e/ou gerenciar o refresh_token, ficando a cargo do cliente a segurança do mesmo.

2.2 - Analisando o Login

Voltar ao Topo

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

Voltar ao Topo

Login com flow=server e rotation=false

Veja, que a API retornou a seguinte resposta:

{
  "accessToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTgwY2E2My0yYjUyLTRlYjAtOTJlNi1mMWNlMzYxM2IxNjYiLCJuYW1lIjoiVG9iaWFzIE1lc3F1aXRhIiwiZW1haWwiOiJtZUB0b2JpYXNtZXNxdWl0YS5kZXYiLCJyb2xlcyI6WyJkZXZlbG9wZXIiLCJhZG1pbiIsInVzZXIiXSwiaWF0IjoxNjc4MDQ4NjQ0LCJleHAiOjE2NzgwNDg2NzQsImF1ZCI6Imh0dHBzOi8vand0LXNhbXBsZS50b2JpYXNtZXNxdWl0YS5kZXYiLCJpc3MiOiJodHRwczovL2p3dC1zYW1wbGUudG9iaWFzbWVzcXVpdGEuZGV2IiwianRpIjoiODBmMDQxMDctMGE4MS00ZWNhLWE1ODItNWM3NzViNDQ4MWJmIn0.GIAeo5sfrA5ksKp5msAJCAxLo4ivHxoHrTz6j9JBWyVgg3BmHN0veF24V27PG_K8yqjMiCKRONuNwkdZHENdQg"
}
Enter fullscreen mode Exit fullscreen mode

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"
}]
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}]
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

2.2.2 - Segundo Cenario - flow=client e rotation=true

Voltar ao Topo

Login com 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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}]
Enter fullscreen mode Exit fullscreen mode

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

Voltar ao Topo

Agora que o login já foi bem dissecado, porém voltar a nossa atenção para o refresh

Endpoint de refresh

3.1 - Parâmetros do Refresh

Voltar ao Topo

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 header RefreshToken.

3.2 - Analisando o Refresh

Voltar ao Topo

Agora, vamos dá continuidade aos cenários apresentados no login.

3.2.1 - Primeiro Cenário - flow=server e rotation=false

Voltar ao Topo

Refresh com 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

Voltar ao Topo

Refresh com 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

Voltar ao Topo

E por fim temos o endpoint /logout:

Endpoint de 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

Voltar ao Topo

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

Voltar ao Topo

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

Voltar ao Topo

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();
})
Enter fullscreen mode Exit fullscreen mode

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.

Boot Axios - SPA

6.2 - Integrando a uma API com flow=server

Voltar ao Topo

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Integração com flow=server

6.3 - Integrando a uma API com rotation=true

Voltar ao Topo

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Neste caso, o refresh token é gerenciado pelo frontend, e por isto podemos ver que o mesmo é lido, escrito e persistido.

Integração com rotation=true

7 - Frontend - Protegendo um App SSR

Voltar ao Topo

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

Voltar ao Topo

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
  }
}
Enter fullscreen mode Exit fullscreen mode

E na imagem abaixo, esta em destaque as alterações no composable:

Integração com rotation=true - SSR

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();
})
Enter fullscreen mode Exit fullscreen mode

E mais uma vez, estou colocando em destaque as alterações relevantes.

Boot utilizado na Integração com rotation=true - SSR

7.2 - Integrando a uma API com rotation=false

Voltar ao Topo

Agora iremos falar sobre como utilizar o rotation=false com o modo SSR.

7.2.1 - Preparando o Server Side

Voltar ao Topo

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
Enter fullscreen mode Exit fullscreen mode

importante: O ssrmiddleware deve ser adicionado ao quasar.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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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.

API Interna

7.2.2 - Comunicação entre o Server Side e o Client Side

Voltar ao Topo

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
  }
}
Enter fullscreen mode Exit fullscreen mode

E por fim os pontos de interrese:

Integração entre API Interna e Externa

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)

Collapse
 
helioh3 profile image
Helio Brito • Edited

Excelente Tobias!