Skip to main content

Overview

Shrtnr uses NextAuth.js v5 for authentication with a credentials-based provider. Authentication is session-based using JWT tokens, with sessions stored in HTTP-only cookies for security.

Authentication Flow

1

User Registration

Users create an account via the /api/auth/register endpoint with email and password.
2

Sign In

Users authenticate through NextAuth’s built-in credential flow at /api/auth/signin/credentials.
3

Session Creation

NextAuth creates a JWT token containing user ID and email, stored in an HTTP-only cookie.
4

API Requests

Authenticated requests automatically include the session cookie, which NextAuth validates.

Session Configuration

Sessions are configured with the following settings (defined in auth.ts:40):
session: { 
  strategy: 'jwt', 
  maxAge: 30 * 24 * 60 * 60  // 30 days
}
  • Strategy: JWT (no database session storage)
  • Max Age: 30 days
  • Storage: HTTP-only cookies (secure in production)

Registration Endpoint

POST /api/auth/register

Create a new user account. Request Body:
{
  "email": "user@example.com",
  "password": "securepassword123",
  "name": "John Doe"
}
Parameters:
  • email (required): Valid email address (case-insensitive)
  • password (required): Minimum 8 characters
  • name (optional): User’s display name
Success Response:
{
  "ok": true
}
Error Responses:
{
  "error": "Valid email is required"
}
Passwords are hashed using bcrypt with 10 salt rounds before storage (auth/register/route.ts:22).

Authentication Endpoint

POST /api/auth/callback/credentials

NextAuth handles credential-based authentication through its built-in handlers. This endpoint is managed by NextAuth and uses the configuration in auth.ts. How It Works:
  1. NextAuth receives credentials (email and password)
  2. The authorize function queries the database for the user (auth.ts:21-26)
  3. Password is verified using bcrypt (auth.ts:30)
  4. If valid, a JWT token is created with user information
  5. Token is stored in an HTTP-only cookie
Credential Verification:
const user = await db`
  SELECT id, email, name, password_hash
  FROM users
  WHERE email = ${email}
  LIMIT 1
`;

const ok = await bcrypt.compare(password, user.password_hash);

Using Sessions in API Routes

All protected API routes use the auth() helper to retrieve the current session:
import { auth } from '@/auth';

export async function GET() {
  const session = await auth();
  
  if (!session?.user?.id) {
    return NextResponse.json(
      { error: 'Unauthorized' }, 
      { status: 401 }
    );
  }
  
  // Access user information
  const userId = session.user.id;
  const userEmail = session.user.email;
  
  // ... your protected logic
}

Session Object Structure

The session object contains the following user information:
{
  user: {
    id: string;        // Unique user identifier (24-char nanoid)
    email: string;     // User's email address
    name?: string;     // Optional display name
  }
}
This structure is defined in the JWT and session callbacks (auth.ts:41-56):
jwt({ token, user }) {
  if (user) {
    token.id = user.id;
    token.email = user.email;
  }
  return token;
},
session({ session, token }) {
  if (session.user) {
    session.user.id = token.id as string;
    session.user.email = token.email as string;
  }
  return session;
}

Authentication Requirements by Endpoint

POST /api/shorten

Optional - Custom slugs require auth, anonymous gets random code

GET /api/links

Required - Only authenticated users can list their links

PATCH /api/links/[id]

Required - Only link owner can update

DELETE /api/links/[id]

Required - Only link owner can delete

Security Features

Password Security

  • Passwords hashed with bcrypt (10 rounds)
  • Minimum password length: 8 characters
  • Password hashes stored in users.password_hash column
  • Plain-text passwords never stored

Session Security

  • HTTP-only cookies: Prevents XSS attacks from accessing tokens
  • JWT tokens: Stateless authentication, no database lookups per request
  • 30-day expiration: Automatic session timeout
  • Secure flag in production (HTTPS only)

Email Handling

  • Emails normalized: trimmed and lowercased (auth.ts:18)
  • Unique constraint on email column prevents duplicates
  • Email validation using regex pattern (auth/register/route.ts:15)

Environment Variables

Required environment variables for authentication:
# NextAuth Configuration
AUTH_SECRET=your-secret-key-here  # Required for JWT signing
NEXTPUBLIC_APP_URL=https://your-domain.com  # Your app's base URL

# Database
DATABASE_URL=postgresql://user:password@host:port/database
Generate a secure AUTH_SECRET using: openssl rand -base64 32

Custom Sign-In Page

Shrtnr uses a custom sign-in page instead of NextAuth’s default (auth.ts:37-39):
pages: {
  signIn: '/login',
}
This allows for a branded authentication experience consistent with the rest of the application.

Best Practices

Always Check Sessions

Validate session?.user?.id exists before accessing protected resources

Return Proper Status Codes

Use 401 for missing auth, 403 for insufficient permissions

Secure Environment Variables

Never commit AUTH_SECRET to version control

Use HTTPS in Production

Ensure cookies are transmitted securely

Example: Protected API Route

Here’s a complete example of a protected endpoint (api/links/route.ts:5-17):
import { NextResponse } from 'next/server';
import { auth } from '@/auth';
import { listLinksByUser } from '@/app/lib/links';

export async function GET() {
  const session = await auth();
  if (!session?.user?.id) {
    return NextResponse.json(
      { error: 'Unauthorized' }, 
      { status: 401 }
    );
  }
  
  try {
    const links = await listLinksByUser(session.user.id);
    return NextResponse.json(links);
  } catch (err) {
    console.error('List links error:', err);
    return NextResponse.json(
      { error: 'Something went wrong' }, 
      { status: 500 }
    );
  }
}

Sign Out

To sign out, use NextAuth’s built-in signOut function:
import { signOut } from '@/auth';

await signOut();
This clears the session cookie and invalidates the JWT token.

Troubleshooting

  • Verify AUTH_SECRET is set in environment variables
  • Check that cookies are enabled in the browser
  • Ensure the request includes credentials (same-origin)
  • Verify database connection is working
This occurs when the email exists in the database (PostgreSQL unique constraint 23505). The user should sign in instead or use password recovery.
Run the database initialization script:
npm run db:init
This creates the required users, urls, and clicks tables.

Next Steps

API Overview

Explore all available API endpoints

Shorten Endpoint

Create short links with authentication

Build docs developers (and LLMs) love