DEV Community

Cover image for Autenticação no Next.js com Usuário e Senha e criando Rotas Privadas.

Autenticação no Next.js com Usuário e Senha e criando Rotas Privadas.

Introdução

Autenticação de usuários e acesso restrito a rotas privadas é fundamental para qualquer aplicação, neste artigo/guia vou descrever o passo a passo para realizar a autenticação de usuários que estejam cadastrados na sua base de dados, e que seja necessário realizar o login através de um backend personalizado ou API externa com um login de e-mail e senha.

Recentemente estava desenvolvendo um projeto, onde surgiu essa necessidade, e pesquisando sobre o assunto, notei que não existem conteúdos com exemplos claros, principalmente se precisar tratar os erros do backend personalizado na sua pagina de login, então decidi juntar essas informações em um único artigo.

Para iniciar o exemplo, vou partir da premissa de que você já possui uma aplicação Next.js com a estrutura do NextAuth. Caso não, você pode iniciar um novo projeto e configura-lo de forma simples com esse guia:

Getting Started | NextAuth.js

The example code below describes how to add authentication to a Next.js app.

favicon next-auth.js.org

Estrutura de Pastas

Seguindo os padrões do Next.js você tem:

📁 pages

📁 api

📁 auth

[…nextauth].ts

Login com Email e Senha

pages/api/auth/[…nextauth].ts

O arquivo nextauth é o responsável por realizar o login, nele vamos configurar os providers, no caso do exemplo, vamos utilizar o Credentials Provider, pode ser configurado outros providers junto com o Credentials Provider, você pode consultar em: Next

Precisamos adicionar no arquivo […nextauth].js, cada parte será explicada posteriormente:

export default NextAuth({
  providers: [
    CredentialProvider({
      name: "credentials",
      credentials: {
        email: {
          label: "Email",
          type: "email",
          placeholder: "Email: exemplo@exemplo.com",
        },
        password: { label: "Senha", type: "password" },
      },
      authorize: async (credentials) => {
        try {
          const user = await api.post("/users/getByEmail", {
            email: credentials.email,
            password: credentials.password,
          });

          if (user) {
            const userAccount = user.data;

            return userAccount;
          } else {
            return null;
          }
        } catch (error) {
          const message = error.response.data.message; 
          throw new Error(message);
        }
      },
    }),
  ],
  callbacks: {
    jwt: async ({ token, user }) => {
      if (user) {
        token.id = user.id;
      }

      return token;
    },
    session: ({ session, token }) => {
      if (token) {
        session.id = token.id;
      }
      return session;
    },
  },
  secret: "jwttoken",
  pages: {
    signIn: "/login", 
    error: "/login", 
  },
  jwt: {
    secret: "jwttoken",
  },
});

Enter fullscreen mode Exit fullscreen mode

Name: Nome a ser exibido no formulário de login;

Credentials: As credenciais são usadas para gerar o formulário na pagina de login, se não tiver uma pagina de login personalizada, o NextAuth cria uma pagina, basta acessar: localhost:3000/api/auth/SignIn. Pode Especificar os campos que espera que sejam enviados ex.: dominio, nome de usuario, senha etc. Essa pagina é padrão do Next Auth. Se possuir uma pagina de login personalizada redirecionaremos o usuário posteriormente.

Authorize: Função que faz a chamada API para realizar o login, é nessa função que vai a lógica para consultar o backend personalizado, ou até mesmo acessar uma rota criada nas API Routes da sua aplicação Next.js

  • Nessa função o retorno caso a resposta do seu backend ou API sejam inválidos, deve ser nulo.
  • O Next Auth espera no retorno dessa função um objeto de User ou NULO.
  • No exemplo eu realizei a consulta na rota definida dentro de um TRY CATCH, faço uma verificação condicional, se existir o usuário eu retorno ele, se não, retorno nulo.
  • No meu caso, a minha rota de backend retorna um json com error caso o usuário ou senha sejam inválidos, eu capturo esse erro no CATCH e por padrão do Next Auth o usuário sera redirecionado para a pagina de erro, assim como o a pagina de login personalizada, esse comportamento sera tratado a seguir.

Callbacks: Nesse ponto é criado o Token JWT utilizando o ID do usuário, lembrando que minha API retorna um Objeto de User com ID, EMAIL e NOME.

Nessa mesma lógica é gerado uma SESSION utilizando o Token gerado, essa sessão fica salva no Cookies e é verificada sempre que o usuário acessar a aplicação, quando o usuário realizar o signOut ou o token expirar a sessão é encerrada.

Pages: Nesse objeto vamos realizar os redirecionamentos para as paginas personalizadas prevenindo o comportamento padrão.

  • SignIn: Pagina padrão gerada pelo Next Auth, aqui vamos indicar o caminho para a nossa pagina de Login personalizada. No exemplo eu tenho uma pagina em pages/login.tsx
  • Error: Essa é a pagina que o usuário será redirecionado caso seja rejeitado o retorno da chamada API com erro. Como estamos enviando o erro, eu redireciono o usuário para a pagina de login, e iremos capturar esse erro na nossa pagina de login personalizada.
  • SignOut: Pode ter uma pagina personalizada caso queira redirecionar o usuário quando ele realizar Logout da sua aplicação, ou para a pagina de Login.

Pagina de Login Personalizada

Nesse exemplo utilizarei uma pagina de login personalizada, pode ser encontrada em “pages/login.tsx”.

A pagina foi criada utilizando ChakraUI com YUP para validação dos campos e React Hook Form para manipular o formulário.

Você pode criar interfaces com Chakra UI acessando: ChakraUi

A sua pagina de login personalizada terá um manipulador de envio de formulario que usará a função signIn do Next Auth para realizar o login do usuário. No meu caso, tenho a função handle signIn, estou utilizando SubmitHandler do useForm.

const handleSignIn: SubmitHandler<SignInFormData> = async (values) => {
    const res = await signIn("credentials", {
      redirect: false,
      email: values.email,
      password: values.password,
      callbackUrl: `${window.location.origin}`,
    });

    if (res.error) {
      toast({
        title: `${res.error}`, 
        status: "error",
        duration: 4000,
        isClosable: true,
      });
    }

    if (res.url) Router.push(res.url);
  };

Enter fullscreen mode Exit fullscreen mode

signIn: Essa função recebe como parâmetro o nome do Provider, definido em “name” no tópico anterior, e um objeto com propriedades.

  • redirect: false, por padrão essa propriedade é true, setamos ela como false, para conseguir capturar a resposta da Função signIn.
    • Ela retorna uma Promise com a seguinte estrutura:
{
    error: string || undefined,
    status: number,
    ok: boolean,
    url: url || null
}
Enter fullscreen mode Exit fullscreen mode
  • Email e Password: Enviamos os valores esperados no Provider.
  • CallbackUrl: Especifica para qual URL o usuário será redirecionado após entrar.

Como definimos redirect como false, ele não irá redireicionar o usuário caso o login seja bem sucedido, por isso fazemos uma verificação condicional se existe uma URL, se sim, utilizamos o Router.push, passando a URL, definida em CallbackURL.

Se houver erro no login, que esta sendo enviado do Provider, recebemos ele, e mostramos em um Toast.

Logout da Aplicação.

Para realizar o logout, utilize o método signOut(), para que seja concluído o fluxo de saída e encerrada a sessão do usuario.

Esse metodo recarrega a pagina quando concluído, e o usuário será redirecionado para pagina de Login. É possivel especificar uma URL para redirecionar o usuário como no exemplo a seguir:

import { signOut } from "next-auth/react"

export default () => <button onClick={() => 
    signOut({ callbackUrl: 'http://localhost:3000/login' })}>Sign out</button>

Enter fullscreen mode Exit fullscreen mode

Session Provider

Na sua pagina do _app.ts deverá envolver o seu componente com o Session Provider para garantir que todas paginas tenha acesso ao Next Auth:

import { SessionProvider } from 'next-auth/client'

export default function App ({ Component, pageProps }) {
  return (
    <SessionProvider session={pageProps.session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Rotas Privadas

A proteção de rotas é crucial em qualquer aplicação que possa ser acessada apenas com Login, a seguir iremos criar rotas protegidas utilizando a lógica do Next Auth.

Existem algumas formas de verificar se o usuário possui uma sessão ativa e então exibir o conteúdo da pagina, porém a forma como o Next.js trata getServerSideProps e getInitialProps, todo carregamento de página protegida irá realizar uma solicitação do lado do servidor para determinar se a sessão é válida anter de gerar a página solicitada(SSR), isso aumenta a carga do servidor, mas há uma opção para fazer solicitações do cliente, se nenhuma sessão for identificada após o carregento inicial, você determinará a ação apropriada.

Função Auth

Vamos modificar o arquivo _app.js:

import { SessionProvider, useSession, signIn } from 'next-auth/react';
export default function App({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    <SessionProvider session={session}>
      {Component.auth ? (
        <Auth>
          <Component {...pageProps} />
        </Auth>
      ) : (
        <Component {...pageProps} />
      )}
    </SessionProvider>
  )
}

function Auth({ children }) {
  const { data: session, status } = useSession()
  const isUser = !!session?.user
  React.useEffect(() => {
    if (status === "loading") return
    if (!isUser) signIn()
  }, [isUser, status])

  if (isUser) {
    return children
  }


  return <div>Loading...</div>
}
Enter fullscreen mode Exit fullscreen mode

A Sessão esta sendo buscada, se não houver nenhum usuário o useEffect vai redirecionar.

  • Se o status for loading, a condicional não faz nada enquanto estiver no estado de carregamento.
  • Se o usuário não estiver autenticado, o usuário é forçado a entrar na pagina de Login, através da função signIn().

Manipulação da Sessão do Cliente personalizada.

Dessa forma será exibido um status de carregamento na verificação inicial e todas transições de paginas serão do lado do cliente, eliminando a necessidade de verificar com o servidor e gerar novamente as páginas.

Nas paginas protegidas é só adicionar:

export default function Dashboard() {
  const { data: session } = useSession()
  return <h1>Pagina Protegida</h1>
}

Dashboard.auth = true
Enter fullscreen mode Exit fullscreen mode

Estamos usando Dashboard.auth = true para verificar se o usuário está autenticado, para todas as paginas que queremos proteger é só adicionar auth = true.

Referências: Protected Routes

Espero que esse guia possa te ajudar!

Discussion (0)