Next.js course ยท Module 7: Authentication and Authorization
Connecting providers (Google, GitHub, Facebook)
In this lesson6
In previous lessons, we covered the basics of authentication in Next.js applications and various authorization strategies. Now we will focus on the practical implementation of authentication with popular external providers - Google, GitHub, and Facebook. This method is extremely popular because it offers users a fast and convenient login without the need to create a new account and remember another password.
Why add external authentication providers?
Signing in with Google, GitHub, or Facebook accounts brings several important benefits for both users and us - application developers:
- Better user experience - faster and more convenient login process, without the need to invent and remember new login credentials
- Higher registration rate - fewer abandoned registrations thanks to the simplified process
- Greater security - providers like Google or Facebook use advanced account protection mechanisms
- Additional information - ability to obtain additional data about the user (with their consent), such as profile picture or contact information
- Multi-factor authentication - many providers already offer built-in 2FA/MFA mechanisms
So let's proceed to the practical implementation of login with these popular providers in a Next.js application.
OAuth configuration for providers
Before we start coding, we need to create appropriate OAuth applications at each provider to obtain the necessary keys.
Google OAuth
- Go to Google Cloud Console
- Create a new project or select an existing one
- Go to "APIs & Services" > "Credentials"
- Click "Create Credentials" > "OAuth client ID"
- Select the application type (Web application)
- Add a name and allowed redirect URIs:
http://localhost:3000/api/auth/callback/google(for the development environment)https://your-domain.com/api/auth/callback/google(for production)
- Save the Client ID and the Client Secret
GitHub OAuth
- Log in to GitHub
- Go to Settings > Developer settings > OAuth Apps
- Click "New OAuth App"
- Fill out the form:
- Application name: the name of your application
- Homepage URL: e.g.
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/api/auth/callback/github
- Register the application
- Generate a new Client Secret and save it together with the Client ID
Facebook OAuth
- Go to Facebook Developers
- Create a new application (select "For Everything Else" as the type)
- In the application panel, add "Facebook Login" and select "Web"
- In the settings, add the allowed domains:
localhost(for the development environment)your-domain.com(for production)
- In the authentication settings, add the OAuth redirect:
http://localhost:3000/api/auth/callback/facebook
- Save the App ID and App Secret
Implementing authentication with NextAuth/Auth.js
Now that we have the OAuth keys, we can proceed to implement authentication in our Next.js application. We will use the NextAuth.js library (now also known as Auth.js).
1. Environment variables configuration
First, letโs add our OAuth keys to the .env.local file:
1# Basic NextAuth.js configuration
2NEXTAUTH_URL=http://localhost:3000
3NEXTAUTH_SECRET=your_secret_key_for_encryption
4
5# Google Provider
6GOOGLE_CLIENT_ID=your_google_client_id
7GOOGLE_CLIENT_SECRET=your_google_client_secret
8
9# GitHub Provider
10GITHUB_ID=your_github_client_id
11GITHUB_SECRET=your_github_client_secret
12
13# Facebook Provider
14FACEBOOK_CLIENT_ID=your_facebook_app_id
15FACEBOOK_CLIENT_SECRET=your_facebook_app_secret2. Provider configuration in NextAuth
Now let's create or update the NextAuth.js configuration file:
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 // We can define what profile data we want to receive
18 profile(profile) {
19 return {
20 id: profile.sub,
21 name: profile.name,
22 email: profile.email,
23 image: profile.picture,
24 // We can add additional fields specific to our application
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 // Additional field specific to 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 // We can also add email/password authentication
56 // for users who prefer traditional login
57 ],
58 callbacks: {
59 async session({ session, user }) {
60 // Add user ID to the session to easily identify the user
61 if (session.user) {
62 session.user.id = user.id;
63 }
64 return session;
65 },
66 async jwt({ token, user, account, profile }) {
67 // If we have a user (e.g. during login), add their ID to the token
68 if (user) {
69 token.uid = user.id;
70 }
71 // We can also add information about the authentication provider
72 if (account) {
73 token.provider = account.provider;
74 }
75 return token;
76 },
77 },
78 pages: {
79 signIn: '/auth/login', // Custom login page
80 signOut: '/auth/logout', // Custom logout page
81 error: '/auth/error', // Authentication error page
82 },
83 session: {
84 strategy: 'database', // We use the database to store sessions
85 maxAge: 30 * 24 * 60 * 60, // 30 days
86 },
87 debug: process.env.NODE_ENV === 'development',
88};
89
90const handler = NextAuth(authOptions);
91export { handler as GET, handler as POST };3. Creating a user interface for login
Now let's create a friendly login interface that will offer all configured authentication methods:
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 // If the user is already logged in, redirect them
20 useEffect(() => {
21 if (status === 'authenticated') {
22 router.push(callbackUrl);
23 }
24 }, [status, router, callbackUrl]);
25
26 // Email/password login handling (if configured)
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('Invalid email or password');
41 } else {
42 router.push(callbackUrl);
43 }
44 } catch (error) {
45 setError('An error occurred during login');
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">Log in to the application</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 Continue with 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 Continue with 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 Continue with Facebook
119 </button>
120 </div>
121
122 {/* Optional: email/password login form */}
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 or use email
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 Password
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 ? 'Logging in...' : 'Log in'}
169 </button>
170 </form>
171
172 <div className="mt-6 text-center">
173 <p className="text-sm text-gray-600">
174 Don't have an account yet?{' '}
175 <a href="/auth/register" className="text-blue-600 hover:underline">
176 Sign up
177 </a>
178 </p>
179 </div>
180 </div>
181 );
182}4. Preparing the Prisma database schema
If we are using the Prisma adapter, we must provide an appropriate database schema:
1// prisma/schema.prisma
2datasource db {
3 provider = "postgresql" // or another database provider
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 // Additional fields specific to your application
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. Creating a SessionProvider for the entire application
For authentication to work properly across the entire application, let's add SessionProvider in the main layout:
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: "My Application",
17 description: "Description of my application",
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. Adding a navigation component with authentication state support
Now let's create a navigation component that will show different options depending on whether the user is logged in:
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="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 Home page
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 Features
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 Pricing
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 Log out
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 Log in
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 Sign up
96 </Link>
97 </div>
98 )}
99 </div>
100 </div>
101 </div>
102 </nav>
103 );
104}7. Creating a Dashboard page accessible only to logged-in users
Let's create a Dashboard page that will be accessible only to logged-in users:
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 Profile information
22 </h3>
23 <p className="mt-1 max-w-2xl text-sm text-gray-500">
24 Your account details and settings.
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">Full name</dt>
31 <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
32 {session.user?.name || "Not provided"}
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">Address 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">Login method</dt>
43 <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
44 {/* This field requires extending the token with provider information */}
45 External authentication provider
46 </dd>
47 </div>
48 </dl>
49 </div>
50 </div>
51 </div>
52 </div>
53 );
54}8. Creating middleware for path protection
Let's secure specific paths in our application using Next.js middleware:
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// Specify paths for which the middleware should apply
28export const config = {
29 matcher: [
30 '/dashboard/:path*',
31 '/profile/:path*',
32 '/settings/:path*',
33 '/auth/:path*'
34 ],
35};Advanced configuration
Modifying user profile data
We can modify profile data from external providers before saving them to our database:
1GoogleProvider({
2 clientId: process.env.GOOGLE_CLIENT_ID!,
3 clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
4 profile(profile) {
5 // Transforming profile data
6 return {
7 id: profile.sub,
8 name: profile.name,
9 email: profile.email,
10 image: profile.picture,
11 // Additional fields specific to the application
12 role: profile.email.endsWith('@yourcompany.com') ? 'admin' : 'user',
13 emailVerified: profile.email_verified ? new Date() : null,
14 };
15 },
16});Linking accounts from different providers
NextAuth.js automatically handles linking accounts from different providers if they use the same email address. If we want to customize this, we can modify the function linkAccount in the callbacks:
1callbacks: {
2 // ...other callbacks
3 async linkAccount({ user, account, profile }) {
4 // Here we can perform custom account linking logic
5 console.log(`Linking account: ${account.provider} for user ${user.id}`);
6 return true;
7 },
8}Adding custom logic to the authentication process
We can add additional steps in the authentication process using the callback signIn:
1callbacks: {
2 // ...other callbacks
3 async signIn({ user, account, profile, email, credentials }) {
4 // Check if the user can log in
5 // For example: check if the email is in the company domain
6 const allowedDomains = ['yourcompany.com', 'partner.com'];
7 const emailDomain = user.email?.split('@')[1];
8
9 if (emailDomain && !allowedDomains.includes(emailDomain)) {
10 return false; // Deny access
11 }
12
13 // We can also update user data on each login
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; // Allow access
22 },
23}Error handling authentication
Let's create a custom authentication error page:
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 = "An error occurred during authentication attempt.";
12 let errorDescription = "Please try again or contact administration.";
13
14 // Map error codes to user-friendly messages
15 switch (error) {
16 case "Configuration":
17 errorMessage = "A server configuration error occurred.";
18 errorDescription = "Contact the system administrator.";
19 break;
20 case "AccessDenied":
21 errorMessage = "Access denied.";
22 errorDescription = "You do not have sufficient permissions to sign in.";
23 break;
24 case "Verification":
25 errorMessage = "The verification link has expired or is invalid.";
26 errorDescription = "Try sending the verification link again.";
27 break;
28 case "OAuthSignin":
29 case "OAuthCallback":
30 case "OAuthCreateAccount":
31 case "EmailCreateAccount":
32 case "Callback":
33 errorMessage = "An error occurred during communication with the authentication provider.";
34 errorDescription = "Try again in a moment or choose a different login method.";
35 break;
36 case "OAuthAccountNotLinked":
37 errorMessage = "An account with this email address already exists.";
38 errorDescription = "Log in using the method you used previously.";
39 break;
40 case "EmailSignin":
41 errorMessage = "Unable to send email with login link.";
42 errorDescription = "Check that the provided email address is correct.";
43 break;
44 case "CredentialsSignin":
45 errorMessage = "Invalid login credentials.";
46 errorDescription = "Check that the provided email and password are correct.";
47 break;
48 case "SessionRequired":
49 errorMessage = "Login required.";
50 errorDescription = "Log in, to access this page.";
51 break;
52 default:
53 // Default message already set
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 Back to login page
74 </Link>
75 <Link
76 href="/"
77 className="text-sm text-blue-600 hover:text-blue-500"
78 >
79 Back to home page
80 </Link>
81 </div>
82 </div>
83 </div>
84 );
85}Summary
In this module, we discussed in detail how to implement authentication with popular external providers (Google, GitHub, Facebook) in a Next.js application. We learned:
- The application registration process at OAuth providers and obtaining keys
- Configuration of NextAuth.js / Auth.js with different providers
- Creating a user interface for login and registration
- Securing routes using middleware
- Customizing the authentication process to our needs
- Error handling authentication
Thanks to this implementation, our Next.js application gains a professional, multi-platform authentication system that improves the user experience while strengthening the security of our application.
In the next module, we will deal in detail with user sessions and token management, which will allow us to better understand how authentication mechanisms work after user login.
Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3// Demo: OAuth Providers - Google, GitHub, Facebook
4interface OAuthProvider {
5 id: string;
6 name: string;
7 icon: string;
8 color: string;
9 scopes: string[];
10 requiredEnvVars: string[];
11}
12
13const providers: OAuthProvider[] = [
14 { id: 'google', name: 'Google', icon: 'G', color: '#4285f4', scopes: ['openid', 'profile', 'email'], requiredEnvVars: ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'] },
15 { id: 'github', name: 'GitHub', icon: 'GH', color: '#333', scopes: ['read:user', 'user:email'], requiredEnvVars: ['GITHUB_ID', 'GITHUB_SECRET'] },
16 { id: 'facebook', name: 'Facebook', icon: 'FB', color: '#1877f2', scopes: ['public_profile', 'email'], requiredEnvVars: ['FACEBOOK_CLIENT_ID', 'FACEBOOK_CLIENT_SECRET'] },
17];
18
19type FlowStep = 'idle' | 'redirect' | 'callback' | 'token' | 'profile' | 'done';
20
21function OAuthFlowDemo() {
22 const [selectedProvider, setSelectedProvider] = useState<OAuthProvider | null>(null);
23 const [currentStep, setCurrentStep] = useState<FlowStep>('idle');
24 const [flowLog, setFlowLog] = useState<string[]>([]);
25
26 const steps: { step: FlowStep; label: string; desc: string }[] = [
27 { step: 'redirect', label: '1. Redirect', desc: 'Redirect to the provider' },
28 { step: 'callback', label: '2. Callback', desc: 'Provider returns the code' },
29 { step: 'token', label: '3. Token', desc: 'Exchange the code for a token' },
30 { step: 'profile', label: '4. Profile', desc: 'Fetch the profile' },
31 { step: 'done', label: '5. Done', desc: 'Session created' },
32 ];
33
34 const simulateFlow = async (provider: OAuthProvider) => {
35 setSelectedProvider(provider);
36 setFlowLog([]);
37 setCurrentStep('idle');
38
39 const addLog = (msg: string) => setFlowLog(prev => [...prev, msg]);
40
41 await new Promise(r => setTimeout(r, 400));
42 setCurrentStep('redirect');
43 addLog('[Client] Clicked "Log in with ' + provider.name + '"');
44
45 await new Promise(r => setTimeout(r, 1000));
46 setCurrentStep('callback');
47 addLog('[' + provider.name + '] User approved access');
48 addLog('[Callback] GET /api/auth/callback?code=abc123');
49
50 await new Promise(r => setTimeout(r, 800));
51 setCurrentStep('token');
52 addLog('[Server] Exchanging authorization code for access_token');
53
54 await new Promise(r => setTimeout(r, 800));
55 setCurrentStep('profile');
56 addLog('[Server] Fetching user profile from ' + provider.name);
57
58 await new Promise(r => setTimeout(r, 600));
59 setCurrentStep('done');
60 addLog('[Server] Session created, user logged in');
61 };
62
63 return (
64 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
65 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>OAuth Providers Flow</h1>
66 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Visualization of the OAuth login process</p>
67
68 <div style={{ display: 'flex', gap: '16px', marginBottom: '32px', flexWrap: 'wrap' }}>
69 {providers.map(p => (
70 <button key={p.id} onClick={() => simulateFlow(p)} style={{
71 padding: '16px 24px', borderRadius: '12px', border: '2px solid',
72 borderColor: selectedProvider?.id === p.id ? p.color : '#333',
73 background: selectedProvider?.id === p.id ? p.color + '20' : '#1a1a2e',
74 color: '#fff', cursor: 'pointer', fontSize: '16px',
75 }}>
76 <div style={{ fontSize: '20px', fontWeight: 'bold' }}>{p.icon}</div>
77 <div style={{ marginTop: '4px' }}>{p.name}</div>
78 </button>
79 ))}
80 </div>
81
82 {selectedProvider && (
83 <>
84 <div style={{ display: 'flex', gap: '8px', marginBottom: '24px', flexWrap: 'wrap' }}>
85 {steps.map(s => {
86 const stepIdx = steps.findIndex(x => x.step === currentStep);
87 const thisIdx = steps.findIndex(x => x.step === s.step);
88 return (
89 <div key={s.step} style={{
90 padding: '8px 16px', borderRadius: '8px', fontSize: '13px',
91 background: currentStep === s.step ? '#64ffda20' : thisIdx < stepIdx ? '#1a3a2e' : '#1a1a2e',
92 border: currentStep === s.step ? '1px solid #64ffda' : '1px solid #333',
93 color: currentStep === s.step ? '#64ffda' : '#8892b0',
94 }}>
95 <strong>{s.label}</strong>
96 <div style={{ fontSize: '11px', marginTop: '2px' }}>{s.desc}</div>
97 </div>
98 );
99 })}
100 </div>
101
102 <div style={{ background: '#1a1a2e', borderRadius: '8px', padding: '16px', border: '1px solid #333', maxHeight: '250px', overflow: 'auto' }}>
103 <h3 style={{ color: '#64ffda', margin: '0 0 12px 0', fontSize: '14px' }}>Flow Log:</h3>
104 {flowLog.map((log, i) => (
105 <div key={i} style={{ fontSize: '13px', padding: '4px 0', borderBottom: '1px solid #222', color: log.includes('[Server]') ? '#f59e0b' : log.includes('[Client]') ? '#64ffda' : '#8892b0' }}>
106 {log}
107 </div>
108 ))}
109 </div>
110
111 <div style={{ marginTop: '16px', background: '#1a1a2e', padding: '12px', borderRadius: '8px', border: '1px solid #333' }}>
112 <strong style={{ color: '#f59e0b' }}>Required .env variables:</strong>
113 <div style={{ marginTop: '8px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
114 {selectedProvider.requiredEnvVars.map(v => (
115 <code key={v} style={{ background: '#0f0f23', padding: '4px 8px', borderRadius: '4px', fontSize: '13px', color: '#64ffda' }}>{v}</code>
116 ))}
117 </div>
118 </div>
119 </>
120 )}
121 </div>
122 );
123}
124
125export default OAuthFlowDemo;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Session-based authentication stores session data:
2. Token-based authentication uses:
Hands-on tasks in the game
- Code editor
Create the [...nextauth].ts file with a basic providers, session, and callbacks configuration
- Vertical ordering
Arrange the elements in the correct order: import 'client-only' โ export function โ
- Click in order
Arrange the getServerSession import syntax from NextAuth
- Code editor
Configure the Google provider in NextAuth with the appropriate client ID, secret, and scopes