Skip to content

Commit 25f555d

Browse files
baijumclaude
andcommitted
feat: add auth checks to spec/validator and JWT_SECRET to infrastructure scripts
Add JWT_SECRET and CORS_ORIGINS to spec optional env vars, Authentication section, and compatibility checklist. Validator now warns (not fails) when JWT_SECRET or JWT library is missing. Infrastructure scripts generate, rotate, and inject JWT_SECRET alongside existing DB/S3 credentials. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ba0aa3a commit 25f555d

8 files changed

Lines changed: 68 additions & 3 deletions

File tree

docs/spec.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ Applications must support configuration via environment variables.
8585
| `S3_BUCKET` | Storage bucket name |
8686
| `S3_ACCESS_KEY` | Storage access key |
8787
| `S3_SECRET_KEY` | Storage secret key |
88+
| `JWT_SECRET` | Secret key for JWT token signing |
89+
| `CORS_ORIGINS` | Comma-separated allowed CORS origins |
8890
| `EMAIL_API_KEY` | Transactional email API key |
8991
| `EMAIL_FROM` | Sender email address |
9092

@@ -177,6 +179,16 @@ Applications should follow basic security practices:
177179
- Validate input data
178180
- Enforce authentication where required
179181

182+
## Authentication
183+
184+
Applications that require user authentication should use:
185+
186+
- **JWT tokens** (HS256) for session management
187+
- **bcrypt** for password hashing
188+
- **HTTPBearer** scheme for token transport
189+
190+
The app-template provides this infrastructure out of the box. See the [app-template README](https://github.com/towlion/app-template#authentication) for usage.
191+
180192
## Compatibility Checklist
181193

182194
To remain compatible with the Towlion platform, applications must:
@@ -195,6 +207,7 @@ To remain compatible with the Towlion platform, applications must:
195207
- [ ] `app/main.py` uses FastAPI
196208
- [ ] Python dependencies (`requirements.txt` or `pyproject.toml`) include `fastapi` and `uvicorn`
197209
- [ ] No hardcoded secrets in source code
210+
- [ ] `deploy/env.template` contains `JWT_SECRET` (if using authentication)
198211

199212
**Runtime:**
200213
- [ ] Expose HTTP service on port 8000

infrastructure/create-app-credentials.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ info "Database: $APP_DB, User: $APP_USER"
4545
# Generate passwords
4646
DB_PASSWORD=$(openssl rand -base64 24)
4747
S3_PASSWORD=$(openssl rand -base64 24)
48+
JWT_SECRET=$(openssl rand -base64 32)
4849

4950
# PostgreSQL user creation (idempotent)
5051
info "Checking PostgreSQL user..."
@@ -127,6 +128,7 @@ DB_USER=${APP_USER}
127128
DB_PASSWORD=${DB_PASSWORD}
128129
S3_ACCESS_KEY=${MINIO_USER}
129130
S3_SECRET_KEY=${S3_PASSWORD}
131+
JWT_SECRET=${JWT_SECRET}
130132
EOF
131133

132134
# Set permissions
@@ -143,6 +145,7 @@ info "PostgreSQL User: ${APP_USER}"
143145
info "PostgreSQL DB: ${APP_DB}"
144146
info "MinIO User: ${MINIO_USER}"
145147
info "MinIO Bucket: ${APP_NAME}-uploads"
148+
info "JWT Secret: (generated)"
146149
info "Credentials File: ${CREDENTIALS_FILE}"
147150
echo ""
148151
info "✓ All credentials provisioned successfully"

infrastructure/deploy-blue-green.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ if [ -f "$CREDENTIALS_FILE" ]; then
125125
&& sed -i "s|^S3_BUCKET=.*|S3_BUCKET=${APP_NAME}-uploads|" deploy/.env \
126126
|| echo "S3_BUCKET=${APP_NAME}-uploads" >> deploy/.env
127127
fi
128+
if [ -n "${JWT_SECRET:-}" ]; then
129+
grep -q "^JWT_SECRET=" deploy/.env \
130+
&& sed -i "s|^JWT_SECRET=.*|JWT_SECRET=${JWT_SECRET}|" deploy/.env \
131+
|| echo "JWT_SECRET=${JWT_SECRET}" >> deploy/.env
132+
fi
128133
info "deploy/.env updated with per-app credentials"
129134
else
130135
warn "Per-app credentials not found at $CREDENTIALS_FILE"

infrastructure/rotate-credentials.sh

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ usage() {
3131
echo " --type Type of credentials to rotate (default: all)"
3232
echo " db — PostgreSQL password only"
3333
echo " s3 — MinIO password only"
34-
echo " all — Both PostgreSQL and MinIO"
34+
echo " jwt — JWT secret only"
35+
echo " all — PostgreSQL, MinIO, and JWT"
3536
echo ""
3637
echo "Examples:"
3738
echo " $0 todo-app"
@@ -55,8 +56,8 @@ while [ $# -gt 0 ]; do
5556
--type)
5657
shift
5758
ROTATE_TYPE="${1:-all}"
58-
if [[ ! "$ROTATE_TYPE" =~ ^(db|s3|all)$ ]]; then
59-
error "Invalid type: $ROTATE_TYPE (must be db, s3, or all)"
59+
if [[ ! "$ROTATE_TYPE" =~ ^(db|s3|jwt|all)$ ]]; then
60+
error "Invalid type: $ROTATE_TYPE (must be db, s3, jwt, or all)"
6061
usage
6162
fi
6263
;;
@@ -174,6 +175,29 @@ if [[ "$ROTATE_TYPE" == "s3" || "$ROTATE_TYPE" == "all" ]]; then
174175
S3_SECRET_KEY="$NEW_S3_PASSWORD"
175176
fi
176177

178+
# Rotate JWT secret
179+
if [[ "$ROTATE_TYPE" == "jwt" || "$ROTATE_TYPE" == "all" ]]; then
180+
info "--- Rotating JWT secret ---"
181+
182+
NEW_JWT_SECRET=$(openssl rand -base64 32)
183+
184+
# Update credentials file
185+
grep -q '^JWT_SECRET=' "$CREDENTIALS_FILE" \
186+
&& sed -i "s|^JWT_SECRET=.*|JWT_SECRET=${NEW_JWT_SECRET}|" "$CREDENTIALS_FILE" \
187+
|| echo "JWT_SECRET=${NEW_JWT_SECRET}" >> "$CREDENTIALS_FILE"
188+
info "Credentials file updated: $CREDENTIALS_FILE"
189+
190+
# Update app deploy/.env
191+
if [ -f "$APP_ENV_FILE" ]; then
192+
grep -q '^JWT_SECRET=' "$APP_ENV_FILE" \
193+
&& sed -i "s|^JWT_SECRET=.*|JWT_SECRET=${NEW_JWT_SECRET}|" "$APP_ENV_FILE" \
194+
|| echo "JWT_SECRET=${NEW_JWT_SECRET}" >> "$APP_ENV_FILE"
195+
info "App .env updated: $APP_ENV_FILE"
196+
else
197+
warn "App .env not found at $APP_ENV_FILE — manual update needed"
198+
fi
199+
fi
200+
177201
# Restart the app container to pick up new credentials
178202
info "--- Restarting app container ---"
179203
cd "/opt/apps/${APP_NAME}"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
APP_DOMAIN=example.com
22
DATABASE_URL=postgresql://user:pass@postgres:5432/app_db
33
REDIS_URL=redis://redis:6379/0
4+
JWT_SECRET=test-secret
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
fastapi>=0.100.0
22
uvicorn>=0.23.0
33
alembic>=1.12.0
4+
pyjwt>=2.8

tests/test_validator.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ def test_valid_app_has_dependencies(self):
9292
assert has_result(v, Result.PASS, "fastapi")
9393
assert has_result(v, Result.PASS, "uvicorn")
9494

95+
def test_valid_app_env_has_jwt_secret(self):
96+
v = run_validator(VALID_APP, tier=2)
97+
assert has_result(v, Result.PASS, "JWT_SECRET")
98+
99+
def test_missing_jwt_secret_warns(self):
100+
v = run_validator(INVALID_APP, tier=2)
101+
assert has_result(v, Result.WARN, "JWT_SECRET")
102+
95103

96104
class TestStrictMode:
97105
def test_strict_mode_treats_warnings_as_errors(self):

validator/validate.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ def _check_env_template(self):
177177
else:
178178
self._record(Result.FAIL, f"env.template contains {var}", "not found")
179179

180+
if "JWT_SECRET" in content:
181+
self._record(Result.PASS, "env.template contains JWT_SECRET")
182+
else:
183+
self._record(Result.WARN, "env.template contains JWT_SECRET", "not found (needed if using authentication)")
184+
180185
def _check_caddyfile(self):
181186
content = self._read("deploy", "Caddyfile")
182187
if content is None:
@@ -239,6 +244,11 @@ def _check_dependencies(self):
239244
else:
240245
self._record(Result.WARN, f"{deps_file} contains alembic", "not found (only needed if using database migrations)")
241246

247+
if "pyjwt" in content_lower or "python-jose" in content_lower:
248+
self._record(Result.PASS, f"{deps_file} contains JWT library")
249+
else:
250+
self._record(Result.WARN, f"{deps_file} contains JWT library", "not found (needed if using authentication)")
251+
242252
def _check_secrets(self):
243253
secret_patterns = [
244254
(r'(?i)password\s*=\s*["\'][^"\']+["\']', "hardcoded password"),

0 commit comments

Comments
 (0)