Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Podłączanie dostawców (Google, GitHub, Facebook)

W poprzednich lekcjach omówiliśmy podstawy uwierzytelniania w aplikacjach Next.js oraz różne strategie autoryzacji. Teraz skupimy się na praktycznej implementacji uwierzytelniania z popularnymi dostawcami zewnętrznymi - Google, GitHub i Facebook. Ta metoda jest niezwykle popularna, ponieważ oferuje użytkownikom szybkie i wygodne logowanie bez konieczności tworzenia nowego konta i zapamiętywania kolejnego hasła.

Dlaczego warto dodać zewnętrznych dostawców uwierzytelniania?

Logowanie z wykorzystaniem kont Google, GitHub czy Facebook przynosi kilka istotnych korzyści zarówno dla użytkowników, jak i dla nas - twórców aplikacji:

  1. Lepsze doświadczenie użytkownika - szybszy i wygodniejszy proces logowania, bez konieczności wymyślania i zapamiętywania nowych danych logowania
  2. Wyższy współczynnik rejestracji - mniejsza liczba porzuconych rejestracji dzięki uproszczonemu procesowi
  3. Większe bezpieczeństwo - dostawcy jak Google czy Facebook stosują zaawansowane mechanizmy ochrony kont
  4. Dodatkowe informacje - możliwość pozyskania dodatkowych danych o użytkowniku (za jego zgodą), takich jak zdjęcie profilowe czy informacje kontaktowe
  5. Uwierzytelnianie wieloskładnikowe - wielu dostawców oferuje już wbudowane mechanizmy 2FA/MFA

Przystąpmy więc do praktycznej implementacji logowania z tymi popularnymi dostawcami w aplikacji Next.js.

Konfiguracja OAuth dla dostawców

Zanim zaczniemy kodować, musimy utworzyć odpowiednie aplikacje OAuth u każdego z dostawców, aby uzyskać niezbędne klucze.

Google OAuth

  1. Przejdź do Google Cloud Console
  2. Utwórz nowy projekt lub wybierz istniejący
  3. Przejdź do "APIs & Services" > "Credentials"
  4. Kliknij "Create Credentials" > "OAuth client ID"
  5. Wybierz typ aplikacji (Web application)
  6. Dodaj nazwę i dozwolone przekierowania (redirect URIs):
    • http://localhost:3000/api/auth/callback/google
      (dla środowiska deweloperskiego)
    • https://twoja-domena.com/api/auth/callback/google
      (dla produkcji)
  7. Zapisz ID klienta (Client ID) i tajny klucz klienta (Client Secret)

GitHub OAuth

  1. Zaloguj się na GitHub
  2. Przejdź do Settings > Developer settings > OAuth Apps
  3. Kliknij "New OAuth App"
  4. Wypełnij formularz:
    • Application name: nazwa twojej aplikacji
    • Homepage URL: np.
      http://localhost:3000
    • Authorization callback URL:
      http://localhost:3000/api/auth/callback/github
  5. Zarejestruj aplikację
  6. Wygeneruj nowy Client Secret i zapisz go wraz z Client ID

Facebook OAuth

  1. Przejdź do Facebook Developers
  2. Utwórz nową aplikację (wybierz "For Everything Else" jako typ)
  3. W panelu aplikacji dodaj "Facebook Login" i wybierz "Web"
  4. W ustawieniach dodaj dozwolone domeny:
    • localhost
      (dla środowiska deweloperskiego)
    • twoja-domena.com
      (dla produkcji)
  5. W ustawieniach uwierzytelniania dodaj przekierowanie OAuth:
    • http://localhost:3000/api/auth/callback/facebook
  6. Zapisz App ID i App Secret

Implementacja uwierzytelniania z Next-Auth/Auth.js

Teraz, gdy mamy już klucze OAuth, możemy przystąpić do implementacji uwierzytelniania w naszej aplikacji Next.js. Będziemy używać biblioteki NextAuth.js (obecnie znanej również jako Auth.js).

1. Konfiguracja zmiennych środowiskowych

Najpierw dodajmy nasze klucze OAuth do pliku

.env.local
:

1# Podstawowa konfiguracja NextAuth.js
2NEXTAUTH_URL=http://localhost:3000
3NEXTAUTH_SECRET=twój_tajny_klucz_do_szyfrowania
4
5# Google Provider
6GOOGLE_CLIENT_ID=twoje_google_client_id
7GOOGLE_CLIENT_SECRET=twoje_google_client_secret
8
9# GitHub Provider
10GITHUB_ID=twoje_github_client_id
11GITHUB_SECRET=twoje_github_client_secret
12
13# Facebook Provider
14FACEBOOK_CLIENT_ID=twoje_facebook_app_id
15FACEBOOK_CLIENT_SECRET=twoje_facebook_app_secret

2. Konfiguracja dostawców w NextAuth

Teraz stwórzmy lub zaktualizujmy plik konfiguracyjny NextAuth.js:

1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from "next-auth";
3import GoogleProvider from "next-auth/providers/google";
4import GitHubProvider from "next-auth/providers/github";
5import FacebookProvider from "next-auth/providers/facebook";
6import { PrismaAdapter } from "@auth/prisma-adapter";
7import { PrismaClient } from "@prisma/client";
8
9const prisma = new PrismaClient();
10
11export const authOptions = {
12  adapter: PrismaAdapter(prisma),
13  providers: [
14    GoogleProvider({
15      clientId: process.env.GOOGLE_CLIENT_ID!,
16      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
17      // Możemy zdefiniować, jakie dane profilu chcemy otrzymać
18      profile(profile) {
19        return {
20          id: profile.sub,
21          name: profile.name,
22          email: profile.email,
23          image: profile.picture,
24          // Możemy dodać dodatkowe pola specyficzne dla naszej aplikacji
25          emailVerified: profile.email_verified,
26        };
27      },
28    }),
29    GitHubProvider({
30      clientId: process.env.GITHUB_ID!,
31      clientSecret: process.env.GITHUB_SECRET!,
32      profile(profile) {
33        return {
34          id: profile.id.toString(),
35          name: profile.name || profile.login,
36          email: profile.email,
37          image: profile.avatar_url,
38          // Dodatkowe pole specyficzne dla GitHub
39          username: profile.login,
40        };
41      },
42    }),
43    FacebookProvider({
44      clientId: process.env.FACEBOOK_CLIENT_ID!,
45      clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
46      profile(profile) {
47        return {
48          id: profile.id,
49          name: profile.name,
50          email: profile.email,
51          image: profile.picture.data.url,
52        };
53      },
54    }),
55    // Możemy również dodać uwierzytelnianie przez email/hasło
56    // dla użytkowników, którzy wolą tradycyjne logowanie
57  ],
58  callbacks: {
59    async session({ session, user }) {
60      // Dodaj ID użytkownika do sesji, aby łatwo identyfikować użytkownika
61      if (session.user) {
62        session.user.id = user.id;
63      }
64      return session;
65    },
66    async jwt({ token, user, account, profile }) {
67      // Jeśli mamy użytkownika (np. podczas logowania), dodaj jego ID do tokenu
68      if (user) {
69        token.uid = user.id;
70      }
71      // Możemy też dodać informacje o dostawcy uwierzytelniania
72      if (account) {
73        token.provider = account.provider;
74      }
75      return token;
76    },
77  },
78  pages: {
79    signIn: '/auth/login',  // Niestandardowa strona logowania
80    signOut: '/auth/logout', // Niestandardowa strona wylogowania
81    error: '/auth/error',    // Strona błędów uwierzytelniania
82  },
83  session: {
84    strategy: 'database',  // Używamy bazy danych do przechowywania sesji
85    maxAge: 30 * 24 * 60 * 60, // 30 dni
86  },
87  debug: process.env.NODE_ENV === 'development',
88};
89
90const handler = NextAuth(authOptions);
91export { handler as GET, handler as POST };

3. Stworzenie interfejsu użytkownika do logowania

Teraz stwórzmy przyjazny interfejs logowania, który będzie oferował wszystkie skonfigurowane metody uwierzytelniania:

1// app/auth/login/page.tsx
2'use client';
3
4import { signIn, useSession } from 'next-auth/react';
5import { useRouter, useSearchParams } from 'next/navigation';
6import { useEffect, useState } from 'react';
7
8export default function LoginPage() {
9  const router = useRouter();
10  const searchParams = useSearchParams();
11  const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
12  const { status } = useSession();
13  
14  const [email, setEmail] = useState('');
15  const [password, setPassword] = useState('');
16  const [isLoading, setIsLoading] = useState(false);
17  const [error, setError] = useState('');
18  
19  // Jeśli użytkownik jest już zalogowany, przekieruj go
20  useEffect(() => {
21    if (status === 'authenticated') {
22      router.push(callbackUrl);
23    }
24  }, [status, router, callbackUrl]);
25  
26  // Obsługa logowania przez email/hasło (jeśli skonfigurowano)
27  const handleEmailLogin = async (e: React.FormEvent) => {
28    e.preventDefault();
29    setIsLoading(true);
30    setError('');
31    
32    try {
33      const result = await signIn('credentials', {
34        redirect: false,
35        email,
36        password,
37      });
38      
39      if (result?.error) {
40        setError('Nieprawidłowy email lub hasło');
41      } else {
42        router.push(callbackUrl);
43      }
44    } catch (error) {
45      setError('Wystąpił błąd podczas logowania');
46    } finally {
47      setIsLoading(false);
48    }
49  };
50  
51  if (status === 'loading') {
52    return (
53      <div className="flex justify-center items-center min-h-screen">
54        <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500" />
55      </div>
56    );
57  }
58  
59  return (
60    <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-lg">
61      <h1 className="text-2xl font-bold mb-6 text-center">Zaloguj się do aplikacji</h1>
62      
63      {error && (
64        <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
65          {error}
66        </div>
67      )}
68      
69      <div className="space-y-4 mb-6">
70        <button
71          onClick={() => signIn('google', { callbackUrl })}
72          className="w-full flex items-center justify-center gap-3 p-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
73          disabled={isLoading}
74        >
75          <svg viewBox="0 0 24 24" width="20" height="20">
76            {/* Logo Google */}
77            <path
78              d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
79              fill="#4285F4"
80            />
81            <path
82              d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
83              fill="#34A853"
84            />
85            <path
86              d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
87              fill="#FBBC05"
88            />
89            <path
90              d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
91              fill="#EA4335"
92            />
93          </svg>
94          Kontynuuj z Google
95        </button>
96        
97        <button
98          onClick={() => signIn('github', { callbackUrl })}
99          className="w-full flex items-center justify-center gap-3 p-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
100          disabled={isLoading}
101        >
102          <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
103            {/* Logo GitHub */}
104            <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
105          </svg>
106          Kontynuuj z GitHub
107        </button>
108        
109        <button
110          onClick={() => signIn('facebook', { callbackUrl })}
111          className="w-full flex items-center justify-center gap-3 p-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
112          disabled={isLoading}
113        >
114          <svg viewBox="0 0 24 24" width="20" height="20" fill="#1877F2">
115            {/* Logo Facebook */}
116            <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
117          </svg>
118          Kontynuuj z Facebook
119        </button>
120      </div>
121      
122      {/* Opcjonalnie: formularz logowania email/hasło */}
123      <div className="relative my-6">
124        <div className="absolute inset-0 flex items-center">
125          <div className="w-full border-t border-gray-300"></div>
126        </div>
127        <div className="relative flex justify-center text-sm">
128          <span className="px-2 bg-white text-gray-500">
129            lub użyj emaila
130          </span>
131        </div>
132      </div>
133      
134      <form onSubmit={handleEmailLogin} className="space-y-4">
135        <div>
136          <label htmlFor="email" className="block text-sm font-medium mb-1">
137            Email
138          </label>
139          <input
140            id="email"
141            type="email"
142            value={email}
143            onChange={(e) => setEmail(e.target.value)}
144            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
145            required
146          />
147        </div>
148        
149        <div>
150          <label htmlFor="password" className="block text-sm font-medium mb-1">
151            Hasło
152          </label>
153          <input
154            id="password"
155            type="password"
156            value={password}
157            onChange={(e) => setPassword(e.target.value)}
158            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
159            required
160          />
161        </div>
162        
163        <button
164          type="submit"
165          className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
166          disabled={isLoading}
167        >
168          {isLoading ? 'Logowanie...' : 'Zaloguj się'}
169        </button>
170      </form>
171      
172      <div className="mt-6 text-center">
173        <p className="text-sm text-gray-600">
174          Nie masz jeszcze konta?{' '}
175          <a href="/auth/register" className="text-blue-600 hover:underline">
176            Zarejestruj się
177          </a>
178        </p>
179      </div>
180    </div>
181  );
182}

4. Przygotowanie schematu bazy danych Prisma

Jeśli korzystamy z adaptera Prisma, musimy zapewnić odpowiedni schemat bazy danych:

1// prisma/schema.prisma
2datasource db {
3  provider = "postgresql" // lub inny dostawca bazy danych
4  url      = env("DATABASE_URL")
5}
6
7generator client {
8  provider = "prisma-client-js"
9}
10
11model Account {
12  id                String  @id @default(cuid())
13  userId            String
14  type              String
15  provider          String
16  providerAccountId String
17  refresh_token     String? @db.Text
18  access_token      String? @db.Text
19  expires_at        Int?
20  token_type        String?
21  scope             String?
22  id_token          String? @db.Text
23  session_state     String?
24
25  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
26
27  @@unique([provider, providerAccountId])
28}
29
30model Session {
31  id           String   @id @default(cuid())
32  sessionToken String   @unique
33  userId       String
34  expires      DateTime
35  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
36}
37
38model User {
39  id            String    @id @default(cuid())
40  name          String?
41  email         String?   @unique
42  emailVerified DateTime?
43  image         String?
44  accounts      Account[]
45  sessions      Session[]
46  // Dodatkowe pola specyficzne dla twojej aplikacji
47  username      String?   @unique
48  role          String?   @default("user")
49  createdAt     DateTime  @default(now())
50  updatedAt     DateTime  @updatedAt
51}
52
53model VerificationToken {
54  identifier String
55  token      String   @unique
56  expires    DateTime
57
58  @@unique([identifier, token])
59}

5. Utworzenie SessionProvider dla całej aplikacji

Aby uwierzytelnianie działało poprawnie w całej aplikacji, dodajmy

SessionProvider
w głównym layoucie:

1// app/providers.tsx
2'use client';
3
4import { SessionProvider } from "next-auth/react";
5import { ReactNode } from "react";
6
7export function Providers({ children }: { children: ReactNode }) {
8  return <SessionProvider>{children}</SessionProvider>;
9}
10
11// app/layout.tsx
12import { Providers } from "./providers";
13import "./globals.css";
14
15export const metadata = {
16  title: "Moja Aplikacja",
17  description: "Opis mojej aplikacji",
18};
19
20export default function RootLayout({
21  children,
22}: {
23  children: React.ReactNode;
24}) {
25  return (
26    <html lang="pl">
27      <body>
28        <Providers>{children}</Providers>
29      </body>
30    </html>
31  );
32}

6. Dodanie komponentu nawigacyjnego z obsługą stanu uwierzytelniania

Teraz stwórzmy komponent nawigacyjny, który będzie pokazywał różne opcje w zależności od tego, czy użytkownik jest zalogowany:

1// components/Navigation.tsx
2'use client';
3
4import { signOut, useSession } from 'next-auth/react';
5import Link from 'next/link';
6import Image from 'next/image';
7
8export default function Navigation() {
9  const { data: session, status } = useSession();
10  const isLoading = status === 'loading';
11  
12  return (
13    <nav className="bg-white shadow-sm">
14      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
15        <div className="flex justify-between h-16">
16          <div className="flex items-center">
17            <Link href="/" className="flex-shrink-0 flex items-center">
18              <span className="text-xl font-bold">MojaAplikacja</span>
19            </Link>
20            <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
21              <Link 
22                href="/" 
23                className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
24              >
25                Strona główna
26              </Link>
27              <Link 
28                href="/features" 
29                className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
30              >
31                Funkcje
32              </Link>
33              <Link 
34                href="/pricing" 
35                className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
36              >
37                Cennik
38              </Link>
39            </div>
40          </div>
41          
42          <div className="flex items-center">
43            {isLoading ? (
44              <div className="h-8 w-8 animate-pulse rounded-full bg-gray-200" />
45            ) : session ? (
46              <div className="relative ml-3">
47                <div className="flex items-center space-x-4">
48                  <Link 
49                    href="/dashboard" 
50                    className="text-sm font-medium text-gray-700 hover:text-gray-800"
51                  >
52                    Dashboard
53                  </Link>
54                  
55                  <div className="flex items-center space-x-2">
56                    {session.user?.image ? (
57                      <Image
58                        className="h-8 w-8 rounded-full"
59                        src={session.user.image}
60                        alt={session.user.name || "Avatar"}
61                        width={32}
62                        height={32}
63                      />
64                    ) : (
65                      <div className="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm">
66                        {session.user?.name?.charAt(0) || "U"}
67                      </div>
68                    )}
69                    
70                    <span className="text-sm font-medium text-gray-700">
71                      {session.user?.name || session.user?.email?.split('@')[0]}
72                    </span>
73                  </div>
74                  
75                  <button
76                    onClick={() => signOut({ callbackUrl: '/' })}
77                    className="text-sm font-medium text-gray-700 hover:text-gray-800"
78                  >
79                    Wyloguj
80                  </button>
81                </div>
82              </div>
83            ) : (
84              <div className="space-x-4">
85                <Link 
86                  href="/auth/login" 
87                  className="text-gray-700 hover:text-gray-900 font-medium"
88                >
89                  Zaloguj się
90                </Link>
91                <Link 
92                  href="/auth/register" 
93                  className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 font-medium"
94                >
95                  Zarejestruj się
96                </Link>
97              </div>
98            )}
99          </div>
100        </div>
101      </div>
102    </nav>
103  );
104}

7. Utworzenie strony Dashboard dostępnej tylko dla zalogowanych użytkowników

Stwórzmy stronę Dashboard, która będzie dostępna tylko dla zalogowanych użytkowników:

1// app/dashboard/page.tsx
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "../api/auth/[...nextauth]/route";
5
6export default async function DashboardPage() {
7  const session = await getServerSession(authOptions);
8  
9  if (!session) {
10    redirect("/auth/login?callbackUrl=/dashboard");
11  }
12  
13  return (
14    <div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
15      <div className="px-4 py-6 sm:px-0">
16        <h1 className="text-2xl font-bold mb-4">Dashboard</h1>
17        
18        <div className="bg-white shadow overflow-hidden sm:rounded-lg">
19          <div className="px-4 py-5 sm:px-6">
20            <h3 className="text-lg leading-6 font-medium text-gray-900">
21              Informacje o profilu
22            </h3>
23            <p className="mt-1 max-w-2xl text-sm text-gray-500">
24              Szczegóły Twojego konta i ustawienia.
25            </p>
26          </div>
27          <div className="border-t border-gray-200">
28            <dl>
29              <div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
30                <dt className="text-sm font-medium text-gray-500">Imię i nazwisko</dt>
31                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
32                  {session.user?.name || "Nie podano"}
33                </dd>
34              </div>
35              <div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
36                <dt className="text-sm font-medium text-gray-500">Adres email</dt>
37                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
38                  {session.user?.email}
39                </dd>
40              </div>
41              <div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
42                <dt className="text-sm font-medium text-gray-500">Metoda logowania</dt>
43                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
44                  {/* To pole wymaga rozszerzenia tokenu o informacje o dostawcy */}
45                  Zewnętrzny dostawca uwierzytelniania
46                </dd>
47              </div>
48            </dl>
49          </div>
50        </div>
51      </div>
52    </div>
53  );
54}

8. Utworzenie middleware dla ochrony ścieżek

Zabezpieczmy określone ścieżki w naszej aplikacji za pomocą middleware Next.js:

1// middleware.ts
2import { NextResponse } from "next/server";
3import type { NextRequest } from "next/server";
4import { getToken } from "next-auth/jwt";
5
6export async function middleware(request: NextRequest) {
7  const token = await getToken({ req: request });
8  
9  const isAuthPage = request.nextUrl.pathname.startsWith('/auth');
10  
11  if (isAuthPage) {
12    if (token) {
13      return NextResponse.redirect(new URL('/dashboard', request.url));
14    }
15    return NextResponse.next();
16  }
17  
18  if (!token) {
19    const url = new URL('/auth/login', request.url);
20    url.searchParams.set('callbackUrl', request.nextUrl.pathname);
21    return NextResponse.redirect(url);
22  }
23  
24  return NextResponse.next();
25}
26
27// Określ ścieżki, dla których ma być stosowane middleware
28export const config = {
29  matcher: [
30    '/dashboard/:path*', 
31    '/profile/:path*', 
32    '/settings/:path*',
33    '/auth/:path*'
34  ],
35};

Zaawansowana konfiguracja

Modyfikacja danych profilu użytkownika

Możemy modyfikować dane profilowe z dostawców zewnętrznych przed zapisaniem ich w naszej bazie danych:

1GoogleProvider({
2  clientId: process.env.GOOGLE_CLIENT_ID!,
3  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
4  profile(profile) {
5    // Transformacja danych profilu
6    return {
7      id: profile.sub,
8      name: profile.name,
9      email: profile.email,
10      image: profile.picture,
11      // Dodatkowe pola specyficzne dla aplikacji
12      role: profile.email.endsWith('@twojafirma.com') ? 'admin' : 'user',
13      emailVerified: profile.email_verified ? new Date() : null,
14    };
15  },
16});

Łączenie kont z różnych dostawców

NextAuth.js automatycznie obsługuje łączenie kont różnych dostawców, jeśli używają tego samego adresu email. Jeśli chcemy to dostosować, możemy zmodyfikować funkcję

linkAccount
w callbackach:

1callbacks: {
2  // ...inne callbacki
3  async linkAccount({ user, account, profile }) {
4    // Tutaj możemy wykonać niestandardową logikę łączenia kont
5    console.log(`Linking account: ${account.provider} for user ${user.id}`);
6    return true;
7  },
8}

Dodanie własnej logiki do procesu uwierzytelniania

Możemy dodać dodatkowe kroki w procesie uwierzytelniania za pomocą callbacku

signIn
:

1callbacks: {
2  // ...inne callbacki
3  async signIn({ user, account, profile, email, credentials }) {
4    // Sprawdź, czy użytkownik może się zalogować
5    // Na przykład: sprawdź, czy email jest w domenie firmy
6    const allowedDomains = ['twojafirma.com', 'partner.com'];
7    const emailDomain = user.email?.split('@')[1];
8    
9    if (emailDomain && !allowedDomains.includes(emailDomain)) {
10      return false; // Odmów dostępu
11    }
12    
13    // Możemy też zaktualizować dane użytkownika przy każdym logowaniu
14    if (user.id) {
15      await prisma.user.update({
16        where: { id: user.id },
17        data: { lastLogin: new Date() }
18      });
19    }
20    
21    return true; // Zezwól na dostęp
22  },
23}

Obsługa błędów uwierzytelniania

Stwórzmy niestandardową stronę błędów uwierzytelniania:

1// app/auth/error/page.tsx
2'use client';
3
4import { useSearchParams } from "next/navigation";
5import Link from "next/link";
6
7export default function AuthErrorPage() {
8  const searchParams = useSearchParams();
9  const error = searchParams.get('error');
10  
11  let errorMessage = "Wystąpił błąd podczas próby uwierzytelnienia.";
12  let errorDescription = "Prosimy spróbować ponownie lub skontaktować się z administracją.";
13  
14  // Mapuj kody błędów na przyjazne dla użytkownika komunikaty
15  switch (error) {
16    case "Configuration":
17      errorMessage = "Wystąpił błąd konfiguracji serwera.";
18      errorDescription = "Skontaktuj się z administratorem systemu.";
19      break;
20    case "AccessDenied":
21      errorMessage = "Brak dostępu.";
22      errorDescription = "Nie masz wystarczających uprawnień, aby się zalogować.";
23      break;
24    case "Verification":
25      errorMessage = "Link weryfikacyjny wygasł lub jest nieprawidłowy.";
26      errorDescription = "Spróbuj wysłać link weryfikacyjny ponownie.";
27      break;
28    case "OAuthSignin":
29    case "OAuthCallback":
30    case "OAuthCreateAccount":
31    case "EmailCreateAccount":
32    case "Callback":
33      errorMessage = "Wystąpił błąd podczas komunikacji z dostawcą uwierzytelniania.";
34      errorDescription = "Spróbuj ponownie za chwilę lub wybierz inną metodę logowania.";
35      break;
36    case "OAuthAccountNotLinked":
37      errorMessage = "Konto z tym adresem email już istnieje.";
38      errorDescription = "Zaloguj się metodą, której użyłeś wcześniej.";
39      break;
40    case "EmailSignin":
41      errorMessage = "Nie można wysłać emaila z linkiem do logowania.";
42      errorDescription = "Sprawdź, czy podany adres email jest poprawny.";
43      break;
44    case "CredentialsSignin":
45      errorMessage = "Nieprawidłowe dane logowania.";
46      errorDescription = "Sprawdź, czy podany email i hasło są poprawne.";
47      break;
48    case "SessionRequired":
49      errorMessage = "Wymagane zalogowanie.";
50      errorDescription = "Zaloguj się, aby uzyskać dostęp do tej strony.";
51      break;
52    default:
53      // Domyślny komunikat już ustawiony
54      break;
55  }
56  
57  return (
58    <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
59      <div className="max-w-md w-full space-y-8">
60        <div>
61          <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
62            {errorMessage}
63          </h2>
64          <p className="mt-2 text-center text-sm text-gray-600">
65            {errorDescription}
66          </p>
67        </div>
68        <div className="flex flex-col items-center space-y-4">
69          <Link 
70            href="/auth/login"
71            className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none"
72          >
73            Wróć do strony logowania
74          </Link>
75          <Link 
76            href="/"
77            className="text-sm text-blue-600 hover:text-blue-500"
78          >
79            Wróć do strony głównej
80          </Link>
81        </div>
82      </div>
83    </div>
84  );
85}

Podsumowanie

W tym module omówiliśmy szczegółowo, jak zaimplementować uwierzytelnianie z popularnymi dostawcami zewnętrznymi (Google, GitHub, Facebook) w aplikacji Next.js. Poznaliśmy:

  1. Proces rejestracji aplikacji u dostawców OAuth i uzyskiwania kluczy
  2. Konfigurację NextAuth.js / Auth.js z różnymi dostawcami
  3. Tworzenie interfejsu użytkownika do logowania i rejestracji
  4. Zabezpieczanie tras za pomocą middleware
  5. Dostosowywanie procesu uwierzytelniania do naszych potrzeb
  6. Obsługę błędów uwierzytelniania

Dzięki tej implementacji nasza aplikacja Next.js zyskuje profesjonalny, wieloplatformowy system uwierzytelniania, który poprawia doświadczenie użytkownika i jednocześnie wzmacnia bezpieczeństwo naszej aplikacji.

W następnym module zajmiemy się szczegółowo sesjami użytkownika i zarządzaniem tokenami, co pozwoli nam lepiej zrozumieć, jak działają mechanizmy uwierzytelniania po zalogowaniu użytkownika.

Ir a CodeWorlds