DEV Community

Cover image for What is JWT? How do Secret, Public, Private Keys actually work?
Ibrohim Abdivokhidov
Ibrohim Abdivokhidov

Posted on

What is JWT? How do Secret, Public, Private Keys actually work?

This guide covers two JWT authentication approaches for the English-Uzbek Translator API:

  1. Secret Key + JWT (Symmetric - Traditional approach)
  2. Public + Private Key (Asymmetric - Current implementation)

πŸ“‹ Table of Contents


Overview

What is JWT?

JSON Web Tokens (JWT) are a secure way to transmit information between parties. A JWT consists of three parts:

  • Header: Algorithm and token type
  • Payload: Claims (user data)
  • Signature: Verification signature

Authentication Flow

secret key based auth

public and private key based auth

Both approaches follow the same basic flow:

  1. Client obtains a JWT token
  2. Client includes token in API requests
  3. Server verifies token and processes request

Approach 1: Secret Key + JWT

πŸ” When to Use

  • Single application environment
  • Centralized token generation (same server creates and verifies)
  • Simple deployment scenarios
  • Internal APIs within same organization

πŸ› οΈ Setup

Step 1: Generate Secret Key

# Generate a secure secret key
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure Environment

# .env file
JWT_SECRET=your_64_character_secret_key_here
JWT_EXPIRES_IN=24h
JWT_ALGORITHM=HS256
Enter fullscreen mode Exit fullscreen mode

Step 3: Server Configuration

// authService.js (Secret Key approach)
import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET;
const JWT_ALGORITHM = 'HS256';

class AuthService {
  // Generate token
  generateToken(user) {
    return jwt.sign(
      { 
        sub: user.id,
        username: user.username,
        iat: Math.floor(Date.now() / 1000)
      },
      JWT_SECRET,
      { 
        expiresIn: process.env.JWT_EXPIRES_IN || '24h',
        algorithm: JWT_ALGORITHM 
      }
    );
  }

  // Verify token
  verifyToken(token) {
    return jwt.verify(token, JWT_SECRET, { algorithms: [JWT_ALGORITHM] });
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ” Token Generation Example

// Example: Creating a JWT token (server-side)
const authService = new AuthService();

const user = {
  id: "user123",
  username: "john_doe"
};

const token = authService.generateToken(user);
console.log("Generated Token:", token);

// Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwidXNlcm5hbWUiOiJqb2huX2RvZSIsImlhdCI6MTY0MjAwMDAwMCwiZXhwIjoxNjQyMDg2NDAwfQ.signature
Enter fullscreen mode Exit fullscreen mode

πŸ“‘ Making API Requests

# Using curl
curl -X POST http://localhost:8000/translate \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello world"}'
Enter fullscreen mode Exit fullscreen mode
// Using JavaScript/Node.js
const response = await fetch('http://localhost:8000/translate', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ text: 'Hello world' })
});
Enter fullscreen mode Exit fullscreen mode

Approach 2: Public + Private Key

πŸ” When to Use (Current Implementation)

  • Distributed systems (microservices)
  • Third-party integration
  • Enhanced security requirements
  • Token generation separation from verification
  • Multiple token issuers

πŸ› οΈ Setup

Step 1: Generate RSA Key Pair

# Using the provided generator
npm run generate-keys

# Or manually with OpenSSL
openssl genrsa -out jwt-private.key 2048
openssl rsa -in jwt-private.key -pubout -out jwt-public.key
Enter fullscreen mode Exit fullscreen mode

Generate RSA Key Pair

Step 2: Configure Environment

# .env file
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"
JWT_ALGORITHM=RS256
Enter fullscreen mode Exit fullscreen mode

Step 3: Server Configuration (Current)

// authService.js (Public Key approach - current implementation)
import jwt from 'jsonwebtoken';

const JWT_PUBLIC_KEY = process.env.JWT_PUBLIC_KEY;
const JWT_ALGORITHM = process.env.JWT_ALGORITHM || 'RS256';

class AuthService {
  constructor() {
    this.jwtPublicKey = this.loadPublicKey();
  }

  loadPublicKey() {
    if (process.env.JWT_PUBLIC_KEY) {
      return process.env.JWT_PUBLIC_KEY.replace(/\\n/g, '\n');
    }
    console.warn('⚠️  WARNING: No JWT public key found!');
    return null;
  }

  // Only verify tokens (no generation on server)
  verifyToken(token) {
    return new Promise((resolve, reject) => {
      jwt.verify(token, this.jwtPublicKey, { algorithms: [this.jwtAlgorithm] }, (err, decoded) => {
        if (err) reject(err);
        else resolve(decoded);
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ” Token Generation (External)

Important: With public/private key approach, tokens are generated externally using the private key.

Using Node.js

// external-token-generator.js
import jwt from 'jsonwebtoken';
import fs from 'fs';

const privateKey = fs.readFileSync('./jwt-private.key', 'utf8');

function generateToken(user) {
  const payload = {
    sub: user.id,
    username: user.username,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) // 24 hours
  };

  return jwt.sign(payload, privateKey, { algorithm: 'RS256' });
}

// Example usage
const user = { id: "user123", username: "john_doe" };
const token = generateToken(user);
console.log("Generated Token:", token);
Enter fullscreen mode Exit fullscreen mode

Using Python

# external_token_generator.py
import jwt
import time
from cryptography.hazmat.primitives import serialization

# Load private key
with open('./jwt-private.key', 'rb') as f:
    private_key = serialization.load_pem_private_key(
        f.read(),
        password=None
    )

def generate_token(user_id, username):
    payload = {
        'sub': user_id,
        'username': username,
        'iat': int(time.time()),
        'exp': int(time.time()) + (24 * 60 * 60)  # 24 hours
    }

    return jwt.encode(payload, private_key, algorithm='RS256')

# Example usage
token = generate_token("user123", "john_doe")
print(f"Generated Token: {token}")
Enter fullscreen mode Exit fullscreen mode

Using JWT.io (Manual)

  1. Go to jwt.io
  2. Select RS256 algorithm
  3. Enter your payload:
{
  "sub": "user123",
  "username": "john_doe",
  "iat": 1642000000,
  "exp": 1642086400
}
Enter fullscreen mode Exit fullscreen mode
  1. Paste your private key in the "Private Key" field
  2. Copy the generated token

jwt.io dashboard

πŸ“‘ Making API Requests

# Check auth status
curl http://localhost:8000/auth/status

# Verify your token
curl -X GET http://localhost:8000/auth/verify \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

# Make translation request
curl -X POST http://localhost:8000/translate \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello world"}'
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Testing

# Run the test suite with your token
JWT_TOKEN="your_generated_token_here" npm run test-auth

# Test the client
JWT_TOKEN="your_generated_token_here" node jwt-client.js
Enter fullscreen mode Exit fullscreen mode

terminal output


Comparison

Feature Secret Key + JWT Public + Private Key
Security Symmetric (shared secret) Asymmetric (separate keys)
Token Generation Server-side External/Client-side
Key Management Single secret key Public key on server, private key secure
Scalability Limited to single service Distributed systems friendly
Complexity Simple More complex
Use Case Internal APIs Third-party integrations
Algorithm HS256 RS256

comparison table

Security Considerations

Secret Key Approach

  • βœ… Simple implementation
  • βœ… Fast verification
  • ❌ Shared secret vulnerability
  • ❌ Key rotation complexity

Public/Private Key Approach

  • βœ… No shared secrets
  • βœ… Easy key rotation
  • βœ… Distributed token generation
  • ❌ Slightly slower verification
  • ❌ More complex setup

Best Practices

πŸ” Security

  1. Never commit private keys to version control
  2. Use environment variables for sensitive data
  3. Implement token expiration (reasonable timeframes)
  4. Validate all JWT claims (exp, iat, sub, etc.)
  5. Use HTTPS in production
  6. Implement rate limiting for authentication endpoints

πŸ—‚οΈ Key Management

# Add to .gitignore
echo "jwt-private.key" >> .gitignore
echo ".env" >> .gitignore

# Set proper file permissions
chmod 600 jwt-private.key  # Private key - owner read/write only
chmod 644 jwt-public.key   # Public key - readable by others
Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ Token Structure

Recommended JWT payload structure:

{
  "sub": "user_unique_identifier",
  "username": "human_readable_name", 
  "iat": 1642000000,
  "exp": 1642086400,
  "roles": ["user", "translator"],
  "permissions": ["read", "translate"]
}
Enter fullscreen mode Exit fullscreen mode

πŸ”„ Token Refresh

// Check token expiration before requests
function isTokenExpired(token) {
  try {
    const decoded = jwt.decode(token);
    return decoded.exp < Date.now() / 1000;
  } catch (error) {
    return true;
  }
}

// Auto-refresh logic
if (isTokenExpired(currentToken)) {
  currentToken = await generateNewToken();
}
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

Common Issues

1. "JWT public key not configured"

# Solution: Set the public key in environment
export JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----..."
Enter fullscreen mode Exit fullscreen mode

2. "Token verification failed"

# Check algorithm mismatch
# Ensure your token is signed with RS256 for public/private key approach
# Ensure your token is signed with HS256 for secret key approach
Enter fullscreen mode Exit fullscreen mode

3. "Token expired"

// Check token expiration
const decoded = jwt.decode(token);
console.log('Token expires at:', new Date(decoded.exp * 1000));
Enter fullscreen mode Exit fullscreen mode

4. "Invalid signature"

# Ensure you're using the correct private key for signing
# Verify the public key matches the private key used for signing
openssl rsa -in jwt-private.key -pubout | diff - jwt-public.key
Enter fullscreen mode Exit fullscreen mode

Debug Mode

# Enable debug logging
DEBUG=jwt* npm start

# Check token details without verification
node -e "console.log(JSON.stringify(require('jsonwebtoken').decode('your_token_here'), null, 2))"
Enter fullscreen mode Exit fullscreen mode

Testing Endpoints

# Health check (no auth required)
curl http://localhost:8000/health

# Auth status
curl http://localhost:8000/auth/status

# Token verification
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8000/auth/verify
Enter fullscreen mode Exit fullscreen mode

🎯 Next Steps

  1. Choose your approach based on your requirements
  2. Generate appropriate keys (secret or RSA pair)
  3. Configure environment variables
  4. Test token generation and verification
  5. Implement in your application
  6. Set up monitoring and logging

Production Checklist

  • [ ] Keys stored securely (env vars, key management service)
  • [ ] Private keys never on server (public/private approach)
  • [ ] HTTPS enabled
  • [ ] Token expiration configured
  • [ ] Rate limiting implemented
  • [ ] Logging and monitoring set up
  • [ ] Error handling implemented
  • [ ] Documentation updated

This tutorial covers both JWT authentication approaches. Choose the one that best fits your architecture and security requirements.

Top comments (0)