Autentykacja to proces weryfikacji tożsamości użytkownika, np. za pomocą nazwy użytkownika i hasła.

Autoryzacja to proces decydowania, do których części aplikacji użytkownik ma dostęp.

Opis jest tworzony dla wersji:

 "next": "14.2.3",
 "next-auth": "^4.24.7"

Middleware

Mechanizm middleware pozwala na przetworzenie każdego żądania do serwera, zanim dotrze ono do docelowego endpointu.

Middleware konfigurujemy tak, aby sprawdzał, czy aktualna ścieżka jest dostępna bez tokena.

export async function middleware(req: NextRequest) {

  // Pobiera JWT token z żądania do weryfikacji tożsamości użytkownika.
  const token = await getToken({ req, secret: process.env.AUTH_SECRET });
  const { pathname } = req.nextUrl;

  // Lista ścieżek dostępnych publicznie, nie wymagających autentykacji.
  const publicPaths = ["/login", "/signin", "/signup", "/auth/signout"];

  // Pozwala kontynuować żądanie, jeśli ścieżka jest publiczna lub token jest ważny.
  if (publicPaths.includes(pathname) || token) {
    return NextResponse.next();
  }

  // Przekierowuje na stronę logowania, jeśli ścieżka nie jest publiczna i brak ważnego tokena.
  const url = req.nextUrl.clone();
  url.pathname = "/login";
  return NextResponse.redirect(url);
}

Jednak to, które dokładnie żądania będą przechodzić przez middleware, zależy od konfiguracji matcherów w sekcji config middleware.

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|.*\\.png$|.*\\.webp$).*)"],
};

Matcher w konfiguracji middleware określa, które żądania będą przetwarzane przez middleware. Matcher '[/((?!api|_next/static|_next/image|.\.png$|.\.webp$).*)]’ oznacza, że middleware zostanie zastosowane do wszystkich URL-i, które nie pasują do określonych wzorców, wykluczając zasoby statyczne i API.

Instalacja NextAuth.js

Aby zainstalować NextAuth.js, użyj polecenia:

npm install next-auth

W pliku .env dodajemy AUTH_SECRET, który możemy wygenerować za pomocą:

openssl rand -base64 32

W uproszczeniu AUTH_SECRET w NextAuth.js to specjalny, tajny klucz, który służy do zabezpieczania danych związanych z logowaniem i sesjami użytkowników w aplikacji internetowej.

Tworzenie ścieżki do logowania

Dodajemy ścieżkę url /login

W katalogu: app/login dodajemy plik page.tsx, w którym tworzymy komponent LoginPage

import { signIn } from "next-auth/react";

export default function LoginPage() {
   const [email, setEmail] = useState("");
   const [password, setPassword] = useState("");
   const [error, setError] = useState(false);
    
   const handleSubmit = async (event: FormEvent) => {
    event.preventDefault();

    if (email.length === 0 || password.length === 0) {
      console.log(`Blędne logowanie pageLogin`);
      setError(true);
      return;
    }
    try {
      const result = await signIn("credentials", {
        redirect: false,
        email,
        password,
      });

      if (result?.error) {
        // W przypadku błędu informuje o problemie
        console.log(`Blędne logowanie pageLogin`);
        setError(true);
      } else {
        // Przekieruj na stronę główną po pomyślnym logowaniu
        console.log(`Pomyślne logowanie pageLogin`);
        router.push("/dashboard");
      }
    } catch (errorInfo) {
    

    }
    return (
      <main className="flex items-center justify-center md:h-screen">
<div className="min-h-screen flex flex-col justify-center items-center">
       {error && <p className="text-red-500 text-xs italic mt-4">Niepoprawny email lub hasło.</p>}
       <form onSubmit={handleSubmit} className="space-y-6 bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
         <div>
           <label htmlFor="email" className="block text-gray-700 text-sm font-bold mb-2">
             Email:
           </label>
           <input type="email" id="email" name="email" required className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" onChange={(e) => setEmail(e.target.value)} />
         </div>
         <div>
           <label htmlFor="password" className="block text-gray-700 text-sm font-bold mb-2">
             Hasło:
           </label>
           <input type="password" id="password" name="password" required className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" onChange={(e) => setPassword(e.target.value)} />
         </div>
         <div>
           <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
             Zaloguj się
           </button>
           <br />
           <br />
           <Link href="/signup" className="bg-green-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
             Zarejestruj się
           </Link>
         </div>
       </form>
     </div>
    </main>
    );
  }

Jeden fragment wymaga omówienia

  const result = await signIn("credentials", {
      redirect: false,
      email,
      password,
    });
  • credentials: To identyfikator dostawcy, który mówi, że logowanie będzie odbywało się przez email i hasło.
  • redirect: false: Oznacza, że NextAuth.js nie będzie próbował automatycznie przekierować po udanym logowaniu, lecz zwróci odpowiedź do klienta.
  • email, password: To dane wprowadzone przez użytkownika.

Tworzenie ścieżki do rejestracji

Dodajemy ścieżkę url /signup

W katalogu: app/signup dodajemy plik page.tsx, w którym tworzymy komponent SignupPage.

Tworzymy routing do obsługi autentykacji

W tym celu tworzymy router w ścieżce: app/api/auth/[…nextauth]/route.ts

import { NextApiRequest, NextApiResponse } from "next";
import NextAuth, { NextAuthOptions, SessionStrategy } from "next-auth";
import { authConfig } from "@/app/api/auth/auth.config";

const nextAuthConfig: NextAuthOptions = {
  ...authConfig,
  session: {
    strategy: "jwt" as SessionStrategy,
  },
};

export async function GET(req: NextApiRequest, res: NextApiResponse) {
   return await NextAuth(req, res, nextAuthConfig);
}

export async function POST(req: NextApiRequest, res: NextApiResponse) {
   return await NextAuth(req, res, nextAuthConfig);
}

Plik ten służy jako centralny punkt obsługi wszystkich żądań związanych z autentykacją w aplikacji, korzystając z konfiguracji dostarczonej przez NextAuth.js. Czyli operacje autentykacyjne, takie jak logowanie czy wylogowywanie.

auth.config.ts

Tworzymy plik auth.config.ts w katalogu głównym aplikacji(albo tak jak ja /app/api/auth, tylko potem musimy poprawnie zaimportować), w którym musimy exportować obiekt authConfig.

import { PrismaAdapter } from "@next-auth/prisma-adapter";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import prisma from "../../../lib/prisma";
import bcrypt from "bcrypt";

export const authConfig = {
  providers: [
  //Provider CredentialsProvider umożliwia logowanie za pomocą emaila i hasła
    CredentialsProvider({
      name: "Credentials",
      //Definiujemy pola, które użytkownik musi wypełnić na formularzu logowania, czyli email i hasło.
      credentials: {
        email: { label: "Email 1", type: "email" },
        password: { label: "Password 2", type: "password" },
      },
      //funkcja authorize jest wywoływana podczas próby logowania
      authorize: async (credentials) => {
        const { email, password } = credentials as { email: string; password: string };
        //szukamy usera o podanym emailu
        const user = await prisma.user.findUnique({ where: { email } });

        //sprawdzenie poprawnego logowania
        if (user && bcrypt.compareSync(password, user.password)) {
          console.log("Poprawne logowanie auth")
          return { id: user.id, username: user.name, email: user.email };
        } else {
          console.log("Błędne logowanie auth")
          return null;
        }
      },
    }),
  ],
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: "jwt",
  },
  //nasz klucz z pliku .env
  secret: process.env.AUTH_SECRET,
  callbacks: {
    //Modyfikujemy tokena JWT
    jwt: async ({ token, user }) => {
      if (user) {
        token.id = user.id;
        token.username = user.username;
      }
      return token;
    },
    
    //Modyfikujemy obiekt sesji z którego skorzystamy potem 
    session: async ({ session, token }) => {
      if (token) {
        session.user = { id: token.id, name: token.username, email: token.email };
        session.username = token.username;
      }
      return session;
    },
    redirect: async ({ url, baseUrl, session }) => {
      // Przekieruj na stronę główną, jeśli użytkownik jest zalogowany
      if (session && session.user) {
        return baseUrl + '/dashboard';
      }
    
      // Użytkownik chce dostać się na stronę, na którą ma uprawnienia - przekieruj go tam
      if (url.startsWith(baseUrl)) {
        return url;
      }
    
      // Domyślnie, jeśli żadne z powyższych nie jest spełnione, przekieruj na stronę logowania
      return baseUrl + '/login';
    },
  },
  //Określamy ściezki do stron logowania
  pages: {
    signIn: "/login",
    signOut: "/auth/signout",
    error: "/login",
  },
};

export default NextAuth(authConfig);

JWT

Tokeny JWT (JSON Web Tokens) są szeroko stosowane w aplikacjach internetowych do zarządzania uwierzytelnianiem i autoryzacją.

Po pomyślnym uwierzytelnieniu użytkownika, serwer generuje token JWT, wstawiając do niego odpowiednie dane, następnie podpisuje go używając tajnego klucza i wysyła do klienta. Klient może następnie przesyłać ten token z powrotem do serwera z każdym żądaniem, zwykle w nagłówku HTTP Authorization. Serwer, po otrzymaniu tokena, weryfikuje podpis, aby upewnić się, że token nie został zmieniony, a następnie odczytuje payload, aby uzyskać informacje o użytkowniku i jego uprawnieniach.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *