Skip to main content
This is an implementation guide, not current functionality. JWT authentication is not yet active in the codebase. This page provides the code needed to implement secure authentication.

Overview

This guide shows how to add JSON Web Tokens (JWT) for stateless authentication to App CR. The dependencies are installed but the authentication logic needs to be implemented.

Dependencies

The JWT implementation relies on these packages (from package.json):
{
  "dependencies": {
    "jsonwebtoken": "^9.0.3",
    "bcryptjs": "^3.0.3",
    "express": "^5.2.1",
    "@prisma/client": "^6.19.2"
  },
  "devDependencies": {
    "dotenv": "^17.3.1"
  }
}

JWT Configuration

Environment Setup

The JWT secret is loaded from environment variables in backend/index.js:11:
require('dotenv').config();
const jwt = require('jsonwebtoken');

const TOKEN = process.env.JWT_SECRET; // Secret key for signing tokens
Security Critical: The JWT_SECRET must be:
  • At least 256 bits (32 characters) long
  • Randomly generated
  • Stored securely in .env file
  • Never committed to version control

Generate a Secure Secret

# Generate a cryptographically secure secret
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
Then add to your .env file:
JWT_SECRET=your_generated_secret_here
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/appcrdb

User Registration with Password Hashing

The current implementation in backend/index.js:24-36 needs to be updated to hash passwords:

Current Implementation (Insecure)

app.post('/usuarios', async (req, res) => {
  try {
    const nuevoUsuario = await prisma.user.create({
      data: {
        email: req.body.email,
        password: req.body.password // ❌ Plain text password
      }
    });
    res.status(201).json(nuevoUsuario);
  } catch (error) {
    console.log("DETALLE DEL ERROR:", error);
    res.status(500).json({ error: "Error al insertar en Postgres" });
  }
});
const bcrypt = require('bcryptjs');

app.post('/usuarios', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Validate input
    if (!email || !password) {
      return res.status(400).json({ error: "Email and password are required" });
    }
    
    // Hash password with salt rounds of 10
    const hashedPassword = await bcrypt.hash(password, 10);
    
    const nuevoUsuario = await prisma.user.create({
      data: {
        email,
        password: hashedPassword // ✅ Hashed password
      }
    });
    
    // Don't send password back in response
    const { password: _, ...userWithoutPassword } = nuevoUsuario;
    res.status(201).json(userWithoutPassword);
    
  } catch (error) {
    console.log("DETALLE DEL ERROR:", error);
    
    // Handle unique constraint violation
    if (error.code === 'P2002') {
      return res.status(409).json({ error: "Email already exists" });
    }
    
    res.status(500).json({ error: "Error al insertar en Postgres" });
  }
});
bcrypt is a password hashing function designed to be slow and computationally expensive, making it resistant to brute-force attacks.
  • Salt rounds (10): Each increment doubles the time needed to hash. 10 rounds is a good balance between security and performance.
  • Automatic salting: bcrypt generates a unique salt for each password
  • One-way function: Passwords cannot be reversed from the hash
Example hash output:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
  • $2a$ - bcrypt algorithm identifier
  • 10$ - cost factor (number of rounds)
  • Next 22 chars - the salt
  • Remaining chars - the actual password hash

User Login with JWT Token Generation

Implement a login endpoint that verifies credentials and returns a JWT token:
app.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Validate input
    if (!email || !password) {
      return res.status(400).json({ error: "Email and password are required" });
    }
    
    // Find user by email
    const user = await prisma.user.findUnique({
      where: { email }
    });
    
    if (!user) {
      return res.status(401).json({ error: "Invalid credentials" });
    }
    
    // Verify password
    const isValidPassword = await bcrypt.compare(password, user.password);
    
    if (!isValidPassword) {
      return res.status(401).json({ error: "Invalid credentials" });
    }
    
    // Generate JWT token
    const token = jwt.sign(
      { 
        userId: user.id, 
        email: user.email 
      },
      process.env.JWT_SECRET,
      { 
        expiresIn: '24h' // Token expires in 24 hours
      }
    );
    
    res.json({ 
      token,
      user: {
        id: user.id,
        email: user.email
      }
    });
    
  } catch (error) {
    console.log("DETALLE DEL ERROR:", error);
    res.status(500).json({ error: "Error during login" });
  }
});
Security Best Practice: Always return the same generic error message (“Invalid credentials”) whether the email doesn’t exist or the password is wrong. This prevents attackers from enumerating valid email addresses.

JWT Middleware for Protected Routes

Create middleware to verify JWT tokens on protected endpoints:
// Middleware to verify JWT tokens
const authenticateToken = (req, res, next) => {
  // Get token from Authorization header
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Format: "Bearer TOKEN"
  
  if (!token) {
    return res.status(401).json({ error: "No token provided" });
  }
  
  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Attach user info to request object
    req.user = decoded;
    next();
    
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: "Token expired" });
    }
    if (error.name === 'JsonWebTokenError') {
      return res.status(403).json({ error: "Invalid token" });
    }
    return res.status(403).json({ error: "Token verification failed" });
  }
};

Using the Middleware

Protect routes by adding the middleware:
// Public route - no authentication needed
app.post('/usuarios', async (req, res) => { /* ... */ });
app.post('/login', async (req, res) => { /* ... */ });

// Protected route - requires valid JWT token
app.get('/me', authenticateToken, async (req, res) => {
  try {
    const user = await prisma.user.findUnique({
      where: { id: req.user.userId },
      select: {
        id: true,
        email: true,
        tasks: true
      }
    });
    
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: "Error fetching user" });
  }
});

// Get user's tasks - protected
app.get('/tasks', authenticateToken, async (req, res) => {
  try {
    const tasks = await prisma.task.findMany({
      where: { userId: req.user.userId }
    });
    
    res.json(tasks);
  } catch (error) {
    res.status(500).json({ error: "Error fetching tasks" });
  }
});

Complete Authentication Flow Examples

1

Register a New User

curl -X POST http://localhost:3000/usuarios \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "SecurePass123!"
  }'
Response:
{
  "id": 1,
  "email": "[email protected]"
}
2

Login to Get JWT Token

curl -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "SecurePass123!"
  }'
Response:
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": 1,
    "email": "[email protected]"
  }
}
3

Access Protected Endpoint

curl -X GET http://localhost:3000/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Response:
{
  "id": 1,
  "email": "[email protected]",
  "tasks": []
}

JWT Token Structure

A JWT token consists of three parts separated by dots:
header.payload.signature
You can decode (but not verify) a JWT token at jwt.io or programmatically:
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
const decoded = jwt.decode(token);

console.log(decoded);
// {
//   userId: 1,
//   email: "[email protected]",
//   iat: 1709755200,  // Issued at
//   exp: 1709841600   // Expires at
// }
jwt.decode() only decodes the token without verifying the signature. Always use jwt.verify() in production to ensure the token is valid and hasn’t been tampered with.

Token Expiration and Refresh

Setting Token Expiration

Tokens should have a reasonable expiration time:
const token = jwt.sign(
  payload,
  process.env.JWT_SECRET,
  { 
    expiresIn: '24h'  // Options: '15m', '1h', '7d', '30d'
  }
);
Use CaseExpirationReason
Web App1-24 hoursBalance between security and UX
Mobile App7-30 daysLess frequent logins acceptable
API KeysNo expirationUse refresh tokens instead
High Security15-60 minutesMinimal exposure window

Error Handling

Common JWT errors and how to handle them:
try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
  switch(error.name) {
    case 'TokenExpiredError':
      // Token has expired - user needs to login again
      return res.status(401).json({ 
        error: "Token expired",
        code: "TOKEN_EXPIRED"
      });
      
    case 'JsonWebTokenError':
      // Token is malformed or invalid
      return res.status(403).json({ 
        error: "Invalid token",
        code: "INVALID_TOKEN"
      });
      
    case 'NotBeforeError':
      // Token not active yet (nbf claim)
      return res.status(403).json({ 
        error: "Token not active yet",
        code: "TOKEN_NOT_ACTIVE"
      });
      
    default:
      return res.status(403).json({ 
        error: "Token verification failed",
        code: "VERIFICATION_FAILED"
      });
  }
}

Security Best Practices

Use HTTPS

Always transmit JWT tokens over HTTPS in production to prevent interception.

Short Expiration

Use short-lived tokens (1-24 hours) to minimize the impact of token theft.

Secure Storage

Store tokens securely on the client (httpOnly cookies or secure storage, not localStorage).

Strong Secret

Use a cryptographically secure JWT_SECRET (at least 256 bits).
Common Security Mistakes to Avoid:
  • Storing sensitive data in the JWT payload (it’s base64 encoded, not encrypted)
  • Using weak or default JWT_SECRET values
  • Not validating tokens on every protected request
  • Storing tokens in localStorage (vulnerable to XSS attacks)
  • Not implementing token expiration

Testing JWT Implementation

Test your authentication flow:
// test-auth.js
const axios = require('axios');

const BASE_URL = 'http://localhost:3000';

async function testAuth() {
  try {
    // 1. Register user
    console.log('1. Registering user...');
    const registerRes = await axios.post(`${BASE_URL}/usuarios`, {
      email: '[email protected]',
      password: 'TestPass123!'
    });
    console.log('✓ User registered:', registerRes.data);
    
    // 2. Login
    console.log('\n2. Logging in...');
    const loginRes = await axios.post(`${BASE_URL}/login`, {
      email: '[email protected]',
      password: 'TestPass123!'
    });
    console.log('✓ Login successful');
    console.log('Token:', loginRes.data.token.substring(0, 50) + '...');
    
    const token = loginRes.data.token;
    
    // 3. Access protected route
    console.log('\n3. Accessing protected route...');
    const meRes = await axios.get(`${BASE_URL}/me`, {
      headers: { Authorization: `Bearer ${token}` }
    });
    console.log('✓ Protected route accessed:', meRes.data);
    
    // 4. Try with invalid token
    console.log('\n4. Testing with invalid token...');
    try {
      await axios.get(`${BASE_URL}/me`, {
        headers: { Authorization: 'Bearer invalid_token' }
      });
    } catch (error) {
      console.log('✓ Invalid token rejected:', error.response.data);
    }
    
  } catch (error) {
    console.error('✗ Error:', error.response?.data || error.message);
  }
}

testAuth();

Next Steps

Authentication Overview

Review the complete authentication architecture

API Reference

Explore all authentication endpoints

Database Schema

Learn about the database structure

Quickstart

Get started with App CR in minutes

Build docs developers (and LLMs) love