suhail
spoecs
9eafd9f
# Research: Authentication & API Security
**Feature**: 001-auth-security
**Date**: 2026-01-09
**Phase**: 0 - Research & Technical Decisions
## Overview
This document captures research findings and technical decisions for implementing authentication and API security using Better Auth (frontend) and JWT verification (backend).
## Research Questions & Resolutions
### 1. Token Expiry Duration
**Question**: Spec says 1 hour, user input says 7 days - which should we use?
**Decision**: **7 days**
**Rationale**:
- The spec explicitly excludes "Token refresh mechanism and refresh tokens" from scope
- Without refresh tokens, 1-hour expiry creates poor UX (users logged out every hour)
- This is a hackathon/MVP project where simplicity is prioritized
- 7 days balances security with usability for the initial release
- Industry standard for web apps *with refresh tokens* is 1 hour access + long-lived refresh
- Industry standard for web apps *without refresh tokens* is 7-30 days
**Alternatives Considered**:
- 1 hour: Too short without refresh mechanism, poor UX
- 24 hours: Reasonable middle ground, but 7 days is acceptable for MVP
- 30 days: Too long, increases security risk unnecessarily
**Implementation**: Set `exp` claim in JWT to 7 days (604800 seconds) from issuance
---
### 2. Better Auth Integration Pattern
**Question**: How should Better Auth be integrated in Next.js 16 App Router?
**Decision**: Use Better Auth with email/password provider and JWT plugin
**Research Findings**:
- Better Auth supports Next.js App Router with server-side session management
- JWT plugin allows issuing tokens that can be verified by external backends
- Configuration file: `lib/auth.ts` with email provider and JWT plugin
- Session management via Better Auth's built-in session handling
- Token accessible via `auth()` helper in server components
**Implementation Pattern**:
```typescript
// lib/auth.ts
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"
export const auth = betterAuth({
database: {
// Database connection for Better Auth's session storage
},
emailAndPassword: {
enabled: true,
},
plugins: [
jwt({
secret: process.env.BETTER_AUTH_SECRET!,
expiresIn: "7d",
})
],
})
```
**Alternatives Considered**:
- NextAuth.js: More popular but heavier, Better Auth is simpler for JWT use case
- Custom JWT implementation: Reinventing the wheel, Better Auth handles edge cases
- Auth0/Clerk: Third-party services, adds external dependency and cost
---
### 3. Backend JWT Verification Strategy
**Question**: How should FastAPI verify JWT tokens from Better Auth?
**Decision**: Use PyJWT library with FastAPI dependency injection
**Research Findings**:
- PyJWT is the standard Python library for JWT handling
- FastAPI's dependency injection system is ideal for auth middleware
- Better Auth uses HS256 (HMAC-SHA256) by default with shared secret
- Token verification should happen in a reusable dependency
**Implementation Pattern**:
```python
# src/core/security.py
import jwt
from fastapi import HTTPException, status
from src.core.config import settings
def verify_jwt_token(token: str) -> dict:
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"
)
# src/api/deps.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> int:
token = credentials.credentials
payload = verify_jwt_token(token)
user_id = payload.get("sub") # Better Auth uses 'sub' for user ID
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload"
)
return int(user_id)
```
**Alternatives Considered**:
- python-jose: Older library, PyJWT is more actively maintained
- Middleware approach: Less flexible than dependency injection
- Manual token parsing: Error-prone, PyJWT handles edge cases
---
### 4. Password Hashing Strategy
**Question**: How should passwords be hashed and verified?
**Decision**: Use passlib with bcrypt algorithm
**Research Findings**:
- Better Auth handles password hashing on the frontend side
- Backend needs to verify passwords for custom auth endpoints (if any)
- passlib is the standard Python library for password hashing
- bcrypt is industry-standard, resistant to rainbow table attacks
- Cost factor of 12 provides good security/performance balance
**Implementation Pattern**:
```python
# src/core/security.py
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
```
**Note**: Since Better Auth handles authentication, backend password hashing may only be needed for:
- Admin user creation scripts
- Testing utilities
- Future direct authentication endpoints
**Alternatives Considered**:
- argon2: More modern but requires C dependencies, complicates deployment
- scrypt: Good but bcrypt is more widely supported
- Plain SHA256: Insecure, vulnerable to rainbow tables
---
### 5. Frontend Token Storage
**Question**: Where should JWT tokens be stored in the frontend?
**Decision**: Use Better Auth's built-in session management (httpOnly cookies)
**Research Findings**:
- Better Auth stores session tokens in httpOnly cookies by default
- This prevents XSS attacks (JavaScript cannot access the token)
- Better Auth's `auth()` helper automatically includes token in requests
- For API calls to backend, extract token from Better Auth session
**Implementation Pattern**:
```typescript
// 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 // Better Auth provides token in session
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
...options.headers,
},
})
// Handle 401 responses
if (response.status === 401) {
// Redirect to login
window.location.href = '/auth/signin'
}
return response.json()
}
```
**Alternatives Considered**:
- localStorage: Vulnerable to XSS attacks
- sessionStorage: Same XSS vulnerability as localStorage
- Memory only: Lost on page refresh, poor UX
---
### 6. Error Handling for Authentication Failures
**Question**: How should authentication errors be communicated to users?
**Decision**: Use standardized error responses with appropriate HTTP status codes
**Research Findings**:
- 401 Unauthorized: Authentication required or failed
- 403 Forbidden: Authenticated but not authorized (not used in this spec)
- Generic error messages prevent information leakage
- Specific errors only in development mode
**Implementation Pattern**:
```python
# Backend error responses
{
"detail": "Invalid credentials", # Generic, doesn't reveal if email or password wrong
"error_code": "AUTH_FAILED"
}
{
"detail": "Token has expired",
"error_code": "TOKEN_EXPIRED"
}
{
"detail": "Invalid token",
"error_code": "TOKEN_INVALID"
}
```
**Security Considerations**:
- Never reveal whether email exists in database
- Never reveal which field (email/password) was incorrect
- Log detailed errors server-side for debugging
- Return generic errors to client
---
### 7. Database Schema Changes
**Question**: What changes are needed to the existing User model?
**Decision**: Add `password_hash` field to users table
**Research Findings**:
- Current User model has: id, email, name, created_at, updated_at
- Need to add: password_hash (string, nullable=False)
- Better Auth may also need its own tables for session management
- Migration should be reversible
**Implementation**:
```python
# alembic/versions/002_add_user_password.py
def upgrade():
op.add_column('users', sa.Column('password_hash', sa.String(255), nullable=False))
def downgrade():
op.drop_column('users', 'password_hash')
```
**Note**: Better Auth may create its own tables (sessions, accounts, etc.) - these should be in a separate migration or handled by Better Auth's migration system.
---
## Dependencies to Add
### Backend
- `PyJWT==2.8.0` - JWT encoding/decoding
- `passlib[bcrypt]==1.7.4` - Password hashing
- `python-multipart==0.0.6` - Form data parsing (for login forms)
### Frontend
- `better-auth` - Authentication library
- `@better-auth/react` - React hooks for Better Auth
---
## Environment Variables
### Backend (.env)
```
BETTER_AUTH_SECRET=<shared-secret-min-32-chars>
DATABASE_URL=<neon-postgres-url>
```
### Frontend (.env.local)
```
BETTER_AUTH_SECRET=<same-shared-secret>
NEXT_PUBLIC_API_URL=http://localhost:8000
```
**Critical**: BETTER_AUTH_SECRET must be identical in both frontend and backend.
---
## Security Checklist
- [x] Passwords hashed with bcrypt (cost factor 12)
- [x] JWT tokens signed with HS256 and shared secret
- [x] Tokens expire after 7 days
- [x] httpOnly cookies prevent XSS attacks
- [x] Generic error messages prevent information leakage
- [x] HTTPS required in production (documented in assumptions)
- [x] User ID extracted from validated token, not request parameters
- [x] All task endpoints require authentication
- [x] Database queries filtered by authenticated user ID
---
## Next Steps
Phase 1 will use these research findings to:
1. Create data-model.md with User entity updates
2. Generate API contracts for auth endpoints
3. Create quickstart.md with setup instructions
4. Update agent context files with new dependencies