DEV Community

Carsten Lebek
Carsten Lebek

Posted on

Credentials Authentication with Database Sessions Using Auth.js, SvelteKit and Prisma

This guide will show you how to get started with implementing credentials authentication using Auth.js, Sveltekit, and Prisma, while utilizing database sessions.

Step 1: Installing Dependencies

First, you'll need to install the necessary packages. In your Sveltekit project, run the following commands:

pnpm i prisma @prisma/client @auth/core @auth/sveltekit bcrypt
Enter fullscreen mode Exit fullscreen mode

Step 2: Create your Prisma Schema

Create a file named schema.prisma and paste the following code:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // Only needed when using a cloud provider that doesn't support the creation of new databases, like Heroku. Learn more: https://pris.ly/migrate-shadow
}

generator client {
  provider        = "prisma-client-js"
}

model Account {
  id                 String  @id @default(cuid())
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?  @db.Text
  access_token       String?  @db.Text
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?  @db.Text
  session_state      String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  username      String?   @unique
  password      String?
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the database schema with Prisma Migrate

Run the following command to create the database schema:

npx prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

This will create an SQL migration file and execute it.

Note that you will need to specify your database connection string in the environment variable DATABASE_URL. You can do this by setting it in a .env file at the root of your project.

Step 4: Initialize the Prisma client

Create a file named client.ts in the src/lib/prisma directory and paste the following code:

// src/lib/prisma/client.ts

import {  PrismaClient } from '@prisma/client';

import { NODE_ENV } from '$env/static/private';

declare global {
    // eslint-disable-next-line no-var
    var prisma: PrismaClient | undefined;
}

export const prisma =
    global.prisma ||
    new PrismaClient({
        // * Uncomment this to see the SQL queries in the console
        // log: NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
    });

if (NODE_ENV !== 'production') {
    global.prisma = prisma;
}

Enter fullscreen mode Exit fullscreen mode

Step 5: Set up the SvelteKit Server Hooks

In this step, you will create a server hooks file in your SvelteKit project to handle authentication with @auth/sveltekit. You will also import the necessary dependencies such as CredentialsProvider from @auth/core, PrismaAdapter from @next-auth/prisma-adapter, and bcrypt for password encryption.

// src/hooks.server.ts

import { SvelteKitAuth } from '@auth/sveltekit';
import CredentialsProvider from '@auth/core/providers/credentials';
import type { Handle } from '@sveltejs/kit';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import type { Adapter } from '@auth/core/adapters';
import { prisma } from '$lib/prisma/client';
import type { Provider } from '@auth/core/providers';
import { encode, decode } from '@auth/core/jwt';
import bcrypt from 'bcrypt';

export const handle: Handle = async ({ event, resolve }) => {
    const result = await SvelteKitAuth({
        adapter: PrismaAdapter(prisma) as Adapter,
        providers: [
            CredentialsProvider({
                // The name to display on the sign in form (e.g. "Sign in with...")
                name: 'Credentials',
                // `credentials` is used to generate a form on the sign in page.
                // You can specify which fields should be submitted, by adding keys to the `credentials` object.
                // e.g. domain, username, password, 2FA token, etc.
                // You can pass any HTML attribute to the <input> tag through the object.
                credentials: {
                    username: { label: 'Username', type: 'text', placeholder: 'Username' },
                    password: { label: 'Password', type: 'password' }
                },
                async authorize(credentials) {
                    if (!credentials) {
                        return null;
                    }

                    const user = await prisma.user.findUnique({
                        where: {
                            username: credentials.username
                        }
                    });

                    if (!user || !user.password) {
                        return null;
                    }

                    const isPasswordValid = await bcrypt.compare(credentials.password, user.password);

                    if (!isPasswordValid) {
                        return null;
                    }

                    return user;
                }
            })
        ] as Provider[],
        callbacks: {
            async signIn({ user, credentials }) {
                // Check if this sign in callback is being called in the credentials authentication flow.
                // If so, use the next - auth adapter to create a session entry in the database
                // (SignIn is called after authorize so we can safely assume the user is valid and already authenticated).
                if (credentials && credentials.username && credentials.password) {
                    if (user) {
                        const sessionToken = crypto.randomUUID();
                        // Set expiry to 30 days
                        const sessionExpiry = new Date(Date.now() + 60 * 60 * 24 * 30 * 1000);

                        await PrismaAdapter(prisma).createSession({
                            sessionToken: sessionToken,
                            userId: user.id,
                            expires: sessionExpiry
                        });

                        event.cookies.set('next-auth.session-token', sessionToken, {
                            expires: sessionExpiry
                        });
                    }
                }

                return true;
            }
        },
        jwt: {
            // Add a callback to the encode method to return the session token from a cookie
            // when in the credentials provider callback flow.
            encode: async (params) => {
                if (
                    event.url.pathname?.includes('callback') &&
                    event.url.pathname?.includes('credentials') &&
                    event.request.method === 'POST'
                ) {
                    // Get the session token cookie
                    const cookie = event.cookies.get('next-auth.session-token');

                    // Return the cookie value, or an empty string if it is not defined
                    return cookie ?? '';
                }
                // Revert to default behaviour when not in the credentials provider callback flow
                return encode(params);
            },
            decode: async (params) => {
                if (
                    event.url.pathname?.includes('callback') &&
                    event.url.pathname?.includes('credentials') &&
                    event.request.method === 'POST'
                ) {
                    return null;
                }

                // Revert to default behaviour when not in the credentials provider callback flow
                return decode(params);
            }
        },
        session: {
            strategy: 'database',
            maxAge: 60 * 60 * 24 * 30, // 30 days
            updateAge: 60 * 60 * 24, // 24 hours
            generateSessionToken: () => {
                return crypto.randomUUID();
            }
        }
    })({
        event,
        resolve
    });

    return result;
};

Enter fullscreen mode Exit fullscreen mode

Signing up users

Creating a user account is quick and effortless when you use Prisma. With just a few lines of code, you can add new users to your database and get them started in no time. Take a look at the code below:

const user = await prisma.user.create({
    data: {
        username: username,
        password: await bcrypt.hash(password, 10)
    }
});

const account = await prisma.account.create({
    data: {
        userId: user.id,
        type: 'credentials',
        provider: 'credentials',
        providerAccountId: user.id
    }
});
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
zfranco55 profile image
zfranco55

since I'm just starting out with this type of programming, the part, which I think is missing, relating to protected pages and user registration/authentication would also be useful.
Is there any link that explains it in simple way?