Authentication mit NextAuth & Postgres

Von Max Schneider ·

Speech

NextAuth ist ein mächtiges Tool, das die Authentifizierung und Autorisierung von Benutzern in Next.js-Anwendungen erleichtert. Mit NextAuth können Entwickler Benutzer einfach erstellen, authentifizieren und verwalten, und das alles mit nur wenigen Zeilen Code.

In diesem Beitrag werde ich erläutern, wie NextAuth verwendet wird, um Benutzer zu erstellen und zu verwalten, und dabei auf den bereitgestellten Code eingehen, der NextAuth mit einer PostgreSQL-Datenbank integriert. Lasst uns eintauchen!

Einbindung von NextAuth

Der Code, den Sie unten sehen, zeigt eine einfache Integration von NextAuth in Ihre Next.js-Anwendung. Hier ist eine kurze Übersicht über den Code:

auth.js

import vercelPostgresAdapter from "@/backend/vercelPostgresAdapter";
import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
export const authOptions = {
secret: process.env.NEXTAUTH_SECRET as string,
adapter: vercelPostgresAdapter(),
providers: [
GitHubProvider({
clientId: process.env.NEXT_PUBLIC_GITHUB_AUTH_TEST_CLIENT_ID!,
clientSecret: process.env.GITHUB_AUTH_TEST_CLIENT_SECRET!,
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
})
],
}
export default NextAuth(authOptions);

Zuerst werden die erforderlichen Provider importiert, in diesem Fall GitHub und Google. Dann werden die Optionen für die Authentifizierung festgelegt, einschließlich des (Secret), das für die Verschlüsselung verwendet wird, des Adapters für die Datenbankanbindung und der konfigurierten Anbieter (GitHub und Google). Schließlich wird NextAuth mit den bereitgestellten Optionen initialisiert und exportiert.

Datenbankadapter

Der bereitgestellte Datenbankadapter (vercelPostgresAdapter) ist speziell auf PostgreSQL zugeschnitten und implementiert Funktionen zum Erstellen, Aktualisieren, Löschen und Abfragen von Benutzerdaten in der Datenbank. Dieser Adapter wird verwendet, um die Interaktion zwischen NextAuth und der Datenbank zu erleichtern.

vercelPostgresAdapter.js

import { sql } from "@vercel/postgres";
import { Account } from "next-auth";
import {
Adapter,
AdapterAccount,
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters";
export default function vercelPostgresAdapter(): Adapter {
try {
const createUser = async (
user: Omit<AdapterUser, "id">
): Promise<AdapterUser> => {
const { rows } = await sql`
INSERT INTO users (name, email, image)
VALUES (${user.name}, ${user.email}, ${user.image})
RETURNING id, name, email, email_verified, image`;
const newUser: AdapterUser = {
...rows[0],
id: rows[0].id.toString(),
emailVerified: rows[0].email_verified,
email: rows[0].email,
};
return newUser;
};
const getUser = async (id: string) => {
const { rows } = await sql`
SELECT *
FROM users
WHERE id = ${id};
`;
return {
...rows[0],
id: rows[0].id.toString(),
emailVerified: rows[0].email_verified,
email: rows[0].email,
};
};
const getUserByEmail = async (email: string) => {
const { rows } = await sql`SELECT * FROM users WHERE email = ${email}`;
return rows[0]
? {
...rows[0],
id: rows[0].id.toString(),
emailVerified: rows[0].email_verified,
email: rows[0].email,
}
: null;
};
const getUserByAccount = async ({
provider,
providerAccountId,
}: {
provider: string;
providerAccountId: string;
}): Promise<AdapterUser | null> => {
const { rows } = await sql`
SELECT u.*
FROM users u join accounts a on u.id = a.user_id
WHERE a.provider_id = ${provider}
AND a.provider_account_id = ${providerAccountId}`;
const user = rows[0]
? {
email: rows[0].email,
emailVerified: rows[0].email_verified,
id: rows[0].id,
}
: null;
return user;
};
const updateUser = async (
user: Partial<AdapterUser> & Pick<AdapterUser, "id">
): Promise<AdapterUser> => {
const { rows } = await sql`
UPDATE users
SET name = ${user.name}, email = ${user.email}, image = ${user.image}
WHERE id = ${user.id}
RETURNING id, name, email, image;
`;
const updatedUser: AdapterUser = {
...rows[0],
id: rows[0].id.toString(),
emailVerified: rows[0].email_verified,
email: rows[0].email,
};
return updatedUser;
};
const deleteUser = async (userId: string) => {
await sql`DELETE FROM users WHERE id = ${userId}`;
return;
};
const createSession = async ({
sessionToken,
userId,
expires,
}: {
sessionToken: string;
userId: string;
expires: Date;
}): Promise<AdapterSession> => {
const expiresString = expires.toDateString();
await sql`
INSERT INTO auth_sessions (user_id, expires, session_token)
VALUES (${userId}, ${expiresString}, ${sessionToken})
`;
const createdSession: AdapterSession = {
sessionToken,
userId,
expires,
};
return createdSession;
};
const getSessionAndUser = async (
sessionToken: string
): Promise<{ session: AdapterSession; user: AdapterUser } | null> => {
const session = await sql`
SELECT *
FROM auth_sessions
WHERE session_token = ${sessionToken}`;
const { rows } = await sql`
SELECT *
FROM users
WHERE id = ${session.rows[0].user_id}`;
const expiresDate = new Date(session.rows[0].expires);
const sessionAndUser: { session: AdapterSession; user: AdapterUser } = {
session: {
sessionToken: session.rows[0].session_token,
userId: session.rows[0].user_id,
expires: expiresDate,
},
user: {
id: rows[0].id,
emailVerified: rows[0].email_verified,
email: rows[0].email,
name: rows[0].name,
image: rows[0].image,
},
};
return sessionAndUser;
};
const updateSession = async (
session: Partial<AdapterSession> & Pick<AdapterSession, "sessionToken">
): Promise<AdapterSession | null | undefined> => {
console.log(
"Unimplemented function! updateSession in vercelPostgresAdapter. Session:",
JSON.stringify(session)
);
return;
};
const deleteSession = async (sessionToken: string) => {
await sql`
DELETE FROM auth_sessions
WHERE session_token = ${sessionToken};
`;
return;
};
const linkAccount = async (
account: AdapterAccount
): Promise<AdapterAccount | null | undefined> => {
await sql`
INSERT INTO accounts (
user_id,
provider_id,
provider_type,
provider_account_id,
refresh_token,
access_token,
expires_at,
token_type,
scope,
id_token
)
VALUES (
${account.userId},
${account.provider},
${account.type},
${account.providerAccountId},
${account.refresh_token},
${account.access_token},
to_timestamp(${account.expires_at}),
${account.token_type},
${account.scope},
${account.id_token}
)`;
return account;
};
const unlinkAccount = async ({
providerAccountId,
provider,
}: {
providerAccountId: Account["providerAccountId"];
provider: Account["provider"];
}) => {
await sql`
DELETE FROM accounts
WHERE provider_account_id = ${providerAccountId} AND provider_id = ${provider}}`;
return;
};
const createVerificationToken = async ({
identifier,
expires,
token,
}: VerificationToken): Promise<VerificationToken | null | undefined> => {
const { rows } = await sql`
INSERT INTO verification_tokens (identifier, token, expires)
VALUES (${identifier}, ${token}, ${expires.toString()})`;
const createdToken: VerificationToken = {
identifier: rows[0].identifier,
token: rows[0].token,
expires: rows[0].expires,
};
return createdToken;
};
//Return verification token from the database and delete it so it cannot be used again.
const useVerificationToken = async ({
identifier,
token,
}: {
identifier: string;
token: string;
}) => {
const { rows } = await sql`
SELECT * FROM verification_tokens
WHERE identifier = ${identifier}
AND token = ${token} AND expires > NOW()`;
await sql`
DELETE FROM verification_tokens
WHERE identifier = ${identifier}
AND token = ${token}`;
return {
expires: rows[0].expires,
identifier: rows[0].identifier,
token: rows[0].token,
};
};
return {
createUser,
getUser,
updateUser,
getUserByEmail,
getUserByAccount,
deleteUser,
getSessionAndUser,
createSession,
updateSession,
deleteSession,
createVerificationToken,
useVerificationToken,
linkAccount,
unlinkAccount,
};
} catch (error) {
throw error;
}
}

PostgreSQL-Datenbank

Die Implementierung einer soliden Authentifizierung erfordert nicht nur die Integration von NextAuth und die Verbindung zur Datenbank, sondern auch die richtige Konfiguration der Datenbanktabellen. Hier werfen wir einen Blick auf die SQL-Anweisungen, um die erforderlichen Tabellen in Ihrer Postgres-Datenbank zu erstellen, und wie dies nahtlos in Ihrer Vercel-basierten Next.js-Anwendung funktioniert.

Datenbank-Tabellen erstellen Bevor wir in die Details der SQL-Anweisungen eintauchen, ist es wichtig zu betonen, dass die Erstellung der Datenbanktabellen ein entscheidender Schritt ist. Hier wird die uuid-ossp-Erweiterung für die Erzeugung von UUIDs verwendet, um eindeutige Identifikatoren für Benutzer und Sitzungen sicherzustellen.

database.sql

-- Erweiterung für UUIDs aktivieren
CREATE EXTENSION IF NOT EXISTS 'uuid-ossp';
-- Benutzer-Tabelle erstellen
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
email_verified BOOLEAN DEFAULT false,
image TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Konten-Tabelle erstellen
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
provider_id VARCHAR(255) NOT NULL,
provider_type VARCHAR(255) NOT NULL,
provider_account_id VARCHAR(255) NOT NULL,
refresh_token TEXT,
access_token TEXT NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE,
token_type VARCHAR(255),
scope TEXT,
id_token TEXT,
session_state TEXT
);
-- Verifikations-Token-Tabelle erstellen
CREATE TABLE verification_tokens (
identifier VARCHAR(255) PRIMARY KEY,
token TEXT NOT NULL,
expires TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Authentifizierungs-Sitzungs-Tabelle erstellen
CREATE TABLE auth_sessions (
id SERIAL PRIMARY KEY,
expires TIMESTAMP WITH TIME ZONE NOT NULL,
session_token TEXT NOT NULL,
user_id UUID NOT NULL REFERENCES users(id)
);
-- Zahlungs-Tabelle erstellen
CREATE TABLE payments (
id SERIAL PRIMARY KEY,
user_email VARCHAR(255) NOT NULL REFERENCES users(email),
stripe_item_id VARCHAR(999) NOT NULL,
isPaid BOOLEAN NOT NULL,
date TIMESTAMP DEFAULT NOW()
);

Funktionsweise und Schlussgedanken

Der Authentifizierungsdurchlauf beginnt mit dem Benutzer, der sich über einen der konfigurierten Anbieter anmeldet. NextAuth übernimmt dann die Kontrolle und verwendet den Adapter, um Daten in der Postgres-Datenbank zu manipulieren. Sitzungen werden erstellt, Benutzerdaten abgerufen und aktualisiert.

Mit dieser Konfiguration erhalten Sie nicht nur eine sichere Authentifizierung, sondern auch eine gut organisierte und effiziente Datenbankanbindung. Dies ermöglicht Ihnen, sich auf das Wesentliche Ihrer Anwendung zu konzentrieren, während NextAuth die Authentifizierungsschwerarbeit übernimmt.





Beitrag teilen


zurück zum Blog

Kauf mir einen Kaffee ☕️

Eure Spende ermöglicht es mir, weiterhin Blogs zu schreiben.

Vielen Dank für eure Unterstützung! ♥️