Rotas Privadas em ReactJS

Rotas Privadas em ReactJS

Na lição anterior, nós aprendemos a criar rotas em nossas aplicações utilizando a biblioteca React Router DOM.

Hoje, nós iremos dar um passo a mais no conteúdo visto anteriormente, onde iremos criar rotas privadas!

Que nada mais são do que a base de um sistema de LOGIN!

Ou seja, rotas que só podem ser acessadas quando uma determinada condição é VERDADEIRA!

A lição de hoje vai te ensinar diversos assuntos super importantes sobre como criar aplicações seguras utilizando estratégias de rotas privadas 😄

Criando seu projeto de testes

Antes de começarmos, vamos criar um novo projeto em ReactJS dedicado a esta lição. No meu caso criei um novo projeto chamado de rotas-privadas:  

npx create-react-app rotas-privadas

Após isso, não se esqueça de fazer aquela limpeza do código, e também aquela organizada nas pastas do src (organização por funcionalidade) 😁

Show, agora vamos instalar o React Router DOM na nossa aplicação:

npm install react-router-dom

E também o Axios, pois iremos utilizá-lo também:

npm install axios

Feito isso, que tal criarmos nossas duas páginas principais?

Criando suas páginas principais

Para começar vamos criar duas páginas dentro de src > pages:

  • Login
  • Dashboard

Login > index.jsx:

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

const Login = () => {
 const [email, setEmail] = useState('');
 const [password, setPassword] = useState('');

 const handleLogin = () => {
 console.log(email, password);
 }

 return (
 <div style={{ margin: '30px' }}>
 <form onSubmit={handleLogin}>
 <input type="text" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Login" /><br />
 <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Senha" /><br />
 <button type="submit">Entrar</button>
 </form>
 </div>
 );

}

export default Login;

Dashboard > index.jsx:

const Dashboard = () => {
 return (
 <div>
 <h1>Seja bem vindo ao Dashboard!</h1>
 </div>
 );
}

export default Dashboard;

Este é o resultado final de cada uma das páginas:

E sim, o layout está bem simples mesmo, pois o nosso objetivo aqui é te explicar a lógica de como funciona de fato uma rota privada 😌

Criando nosso arquivo de rotas

Após isso, vamos criar um novo arquivo chamado de routes.js dentro da pasta routes, com o seguinte código:

import { Routes, Route } from 'react-router-dom';

import Login from '../pages/Login';
import Dashboard from '../pages/Dashboard';

function RouterApp(){
 return(
 <Routes>
 <Route path="/" element={<Login />} />
 <Route path="/dashboard" element={<Dashboard />} />
 </Routes>
 );
}

export default RouterApp;

Por fim, chame esse arquivo dentro do App.js encapsulado dentro de um <BrowserRoute>:

import { BrowserRouter } from 'react-router-dom';
import RouterApp from './routes/routes.js';

export default function App() {
 return (
 <BrowserRouter>
 <RouterApp /> 
 </BrowserRouter>
 );
}

A partir daí, as nossas rotas já começam a funcionar perfeitamente, maaaaas, o usuário ainda consegue acessar a página de /dashboard digitando ela no navegador 🥲

O que é um comportamento esperado, visto que ainda não criamos a lógica das nossas rotas privadas, não é verdade? 😄

O CORE de uma Rota Privada

Antes de visualizarmos na prática o funcionamento de uma rota privada em ReactJS, vamos entender a lógica primária de como ela funciona de fato.

Como você já sabe, uma rota privada refere-se a uma rota que só pode ser acessada por usuários autenticados ou com determinadas permissões.

Isso significa que quando um usuário não autenticado ou não permissionado tenta acessar uma determinada URL, ele será automaticamente redirecionado para a tela de login ou de cadastro.

Esse é o funcionamento básico de qualquer sistema da web que possui autenticação, como Google, Facebook, Instagram, TikTok, Mercado Livre e entre outros sistemas.

Já em uma aplicação em ReactJS com roteamento (React Router DOM), você pode criar um componente especial para lidar com rotas que requerem autenticação.

Basicamente, você criaria um componente que encapsularia determinada rotas usando a estratégia do children (ainda se lembra dele?), onde a ideia é executar certas operações antes de retornar o children, e é aí que você pode verificar se o usuário está autenticado ou não, ou seja, se ele está apto para abrir essa página (children) ou não (ser redirecionado).

É como se colocássemos "seguranças" em um passo anterior, antes mesmo do ReactJS abrir o componente declarado no element:

<Route path="/dashboard" element={👮‍♂️ <Dashboard />} />

Obviamente que esse emoji (👮‍♂️) é só uma forma figurada de mostrar como a "coisa" funciona, ok? (Não coloque emojis alí dentro rs)

Na vida real, nós basicamente teríamos um novo componente nesse estilo aqui:

import React from 'react';
import { Route, Redirect } from 'react-router-dom';

const PrivateRoute = ({ component: Component, ...rest }) => (
 <Route {...rest} render={(props) =>
 isAuthenticated() ? (
 <Component {...props} />
 ) : (
 <Redirect to="/login" />
 )
 } />
);

export default PrivateRoute;

E que o mesmo poderia ser acionado mais ou menos assim:

<PrivateRoute path="/dashboard" component={Dashboard} />

Observe que existe um componente chamado de PrivateRoute que renderiza um componente do tipo <Route> que herda dentro do {...rest} os atributos path e component.

Onde por fim, ele executa uma função chamada de render que é usada para renderizar componentes com base na correspondência das suas rotas.

O render é uma propriedade usada dentro de um componente de rota (Route) para determinar o que será renderizado quando a rota correspondente for acessada.

Veja um exemplo mais simples de sua utilização:

<Route path="/dashboard" render={(props) => (
 isAuthenticated() ? (
 <Dashboard {...props} />
 ) : (
 <Redirect to="/login" />
 )
)} />

Pois bem, voltando ao componente <PrivateRoute />, o render está executando uma função chamada de isAuthenticated(), que verifica se o usuário está autenticado, e em caso positivo, ele abre a página de Dashboard, já em caso negativo, ele redireciona o usuário para a tela de /login.

A partir de agora, nós veremos alguns metodos de se criar rotas privadas dentro da nossa aplicação de exemplo.

Método Fácil) Criando Rotas Privadas de forma fácil

O primeiro método que veremos agora, é sobre como criar rotas privadas de uma maneira um pouco mais fácil do que aquela que você viu no código cima.

Tudo começa na criação de um novo arquivo chamado de PrivateRoute.js que deve existir dentro da pasta de routes:

Esse arquivo deve conter o seguinte código:

import { useContext } from 'react';
import { Navigate } from 'react-router-dom';
import { AuthContext } from '../contexts/auth';

export default function PrivateRoute({ children }) {
 const { signed, loading } = useContext(AuthContext);

 if(loading){
 return <div></div>;
 }

 if(!signed){
 return <Navigate to='/' />;
 }

 return children;
}

Como podemos ver no código acima, o PrivateRoute recebe uma children, que nada mais é do que outro componente.

Só que antes desse componente ser mostrado, o PrivaterRoute faz uma verificação por meio de um contexto chamado de AuthContext, que é responsável por fazer validações do usuário.

Nesse caso, precisamos criar um novo arquivo chamado de authContext.js dentro da pasta contexts:

Com o seguinte código:

import { useState, createContext, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';

export const AuthContext = createContext({});

const AuthProvider = ({ children }) => {
 const [userInfo, setUserInfo] = useState(null);
 const [loading, setLoading] = useState(true);
 const [loadingAuth, setLoadingAuth] = useState(false);
 const navigate = useNavigate();

 useEffect(() => {
 async function loadUserInfo(){
 const userStorageInfo = localStorage.getItem('userInfo');

 if(userStorageInfo){
 setUserInfo(JSON.parse(userStorageInfo));
 setLoading(false);
 }

 setLoading(false);
 }

 loadUserInfo();
 }, []);

 const signIn = async (email, password) => {
 setLoadingAuth(true);
 try {
 const { data } = await axios.post('http://localhost/signIn.php', { email, password });

 setUserInfo(data);
 setLoadingAuth(false);
 localStorage.setItem('userInfo', JSON.stringify(data));
 navigate('/dashboard');
 } catch (error) {
 alert(error.response.data.message);
 setLoadingAuth(false);
 console.error(error);
 }
 }

 const logout = () => {
 setUserInfo(null);
 localStorage.removeItem('userInfo');
 navigate('/');
 }

 return(
 <AuthContext.Provider value={{ signed: !!userInfo, userInfo, loadingAuth, loading, signIn, logout }}>
 {children}
 </AuthContext.Provider>
 );
}

export default AuthProvider;

Este código é um exemplo de um componente ReactJS que está chamado o AuthProvider, que por sua vez, utiliza o contexto (Context API) do ReactJS para gerenciar o estado de autenticação de um usuário em uma aplicação.

userInfo: Armazena as informações do usuário autenticado.

loading: Indica se está carregando as informações de usuário.

loadingAuth: Indica se está no processo de autenticação.

Na função de signIn, ela é uma função assíncrona que tenta autenticar o usuário usando uma requisição POST para um endpoint http://localhost/sigIn.php.

Se a autenticação for bem-sucedida, userInfo é atualizado com os dados do usuário recebidos, e a informação é armazenada no localStorage, e então, navega para a URL /dashboard após o login.

E em caso de erro, exibe uma mensagem no console.

Já a função logout é uma função que limpa as informações de autenticação do usuário.

Feito isso, precisamos importar esse AuthProvider dentro do nosso App.js, de modo que englobe nosso componente <RoutesApp>:

import { BrowserRouter } from 'react-router-dom';
import RouterApp from './routes/routes.js';
import AuthProvider from './contexts/authContext.js';

export default function App() {
 return (
 <BrowserRouter>
 <AuthProvider>
 <RouterApp />
 </AuthProvider>
 </BrowserRouter>
 );
}

Perfeito, agora basta usar o componente <PrivateRoute> dentro do arquivo routes.js, de forma a encapsular as páginas privadas, por exemplo:

import { Routes, Route } from 'react-router-dom';
import PrivateRoute from './PrivateRoute';

import Login from '../pages/Login';
import Dashboard from '../pages/Dashboard';

function RouterApp(){
 return(
 <Routes>
 <Route path="/" element={<Login />} />
 <Route path="/dashboard" element={ <PrivateRoute><Dashboard /></PrivateRoute>} />
 </Routes>
 );
}

export default RouterApp;

Observe que estamos encapsulando o componente <Dashboard /> usando o componente <PrivateRoute>.

Se tentarmos acessar a URL /dashboard na nossa aplicação:

O sistema irá nos redirecionar de volta a página inicial (tela de login), o que indica que as rotas privadas estão funcionando perfeitamente.

Implementando o sistema de login

Ainda no método fácil, para terminá-lo, vamos botar para funcionar aquele formulário de login 😁

Mas antes, segue o código PHP (signIn.php) que você deve inserir na sua máquina local para testar esse código:

<?php

// Permitir solicitações CORS
header("Access-Control-Allow-Origin: http://localhost:3000");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");

// Verifica se a requisição é do tipo OPTIONS (preflight request)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
 // Responde apenas com status 200 OK
 http_response_code(200);
 exit();
}

// Verifica se a requisição é do tipo POST
if ($_SERVER["REQUEST_METHOD"] === "POST") {
 // Recebe os dados enviados via POST
 $data = json_decode(file_get_contents("php://input"), true);

 // Verifica se existem os campos email e password no payload JSON
 if(isset($data['email']) && isset($data['password'])){
 // Verifica as credenciais (exemplo de credenciais estáticas para teste)
 if($data['email'] == "admin@micilini.com" && $data['password'] == "admin"){
 http_response_code(200);
 echo json_encode(array("status" => true, "nome" => "Micilini", "site" => "https://micilini.com/"));
 exit();
 } else {
 // Credenciais inválidas
 http_response_code(401); // Unauthorized
 echo json_encode(array("message" => "Credenciais inválidas!"));
 exit();
 }
 } else {
 // Campos email e password não foram enviados
 http_response_code(400); // Bad Request
 echo json_encode(array("message" => "Parâmetros 'email' e 'password' são obrigatórios."));
 exit();
 }
}

// Método não permitido
http_response_code(405); // Method Not Allowed
echo json_encode(array("message" => "Método não permitido. Use apenas o método POST."));
exit();

O código acima recebe uma verificação via POST, e só retorna os dados do usuário caso o email for admin@micilini.com e a senha admin.

Agora, vamos voltar para o nosso componente Login e fazer mais algumas modificações no código:

Login > index.jsx:

import { useState, useContext } from 'react';

import { AuthContext } from '../../contexts/authContext';

const Login = () => {
 const [email, setEmail] = useState('');
 const [password, setPassword] = useState('');

 const { signIn, loadingAuth } = useContext(AuthContext);

 const handleLogin = async (e) => {
 e.preventDefault();
 
 if(email !== '' && password !== ''){
 await signIn(email, password);
 }
 }

 return (
 <div style={{ margin: '30px' }}>
 <form onSubmit={handleLogin}>
 <input type="text" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Login" /><br />
 <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Senha" /><br />
 <button type="submit">{loadingAuth ? 'Carregando...' : 'Acessar'}</button>
 </form>
 </div>
 );

}

export default Login;

Com o comando acima, somos capazes de realizar um login dentro da nossa aplicação de modo a sermos redirecionados a tela de Dashboard.

Legal, não acha?

Se você abrir o localStorage do seu navegador, vai notar que uma nova chave chamada de userInfo foi criada, e dentro dela existem os dados que foram retornados do PHP:

E falando em Dashboard, que tal darmos uma estilizada nele de modo a recuperar esses dados, e implementar a funcionalidade de logout?

Dashboard > index.jsx:

import { useContext } from 'react';
import { AuthContext } from '../../contexts/authContext';

const Dashboard = () => {
 const { logout, userInfo } = useContext(AuthContext);

 const handleLogout = async () => {
 await logout();
 }

 return (
 <div style={{ margin: '30px' }}>
 <h1>Seja bem vindo ao Dashboard!</h1>
 <p>Nome: {userInfo.nome} / Site: <a href="https://micilini.com">{userInfo.site}</a></p>
 <button onClick={handleLogout}>Sair</button>
 </div>
 );
}

export default Dashboard;

Veja como ficou o resultado final:

Lembrando que se o usuário tentar acessar novamente a tela de dashboard via URL, ele será redirecinado a tela principal (tela de login) 😊

Método Profissional) Criando rotas privadas de maneira profissional no ReactJS com tokens JWT

ATENÇÃO: clique aqui para ver este tópico em formato de vídeo.

O método anterior é muito legal e funcional, porém... hoje em dia já existem alguns métodos, digamos que, um pouco mais profissionais de se criar sistemas de login (e muito mais seguros).

Estou me referindo a sistemas de autenticação usando Tokens JWT.

Há um tempo atrás, eu postei u martigo na micilini que fala sobre Cookies VS LocalStorage: Dicas de como armazenar dados e tokens (JWT) com segurança, vale a pena dar uma olhada antes de continuarmos nossa lição.

Dito isto, vamos começar criando um novo projeto TOTALMENTE DO ZERO, pois esse tópico merece 😉

Criando nosso projeto de testes

Antes de começarmos, vamos criar um novo projeto em ReactJS dedicado a este tópico. No meu caso criei um novo projeto, também chamado de rotas-privadas:

npx create-react-app rotas-privadas

Após isso, não se esqueça de fazer aquela limpeza do código, e também aquela organizada nas pastas do src (organização por funcionalidade) 😁

E não se esqueça de instalar as duas bibliotecas necessárias para este projeto:

npm install react-router-dom
npm install axios

Feito isso, vamos criar nossas duas telas principais!

Criando nossas telas principais

Para começar vamos criar três telas principais que serão utilizadas durante à nossa aplicação. Lembre-se de localizá-las dentro de src > pages 😁

Login > index.jsx:

import { useState, useContext } from 'react';

import { AuthContext } from '../../contexts/authContext';

const Login = () => {
 const [email, setEmail] = useState('admin@micilini.com');
 const [password, setPassword] = useState('admin');
 const [loadingSignIn, setLoadingSignIn] = useState(false);

 const { signIn } = useContext(AuthContext);

 const handleLogin = async (e) => {
 e.preventDefault();
 setLoadingSignIn(true);
 
 if(email !== '' && password !== ''){
 await signIn(email, password);
 }
 }

 return (
 <div style={{ margin: '30px' }}>
 <form onSubmit={handleLogin}>
 <input type="text" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Login" /><br />
 <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Senha" /><br />
 <button type="submit">{loadingSignIn ? 'Carregando...' : 'Acessar'}</button>
 </form>
 </div>
 );

}

export default Login;

Dashboard > index.jsx:

import { useContext } from 'react';
import { AuthContext } from '../../contexts/authContext';
import { Link } from 'react-router-dom';

const Dashboard = () => {
 const { logout, userInfo } = useContext(AuthContext);

 const handleLogout = async () => {
 await logout();
 }

 return (
 <div style={{ margin: '30px' }}>
 <h1>Seja bem vindo ao Dashboard!</h1>
 <p>Olá {userInfo?.nome}</p>
 <button onClick={handleLogout}>Sair</button>
 <Link to="/profile">Ir para o Profile</Link>
 </div>
 );
}

export default Dashboard;

Profile > index.jsx:

import { useContext } from 'react';
import { AuthContext } from '../../contexts/authContext';
import { Link } from 'react-router-dom';

const Dashboard = () => {
 const { accessToken } = useContext(AuthContext);

 return (
 <div style={{ margin: '30px' }}>
 <h1>Meu Perfil</h1>
 <p>{accessToken}</p>
 <Link to="/dashboard">Dashboard</Link>
 </div>
 );
}

export default Dashboard;

Criando nossas rotas

Após isso, vamos criar um novo arquivo chamado de routes.js dentro da pasta routes. 

Routes > index.js:

import { Routes, Route } from 'react-router-dom';
import PrivateRoute from './PrivateRoute';

import Login from '../pages/Login';
import Dashboard from '../pages/Dashboard';
import Profile from '../pages/Profile';

function RouterApp(){
 return(
 <Routes>
 <Route path="/" element={<Login />} />
 <Route path="/dashboard" element={ <PrivateRoute><Dashboard /></PrivateRoute>} />
 <Route path="/profile" element={ <PrivateRoute><Profile /></PrivateRoute>} />
 </Routes>
 );
}

export default RouterApp;

Em seguida, não esqueça de atualizar o App.js para que nossas rotas sejam aplicadas com sucesso na nossa aplicação:

import { BrowserRouter } from 'react-router-dom';
import RouterApp from './routes/routes.js';
import AuthProvider from './contexts/authContext.js';

export default function App() {
 return (
 <BrowserRouter>
 <AuthProvider>
 <RouterApp />
 </AuthProvider>
 </BrowserRouter>
 );
}

Agora, vamos então criar nossas rotas privadas de maneira profissional usando autenticação JWT!

Criando nosso contexto de autenticação

Antes de mais nada, é importante que você crie um contexto de autenticação, uma vez que ele já está até importado dentro do App.js em passos anteriores.

Para isso crie um novo arquivo dentro da pasta contexts chamado de authContext.js:

import { useState, createContext } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';

export const AuthContext = createContext({});

const AuthProvider = ({ children }) => {
 const [accessToken, setAccessToken] = useState(null);
 const [userInfo, setUserInfo] = useState(null);
 const [loadingAuth, setLoadingAuth] = useState(true);
 const navigate = useNavigate();

 //signIn é uma função usada para fazer login na aplicação. Ela é chamada de dentro do componente Login.
 const signIn = async (email, password) => {
 setLoadingAuth(true);
 try {
 const { data } = await axios.post('http://localhost/api-tokens/signIn.php', { email: email, password: password }, {withCredentials: true});//{withCredentials: true} é para permitir o uso de cookies do lado do servidor.

 setAccessToken(data.accessToken);//Seta o access_token que foi retornado na requisição dentro do estado do nosso contexto (Isso é salvar na memoria da aplicação).

 setLoadingAuth(false);//Seta o loadingAuth como false, para que o botão de login possa ser clicado novamente.

 navigate('/dashboard');//Redireciona o usuário para a rota /dashboard, pois a partir de agora o usuário está autenticado.
 } catch (error) {
 alert(error.response.data.message);//Se houver algum erro, eu mostro um alerta com a mensagem de erro que veio do back-end. (Aqui você poderia repassar para o componente de forma a mostrar tal informação de forma mais visual para o usuário.)

 setLoadingAuth(false);//Seta o loadingAuth como false, para que o botão de login possa ser clicado novamente.
 
 console.error(error);//Aqui eu mostro o erro no console, para que eu possa debugar o erro. (Isso deve ser removido em modo de produção.)
 }
 }

 //verifyToken é uma função usada para verificar se o token do usuário está válido ou não. Ela é chamada dentro do componente PrivateRoute sempre quando o usuário atualiza a página ou navega entre as rotas. A função retorna os dados do usuário em caso de sucesso e false em caso de erro.
 const verifyToken = async () => {
 console.log('VerifyToken called!');
 try {
 const { data } = await axios.get('http://localhost/api-tokens/authUser.php', {
 params: {
 access_token: accessToken//Envio o access_token como parâmetro para a rota authUser.php.
 }
 });//Faço uma requisição para a rota authUser.php, passando o access_token como parâmetro de modo a validar se o token ainda está expirado ou não.

 setUserInfo(data);//Como ele retorna os dados do usuário, eu seto esses dados no estado do contexto. ()
 console.log(data);
 setLoadingAuth(false);//Seta o loadingAuth como false, para que o botão de login possa ser clicado novamente.

 return true;//Retorno true, pois o token está válido.
 } catch (error) {
 setLoadingAuth(false);//Seta o loadingAuth como false, para que o botão de login possa ser clicado novamente.
 return false;//Retorno false, pois o token está expirado.
 }
 }

 //refreshUser é uma função usada para gerar um novo access_token via refresh_token. Ela é chamada dentro do componente PrivateRoute sempre que o token do usuário está expirado (ou seja, quando a função verifyToken criada acima retorna FALSE). A função retorna true em caso de sucesso e false em caso de erro.
 const refreshUser = async () => {
 console.log('RefreshUser called!');
 try {
 const { data } = await axios.post('http://localhost/api-tokens/refreshUser.php', {}, {withCredentials: true});//Faço uma requisição para a rota refreshUser.php, de modo a gerar um novo access_token via refresh_token. {withCredentials: true} é para permitir o uso de cookies do lado do servidor.

 setAccessToken(data.accessToken);//Seto o novo access_token no estado do contexto.
 setUserInfo(data.user);//Seto os dados do usuário no estado do contexto.
 console.log(data.user);
 setLoadingAuth(false);//Seta o loadingAuth como false, para que o botão de login possa ser clicado novamente.

 return true;//Retorno true, pois o token foi gerado com sucesso.
 } catch (error) {
 setLoadingAuth(false);//Seta o loadingAuth como false, para que o botão de login possa ser clicado novamente.
 return false;//Retorno false, pois houve um erro ao tentar gerar o novo token.
 }
 }

 //logout é uma função usada para fazer logout na aplicação. Ela é chamada de dentro do componente Dashboard ou de qualquer outro componente que consiga acessar o contexto e precise deslogar o usuário.
 const logout = () => {
 try{
 axios.post('http://localhost/api-tokens/logout.php', {}, {withCredentials: true});//Faço uma requisição para a rota logout.php, de modo a deslogar o usuário no back-end. {withCredentials: true} é para permitir o uso de cookies do lado do servidor.
 } catch (error) {
 return false;//Retorno false, pois houve um erro ao tentar gerar o novo token.
 }finally{
 setAccessToken(null);
 setUserInfo(null);
 navigate('/');
 }
 }

 //O retorno do AuthContext.Provider é o que será disponibilizado para os componentes que estiverem dentro do AuthProvider.
 return(
 <AuthContext.Provider value={{ accessToken, userInfo, loadingAuth, signIn, logout, refreshUser, verifyToken }}>
 {children}
 </AuthContext.Provider>
 );
}

export default AuthProvider;

Criando rotas privadas

É importante ressaltar que as rotas privadas dessa aplicação farão uso do JWT em conjunto com access token e refresh token.

Sendo assim, como parte dessa lógica foi aplicada dentro do nosso contexto, precisamos então criar um novo arquivo chamado de PrivateRoute.js dentro da pasta routes:

import { useContext, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { AuthContext } from '../contexts/authContext';

export default function PrivateRoute({ children }) {
 const navigate = useNavigate();
 const { accessToken, loadingAuth, refreshUser, verifyToken } = useContext(AuthContext);
 
 const doRefresh = async () => {
 const user = await refreshUser();
 if (!user) {
 navigate("/");
 }
 };
 
 const isAuth = async () => {
 if (accessToken == null) {
 await doRefresh();
 } else {
 const isTokenValid = await verifyToken();
 if (!isTokenValid) {
 await doRefresh();
 }
 }
 };
 
 useEffect(() => {
 isAuth();
 }, [children]);
 
 if (loadingAuth) {
 return <div>Loading...</div>;
 }
 
 return children;
 }

É dentro desse arquivo que a verdadeira mágica acontece!

Back-end em PHP

Com você viu anteriormente, eu estou chamando uma série de arquivos que foram criados com o PHP, que representam a nossa "api".

Segue abaixo o código fonte de cada um deles 🙂

signIn.php:

<?php

// Definir segredos para assinar os tokens JWT
define('ACCESS_SECRET', 'seu_access_secret_aqui');
define('REFRESH_SECRET', 'seu_refresh_secret_aqui');

// Permitir solicitações CORS
header("Access-Control-Allow-Origin: http://localhost:3000");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Access-Control-Allow-Credentials: true"); // Permitir credenciais (cookies)

// Verifica se a requisição é do tipo OPTIONS (preflight request)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
 // Responde apenas com status 200 OK
 header("Access-Control-Allow-Headers: Content-Type, Authorization");
 header("Access-Control-Allow-Methods: POST, OPTIONS");
 http_response_code(200);
 exit();
}

// Incluir a biblioteca JWT
require __DIR__ . '/vendor/autoload.php';
use \Firebase\JWT\JWT;

// Função para gerar AccessToken
function generateAccessToken($userID, $expiresIn) {
 $accessTokenPayload = array(
 "user_id" => $userID,
 "exp" => time() + $expiresIn // expiração em segundos
 );

 // Gera e retorna o AccessToken
 return JWT::encode($accessTokenPayload, ACCESS_SECRET, 'HS256');
}

// Função para gerar RefreshToken
function generateRefreshToken($userID) {
 // Tempo de expiração do token (por exemplo, 30 dias)
 $expiracao = time() + (30 * 24 * 60 * 60); // Adiciona 30 dias em segundos

 // Dados do token
 $token = array(
 "user_id" => $userID,
 "exp" => $expiracao
 );

 // Gerar o token JWT
 $refreshToken = JWT::encode($token, REFRESH_SECRET, 'HS256');

 return $refreshToken;
}

// Verifica se a requisição é do tipo POST
if ($_SERVER["REQUEST_METHOD"] === "POST") {
 // Recebe os dados enviados via POST
 $data = json_decode(file_get_contents("php://input"), true);

 // Verifica se existem os campos email e password no payload JSON
 if(isset($data['email']) && isset($data['password'])){
 // Verifica as credenciais (exemplo de credenciais estáticas para teste)
 if($data['email'] == "admin@micilini.com" && $data['password'] == "admin"){

 // Simula dados do usuário
 $userID = 98;
 $expiresIn = 30; // 30 segundos para expiração do AccessToken

 // Gera AccessToken
 $accessToken = generateAccessToken($userID, $expiresIn);

 // Gera RefreshToken
 $refreshToken = generateRefreshToken($userID);

 // Configura o RefreshToken em um cookie HTTP-only
 setcookie('refreshToken', $refreshToken, [
				'expires' => time() + 30 * 24 * 60 * 60, // expira em 30 dias
				'path' => '/api-tokens',
				'domain' => '', // ou ajuste conforme necessário
				'secure' => true, // apenas HTTPS
				'httponly' => true, // acessível apenas via HTTP
				'samesite' => 'None' // ajuste conforme necessário
			]);

 // Retorna AccessToken para o frontend
 http_response_code(200);
 echo json_encode(array(
 "status" => true,
 "accessToken" => $accessToken
 ));
 exit();

 } else {
 // Credenciais inválidas
 http_response_code(401); // Unauthorized
 echo json_encode(array("message" => "Credenciais inválidas!"));
 exit();
 }
 } else {
 // Campos email e password não foram enviados
 http_response_code(400); // Bad Request
 echo json_encode(array("message" => "Parâmetros 'email' e 'password' são obrigatórios."));
 exit();
 }
}

// Método não permitido
http_response_code(405); // Method Not Allowed
echo json_encode(array("message" => "Método não permitido. Use apenas o método POST."));
exit();

authUser.php:

<?php

define('ACCESS_SECRET', 'seu_access_secret_aqui');

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header("Access-Control-Allow-Headers: X-Requested-With");

// Incluir a biblioteca JWT
require __DIR__ . '/vendor/autoload.php';
use \Firebase\JWT\JWT;
use Firebase\JWT\Key;

if(isset($_GET['access_token'])) {
 $accessToken = $_GET['access_token'];
 
 try {
 // Executa o payload no token JWT enviando o accessToken e ACCESS_SECRET
 $decoded = JWT::decode($accessToken, new Key(ACCESS_SECRET, 'HS256'));

 // Verifica se o payload está correto
 if ($decoded->user_id == 98) {
 // Se o user_id for igual a 98, retorna algumas informações
 $response = array(
 "nome" => "Nome do usuário",
 "site" => "https://micilini.com/",
 "role" => array("admin", "user", "editor")
 );
 http_response_code(200);
 echo json_encode($response);
 exit();
 } else {
 // Caso o user_id não seja 98, retorna um erro
 http_response_code(403);
 echo json_encode(array("message" => "Acesso não autorizado"));
 exit();
 }
 } catch (Exception $e) {
 // Caso ocorra algum erro ao decodificar o token JWT
 http_response_code(401);
 echo json_encode(array("message" => "Token inválido: " . $e->getMessage()));
 exit();
 }
}

http_response_code(400);
echo json_encode(array("message" => "Houve algum problema na aplicação, tente novamente mais tarde!"));
exit();

refreshUser.php:

<?php

// Definir segredos para assinar os tokens JWT
define('ACCESS_SECRET', 'seu_access_secret_aqui');
define('REFRESH_SECRET', 'seu_refresh_secret_aqui');

// Permitir solicitações CORS
header("Access-Control-Allow-Origin: http://localhost:3000");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Access-Control-Allow-Credentials: true"); // Permitir credenciais (cookies)

// Incluir a biblioteca JWT
require __DIR__ . '/vendor/autoload.php';
use \Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Função para gerar AccessToken
function generateAccessToken($userID, $expiresIn) {
 $accessTokenPayload = array(
 "user_id" => $userID,
 "exp" => time() + $expiresIn // expiração em segundos
 );

 // Gera e retorna o AccessToken
 return JWT::encode($accessTokenPayload, ACCESS_SECRET, 'HS256');
}

// Função para verificar RefreshToken
function verifyRefreshToken($refreshToken) {
 try {
 // Decodificar o token JWT
 $decoded = JWT::decode($refreshToken, new Key(REFRESH_SECRET, 'HS256'));

 // Verificar se o token está expirado
 if ($decoded->exp < time()) {
 return false; // Token expirado
 }

 // Retornar os dados do token decodificado
 return $decoded;

 } catch (Exception $e) {
 return false; // Token inválido (erro na decodificação)
 }
}

// Verifica se a requisição é do tipo OPTIONS (preflight request)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
 // Responde apenas com status 200 OK
 header("Access-Control-Allow-Headers: Content-Type, Authorization");
 header("Access-Control-Allow-Methods: POST, OPTIONS");
 http_response_code(200);
 exit();
}

// Verifica se a requisição é do tipo POST
if ($_SERVER["REQUEST_METHOD"] === "POST") {
 // Verifica se o cookie refresh_token está presente na requisição
 if (isset($_COOKIE['refreshToken'])) {
 $refreshToken = $_COOKIE['refreshToken'];

 try {
			//Verifica se o Token é válido
			$decoded = verifyRefreshToken($refreshToken);
			
			if(verifyRefreshToken($refreshToken)){
				// Verifica se o payload está correto
				if (isset($decoded->user_id)) {
					// Gera um novo AccessToken com expiração de 30 segundos
					$accessToken = generateAccessToken($decoded->user_id, 30);
					
					$response = array(
						"nome" => "Nome do usuário",
						"site" => "https://micilini.com/",
 "role" => array("admin", "user", "editor")
					);

					// Retorna o access_token para o front-end
					http_response_code(200);
					echo json_encode(array("accessToken" => $accessToken, "user" => $response));
					exit();
				} else {
					// Se o payload do refresh_token estiver incorreto
					http_response_code(401); // Unauthorized
					echo json_encode(array("message" => "Payload do refresh_token inválido"));
					exit();
				}
			}else{
				// O Refresh Token é inválido, joga o usuário de volta a tela de Login
				http_response_code(401); // Unauthorized
				echo json_encode(array("message" => "Refresh Token expirado."));
				exit();
			}

 
 } catch (ExpiredException $e) {
 http_response_code(401); // Unauthorized
 echo json_encode(array("message" => "Token expirado: " . $e->getMessage()));
 exit();
 } catch (BeforeValidException $e) {
 http_response_code(401); // Unauthorized
 echo json_encode(array("message" => "Token ainda não é válido: " . $e->getMessage()));
 exit();
 } catch (SignatureInvalidException $e) {
 http_response_code(401); // Unauthorized
 echo json_encode(array("message" => "Assinatura do token inválida: " . $e->getMessage()));
 exit();
 } catch (Exception $e) {
 // Caso ocorra algum outro erro ao decodificar o refresh_token
 http_response_code(401); // Unauthorized
 echo json_encode(array("message" => "Erro ao decodificar o refresh_token: " . $e->getMessage()));
 exit();
 }
 } else {
 // Se o cookie refresh_token não estiver presente na requisição
 http_response_code(401); // Unauthorized
 echo json_encode(array("message" => "Refresh_token não encontrado"));
 exit();
 }
}

// Método não permitido
http_response_code(405); // Method Not Allowed
echo json_encode(array("message" => "Método não permitido. Use apenas o método POST."));
exit();

logout.php:

<?php

// Permitir solicitações CORS
header("Access-Control-Allow-Origin: http://localhost:3000");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Access-Control-Allow-Credentials: true"); // Permitir credenciais (cookies)

if (isset($_COOKIE['refreshToken'])) {
 unset($_COOKIE['refreshToken']); 
 setcookie('refreshToken', '', -1, '/api-tokens'); 
 return true;
} else {
 return false;
}

Lembrando que o objetivo desses arquivos PHP servem somente para te mostrar como você pode criar um back-end que lide com esses tokens, mas não refletem a criação de tokens JWT em aplicações profissionais.

Até porque em aplicações profissionais, você fará o uso de APIs feitas em NodeJS ou Laravel (no caso do PHP).

E pronto, você acaba de construir um sistema de autenticação super profissional utilizando tokens JWT 🤩

Arquivos da lição

Os arquivos dessa lição podem ser encontrados neste repositório do GitHub.

Conclusão

Nesta lição você aprendeu a criar rotas privadas, que é o princípio básico de um sistema de login.

Além disso, aprendeu a criar um pequeno sistema de autenticação utilizando tokens JWT.

Até a próxima aula 😊