Skip to main content
POST
/
auth
/
login
Login
curl --request POST \
  --url https://auth.nullpass.xyz/api/auth/login \
  --header 'Content-Type: application/json' \
  --data '
{
  "email": "jsmith@example.com",
  "password": "<string>",
  "verificationCode": "<string>"
}
'
{
  "user": {
    "id": "<string>",
    "email": "jsmith@example.com",
    "username": "<string>",
    "displayName": "<string>",
    "avatar": "<string>",
    "twoFactorEnabled": true,
    "createdAt": "2023-11-07T05:31:56Z"
  },
  "token": "<string>",
  "services": [
    {
      "id": "<string>",
      "userId": "<string>",
      "service": "DROP",
      "tier": "premium",
      "isPremium": true,
      "accessFlags": {},
      "metadata": {},
      "customStorageLimit": 123,
      "customApiKeyLimit": 123,
      "createdAt": "2023-11-07T05:31:56Z",
      "updatedAt": "2023-11-07T05:31:56Z"
    }
  ],
  "banned": true,
  "disabled": true
}

Endpoint

POST /api/auth/login

Overview

Authenticates a user with email and password. If 2FA is enabled, returns a pending token that requires verification code. Otherwise, returns a full JWT token and user’s service access information.

Request

email
string
required
User email address
password
string
required
User password
verificationCode
string
Required if 2FA is enabled. TOTP code from authenticator app.

Response (Without 2FA or Valid Code)

user
object
Authenticated user object
token
string
JWT token for authentication
services
array
Array of user’s service entitlements
banned
boolean
Whether the user account is banned
disabled
boolean
Whether the user account is disabled

Response (2FA Required)

user
object
Partial user object (without sensitive data)
requires2FA
boolean
Always true when 2FA is required
pendingToken
string
Temporary token for 2FA verification. Not a full JWT token.
message
string
“2FA verification required”

Implementation Details

Process Flow

  1. CORS & Arcjet: Validates CORS and applies rate limiting
  2. User Lookup: Finds user with service access included
  3. Password Verification: Compares password with bcrypt hash
  4. 2FA Check: If enabled, validates verification code or returns pending token
  5. Session Management:
    • Reuses existing session if valid and from same IP
    • Creates new session if none exists
    • Updates expiration on existing sessions
  6. Audit Logging: Logs USER_LOGIN and SESSION_CREATE (if new session)

Code Reference

export async function POST(request: NextRequest) {
  const corsResponse = handleCors(request)
  if (corsResponse) return corsResponse

  const blocked = await protectRoute(request, { requested: 2 })
  if (blocked) return blocked

  try {
    const body = await request.json()
    const validated = loginSchema.parse(body)

    logger.ups('Login attempt:', validated.email)

    const user = await prisma.user.findUnique({
      where: { email: validated.email },
      include: {
        serviceAccess: true,
      },
    })

    if (!user || !user.passwordHash) {
      logger.warn('Login failed: Invalid credentials', validated.email)
      return errorResponse('Invalid credentials', 401, request.headers.get('origin'))
    }

    const isValid = await bcrypt.compare(validated.password, user.passwordHash)
    if (!isValid) {
      logger.warn('Login failed: Invalid password', validated.email)
      return errorResponse('Invalid credentials', 401, request.headers.get('origin'))
    }

    const verificationCode = validated.verificationCode

    if (user.twoFactorEnabled) {
      if (!verificationCode) {
        const pendingToken = generateToken({ userId: user.id, email: user.email })
        return jsonResponse(
          {
            user: {
              id: user.id,
              email: user.email,
              displayName: user.displayName,
              avatar: user.avatar,
            },
            requires2FA: true,
            pendingToken,
            message: '2FA verification required',
          },
          200,
          request.headers.get('origin')
        )
      }

      if (!user.twoFactorSecret) {
        logger.warn('2FA enabled but no secret found', user.id)
        return errorResponse('2FA configuration error', 500, request.headers.get('origin'))
      }

      const verified = speakeasy.totp.verify({
        secret: user.twoFactorSecret,
        encoding: 'base32',
        token: verificationCode,
        window: 2,
      })

      if (!verified) {
        logger.warn('2FA verification failed', user.id)
        return errorResponse('Invalid 2FA verification code', 401, request.headers.get('origin'))
      }
    }

    const clientIp = getClientIp(request)
    const encryptedIp = getClientIpForStorage(request, user.id)

    const existingSession = await prisma.session.findFirst({
      where: {
        userId: user.id,
        ip: encryptedIp,
        expiresAt: {
          gt: new Date(),
        },
      },
      orderBy: {
        createdAt: 'desc',
      },
    })

    let token: string
    const expiresAt = getSessionExpiresAt()

    let sessionCreated = false
    
    if (existingSession) {
      const { verifyToken } = await import('@/lib/auth')
      const tokenPayload = verifyToken(existingSession.token)
      
      if (tokenPayload && tokenPayload.userId === user.id) {
        token = existingSession.token
        await prisma.session.update({
          where: { id: existingSession.id },
          data: {
            expiresAt,
          },
        })
      } else {
        token = generateToken({ userId: user.id, email: user.email })
        await prisma.session.update({
          where: { id: existingSession.id },
          data: {
            token,
            expiresAt,
          },
        })
      }
    } else {
      token = generateToken({ userId: user.id, email: user.email })
      await prisma.session.create({
        data: {
          userId: user.id,
          token,
          expiresAt,
          ip: encryptedIp,
        },
      })
      sessionCreated = true
    }

    await prisma.user.update({
      where: { id: user.id },
      data: { updatedAt: new Date() },
    })

    await createAuditLog(user.id, 'USER_LOGIN', {
      ip: encryptedIp,
      twoFactorUsed: user.twoFactorEnabled && !!verificationCode,
    })
    
    if (sessionCreated) {
      await createAuditLog(user.id, 'SESSION_CREATE', {
        ip: encryptedIp,
      })
    }

    return jsonResponse(
      {
        user: {
          id: user.id,
          email: user.email,
          displayName: user.displayName,
          avatar: user.avatar,
        },
        token,
        services: user.serviceAccess,
      },
      200,
      request.headers.get('origin')
    )
  } catch (error: any) {
    if (error.name === 'ZodError') {
      logger.warn('Login validation error:', error.errors)
      return errorResponse(error.errors[0].message, 400, request.headers.get('origin'))
    }
    logger.error('Login error:', error)
    return errorResponse('Internal server error', 500, request.headers.get('origin'))
  }
}

Status Codes

200
OK
Login successful (with or without 2FA)
400
Bad Request
Validation error
401
Unauthorized
Invalid credentials or invalid 2FA code
403
Forbidden
Blocked by Arcjet
500
Internal Server Error
Server error or 2FA configuration error

Example Requests

Without 2FA

curl -X POST https://auth.nullpass.xyz/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "securepassword123"
  }'

With 2FA

# Step 1: Initial login (returns requires2FA: true)
curl -X POST https://auth.nullpass.xyz/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "securepassword123"
  }'

# Step 2: Verify with 2FA code
curl -X POST https://auth.nullpass.xyz/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "securepassword123",
    "verificationCode": "123456"
  }'

Session Reuse Logic

The endpoint implements smart session reuse:
  • If a valid session exists for the user from the same IP, it reuses the token
  • Session expiration is updated on reuse
  • New sessions are only created when none exist or existing session is invalid

Audit Events

  • USER_LOGIN: Successful login (includes twoFactorUsed flag)
  • SESSION_CREATE: New session created (only if new session was created)

Body

application/json
email
string<email>
required
password
string
required
verificationCode
string

Required if 2FA is enabled

Response

Login successful

user
object
token
string
services
object[]
banned
boolean

Whether the user account is banned

disabled
boolean

Whether the user account is disabled