Spaces:
Sleeping
Quickstart: Authentication & API Security
Feature: 001-auth-security Date: 2026-01-09
Overview
This guide provides step-by-step instructions for setting up and testing the authentication and API security feature. Follow these steps to configure Better Auth on the frontend and JWT verification on the backend.
Prerequisites
- Node.js 18+ and npm installed
- Python 3.11+ installed
- PostgreSQL database (Neon Serverless) accessible
- Git repository cloned
- Existing task CRUD functionality working (from Spec 001-task-crud)
Setup Instructions
1. Environment Configuration
Backend Environment Variables
Create or update backend/.env:
# Database
DATABASE_URL=postgresql://user:password@host:5432/database
# Authentication
BETTER_AUTH_SECRET=your-secret-key-min-32-characters-long-and-random
# Application
APP_NAME=Task CRUD API
DEBUG=True
CORS_ORIGINS=http://localhost:3000
Important: Generate a strong random secret for BETTER_AUTH_SECRET:
# Generate a secure random secret (32+ characters)
python -c "import secrets; print(secrets.token_urlsafe(32))"
Frontend Environment Variables
Create or update frontend/.env.local:
# API Configuration
NEXT_PUBLIC_API_URL=http://localhost:8000
# Authentication (MUST match backend secret)
BETTER_AUTH_SECRET=your-secret-key-min-32-characters-long-and-random
# Better Auth Database (optional - uses same as backend)
DATABASE_URL=postgresql://user:password@host:5432/database
Critical: The BETTER_AUTH_SECRET must be identical in both frontend and backend.
2. Install Dependencies
Backend Dependencies
cd backend
# Add new dependencies to requirements.txt
echo "PyJWT==2.8.0" >> requirements.txt
echo "passlib[bcrypt]==1.7.4" >> requirements.txt
echo "python-multipart==0.0.6" >> requirements.txt
# Install all dependencies
pip install -r requirements.txt
Frontend Dependencies
cd frontend
# Install Better Auth
npm install better-auth @better-auth/react
# Install development dependencies (if not already installed)
npm install --save-dev @types/node @types/react @types/react-dom
3. Database Migration
Run Migration to Add Password Field
cd backend
# Create migration
alembic revision --autogenerate -m "Add password_hash to users"
# Review the generated migration file in alembic/versions/
# Ensure it adds password_hash column to users table
# Apply migration
alembic upgrade head
Expected Migration:
def upgrade():
op.add_column('users', sa.Column('password_hash', sa.String(255), nullable=False))
def downgrade():
op.drop_column('users', 'password_hash')
4. Backend Implementation
Create Security Module
Create backend/src/core/security.py:
import jwt
from datetime import datetime, timedelta
from passlib.context import CryptContext
from fastapi import HTTPException, status
from src.core.config import settings
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
"""Hash a password using bcrypt."""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash."""
return pwd_context.verify(plain_password, hashed_password)
def create_jwt_token(user_id: int, email: str) -> str:
"""Create a JWT token for a user."""
payload = {
"sub": str(user_id),
"email": email,
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(days=7),
"iss": "better-auth"
}
return jwt.encode(payload, settings.BETTER_AUTH_SECRET, algorithm="HS256")
def verify_jwt_token(token: str) -> dict:
"""Verify and decode a JWT token."""
try:
payload = jwt.decode(
token,
settings.BETTER_AUTH_SECRET,
algorithms=["HS256"]
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
Update Dependencies
Modify backend/src/api/deps.py:
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlmodel import Session
from src.core.database import get_session
from src.core.security import verify_jwt_token
security = HTTPBearer()
def get_db() -> Generator[Session, None, None]:
"""Get database session dependency."""
yield from get_session()
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> int:
"""
Get current user ID from JWT token.
Extracts and verifies JWT from Authorization header.
"""
token = credentials.credentials
payload = verify_jwt_token(token)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload"
)
return int(user_id)
Update Configuration
Modify backend/src/core/config.py:
class Settings(BaseSettings):
# ... existing fields ...
# Authentication
BETTER_AUTH_SECRET: str # Remove Optional, make required
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRATION_DAYS: int = 7
5. Frontend Implementation
Configure Better Auth
Create frontend/src/lib/auth.ts:
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"
export const auth = betterAuth({
database: {
provider: "postgres",
url: process.env.DATABASE_URL!,
},
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
plugins: [
jwt({
secret: process.env.BETTER_AUTH_SECRET!,
expiresIn: "7d",
})
],
secret: process.env.BETTER_AUTH_SECRET!,
})
Update API Client
Modify frontend/src/lib/api.ts:
import { auth } from './auth'
async function fetchAPI<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const session = await auth()
const token = session?.token
const url = `${API_BASE_URL}${endpoint}`
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
...options.headers,
},
})
if (response.status === 401) {
// Redirect to login
if (typeof window !== 'undefined') {
window.location.href = '/auth/signin'
}
throw new APIError('Authentication required', 401)
}
if (!response.ok) {
const errorData: ErrorResponse = await response.json().catch(() => ({
detail: 'An unexpected error occurred',
}))
throw new APIError(
errorData.detail,
response.status,
errorData.error_code,
errorData.field_errors
)
}
return response.json()
}
6. Testing
Backend Tests
cd backend
# Test authentication endpoints
pytest tests/test_auth.py -v
# Test JWT protection on task endpoints
pytest tests/test_tasks.py -v
# Run all tests
pytest -v
Manual Testing with curl
Sign Up:
curl -X POST http://localhost:8000/api/auth/signup \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "SecurePass123!",
"name": "Test User"
}'
Sign In:
curl -X POST http://localhost:8000/api/auth/signin \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "SecurePass123!"
}'
Access Protected Endpoint:
# Save token from signin response
TOKEN="your-jwt-token-here"
curl -X GET http://localhost:8000/api/tasks \
-H "Authorization: Bearer $TOKEN"
Test Unauthorized Access:
# Should return 401
curl -X GET http://localhost:8000/api/tasks
7. Running the Application
Start Backend
cd backend
uvicorn src.main:app --reload --port 8000
Start Frontend
cd frontend
npm run dev
Access Application
- Frontend: http://localhost:3000
- Backend API: http://localhost:8000
- API Docs: http://localhost:8000/docs
Verification Checklist
Backend Verification
-
BETTER_AUTH_SECRETis set in backend/.env - PyJWT, passlib, python-multipart installed
- Database migration applied (password_hash column exists)
-
src/core/security.pycreated with JWT functions -
src/api/deps.pyupdated with JWT verification - Backend starts without errors:
uvicorn src.main:app --reload - API docs accessible at http://localhost:8000/docs
Frontend Verification
-
BETTER_AUTH_SECRETmatches backend (identical value) - better-auth and @better-auth/react installed
-
src/lib/auth.tscreated with Better Auth config -
src/lib/api.tsupdated to include JWT in headers - Frontend starts without errors:
npm run dev - Can access http://localhost:3000
Integration Verification
- User can sign up with email/password
- User can sign in and receive JWT token
- Authenticated requests to /api/tasks succeed
- Unauthenticated requests to /api/tasks return 401
- User can only see their own tasks
- Token expires after 7 days (test with modified exp claim)
Troubleshooting
"Invalid token" errors
Cause: BETTER_AUTH_SECRET mismatch between frontend and backend
Solution: Verify both .env files have identical BETTER_AUTH_SECRET values
"Token has expired" immediately
Cause: System clock skew or incorrect exp claim
Solution: Check system time, verify token exp claim is 7 days in future
"Not authenticated" on all requests
Cause: Token not being included in Authorization header
Solution: Check frontend api.ts includes Authorization: Bearer ${token} header
Database connection errors
Cause: DATABASE_URL incorrect or database not accessible
Solution: Verify DATABASE_URL format and database is running
Import errors for better-auth
Cause: Package not installed or wrong version
Solution: Run npm install better-auth @better-auth/react in frontend directory
Next Steps
After completing this setup:
- Run
/sp.tasksto generate implementation tasks - Implement authentication endpoints (signup, signin)
- Implement JWT verification middleware
- Update task endpoints to require authentication
- Create frontend auth pages (signin, signup)
- Test end-to-end authentication flow
- Deploy to production with HTTPS enabled
Security Reminders
- ✅ Never commit .env files to git
- ✅ Use HTTPS in production
- ✅ Rotate BETTER_AUTH_SECRET periodically
- ✅ Use strong passwords (min 8 chars, complexity requirements)
- ✅ Monitor for suspicious authentication attempts
- ✅ Keep dependencies updated for security patches
Reference Documentation
- Better Auth: https://better-auth.com/docs
- PyJWT: https://pyjwt.readthedocs.io/
- FastAPI Security: https://fastapi.tiangolo.com/tutorial/security/
- JWT.io: https://jwt.io/ (for debugging tokens)