Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ detector-logs
app-logs
start.sh
config.dev.json
*.md
*.md
.tokeignore
61 changes: 32 additions & 29 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,6 @@ jobs:
contents: read
environment: copilot

services:
mysql:
image: mysql:8.4
env:
MYSQL_DATABASE: app_db
MYSQL_ROOT_PASSWORD: 1234
ports: [ "3306:3306" ]
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -p1234"
--health-interval=10s --health-timeout=5s --health-retries=10

nginx:
image: nginx:1.27-alpine
ports: [ "8080:80" ]

steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
Expand All @@ -43,31 +28,49 @@ jobs:
env:
DEPLOY_KEY: ${{ secrets.BOTDETECTOR_DEPLOY_KEY }}
run: |
install -m 700 -d ~/.ssh
mkdir -p ~/.ssh
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan github.com >> ~/.ssh/known_hosts
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV

- name: Trust github.com host key
- name: Install dependencies
run: |
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
sudo apt-get update
sudo apt-get install -y age docker-compose-plugin

- name: Tools
- name: Create Test Env
run: |
sudo apt-get update
sudo apt-get install -y mysql-client curl age
# Create .env.test for local test runner and setupTestDB.ts
echo "TEST_DB_HOST=localhost" > .env.test
echo "TEST_DB_PORT=3308" >> .env.test
echo "TEST_DB_USER=root" >> .env.test
echo "TEST_DB_PASSWORD=very_secure_password_for_tests" >> .env.test
echo "TEST_DB_NAME=my_auth_tests_db" >> .env.test

# We assume config.test.json is committed by user

- name: Start Services (Build & Up)
env:
SSH_KEY_PATH: ~/.ssh/id_ed25519
run: |
chmod +x ./start.sh
# Uses committed config.test.json
./start.sh config.test.json auth-test

- name: Wait for MySQL
run: |
for i in {1..60}; do mysqladmin ping -h 127.0.0.1 -uroot -p1234 --silent && break; sleep 2; done
mysql -h 127.0.0.1 -uroot -p1234 -e "CREATE DATABASE IF NOT EXISTS app_db;"
# Wait for exposed port 3307
for i in {1..60}; do mysqladmin ping -h 127.0.0.1 -P 3308 -uroot -pvery_secure_password_for_tests --silent && break; sleep 2; done

- name: Install dependencies
run: npm install

- name: Build
run: npm run build
- name: Setup Database Schema
run: |
npm ci
# Assuming setupTestDB.ts handles creating tables
npx ts-node test/setup/setupTestDB.ts

- name: Run All Tests
run: |
npm test
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ public_key
detector-logs
app-logs
config.dev.json
.vscode/
.vscode/
.tokeignore
32 changes: 32 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,38 @@ sequenceDiagram
A->>C: Return tokens + cookies
```

### Custom MFA Flow

```mermaid
sequenceDiagram
participant C as Client/BFF
participant A as Auth Service
participant D as Database
participant E as Email Service

C->>A: POST /custom/mfa:reason?rand=...
A->>A: Rate limit check
A->>A: Session validation
A->>A: Verify BFF client IP
A->>A: Generate MFA code + Magic Link JWT
A->>D: Store hashed code in mfa_codes
A->>E: Send MFA email with link
A->>C: Return {ok: true}

Note over C,A: User clicks magic link

C->>A: GET /auth/verify-custom-mfa?visitor=...&temp=...&random=...
A->>A: Validate JWT signature
A->>A: Compare random hash (timingSafeEqual)
A->>C: Return {link: "Custom MFA"}

C->>A: POST /auth/verify-custom-mfa?visitor=...&... {code}
A->>D: Verify code_hash against mfa_codes
A->>D: Delete code (atomic consumption)
A->>D: Rotate session tokens
A->>C: Return new access token + cookies
```

## Scalability Considerations

### Horizontal Scaling
Expand Down
15 changes: 11 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,17 @@ test/
│ ├── verification.test.ts
│ ├── security.test.ts
│ └── ...
└── refreshTokens-test/ # Refresh token tests
├── generateRefreshToken.test.ts
├── rotateRefreshToken.test.ts
└── ...
├── refreshTokens-test/ # Refresh token tests
│ ├── generateRefreshToken.test.ts
│ ├── rotateRefreshToken.test.ts
│ └── ...
├── initCustomMfaFlow/ # Custom MFA initialization tests
│ └── init.test.ts
├── verifyCustomMfaController/ # Custom MFA verification tests
│ └── verify.test.ts
└── utils/ # Utility tests
└── verifyMfaCode/
└── verifyMfaCode.test.ts
```

## Development Workflow
Expand Down
10 changes: 8 additions & 2 deletions decrypt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@
set -e

KEY_FILE="/run/secrets/age_key"
OUT="/run/app/config.json"
OUT=${CONFIG_PATH:-"/run/app/config.json"}
FILE=${ENCRYPTED_SOURCE:-"config.json.age"}

if [ ! -f "$KEY_FILE" ]; then
echo "ERROR: Secret key file not found at $KEY_FILE"
exit 1
fi

if [ ! -f "$FILE" ]; then
echo "ERROR: Encrypted config file not found at $FILE"
exit 1
fi

echo "Decrypting secrets..."
age -d -i "$KEY_FILE" -o "$OUT" /app/config.json.age
age -d -i "$KEY_FILE" -o "$OUT" $FILE
chmod 0400 "$OUT"

echo "Secrets decrypted and loaded into environment."
Expand Down
52 changes: 52 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
services:
mysql-test:
image: mysql:8.0
restart: unless-stopped
command: --local-infile=1
environment:
MYSQL_ROOT_PASSWORD: ${TEST_DB_PASSWORD}
MYSQL_DATABASE: ${TEST_DB_NAME}
MYSQL_USER: ${TEST_DB_USER}
MYSQL_PASSWORD: ${TEST_DB_PASSWORD}
ports:
- "3308:3306"
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 20s
retries: 10
tmpfs:
- /var/lib/mysql:rw,noexec,nosuid,nodev,size=512m

auth-test:
image: jwtauth-service:latest
extra_hosts:
- "host.docker.internal:host-gateway"
pull_policy: never
read_only: true
build:
context: ./
dockerfile: Dockerfile
ssh:
- default
restart: unless-stopped
cap_drop: ["ALL"]
user: 10001:10001
depends_on:
mysql-test:
condition: service_healthy
volumes:
- ./app-logs:/app/logs:rw
- ./detector-logs:/app/node_modules/@riavzon/botdetector/logs:rw
tmpfs:
- /run/app:rw,noexec,nosuid,nodev,uid=10001,gid=10001,size=1m
pids_limit: 200
ports:
- "10000:10000"
secrets:
- age_key
security_opt:
- "no-new-privileges:true"

secrets:
age_key:
file: ./age_key
70 changes: 35 additions & 35 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,46 +34,46 @@
},
"dependencies": {
"@riavzon/botdetector": "github:Sergo706/botDetector#71a1c78e65cdd609c0a389496be84eb8797fac33",
"@types/date-fns": "^2.5.3",
"@types/ejs": "^3.1.5",
"@types/he": "^1.2.3",
"@types/jsonwebtoken": "^9.0.9",
"@types/sanitize-html": "^2.16.0",
"argon2": "^0.44.0",
"cookie-parser": "^1.4.7",
"date-fns": "^4.1.0",
"ejs": "^4.0.1",
"express": "^5.1.0",
"he": "^1.2.0",
"helmet": "^8.1.0",
"ip-range-check": "^0.2.0",
"jsonwebtoken": "^9.0.2",
"lru-cache": "^11.1.0",
"mysql2": "^3.14.0",
"pino": "^10.0.0",
"pino-http": "^10.5.0",
"rate-limiter-flexible": "^9.0.1",
"resend": "^6.0.1",
"sanitize-html": "^2.17.0",
"telegraf": "^4.16.3",
"ts-node": "^10.9.2",
"argon2": "0.44.0",
"cookie-parser": "1.4.7",
"date-fns": "4.1.0",
"ejs": "4.0.1",
"express": "5.1.0",
"he": "1.2.0",
"helmet": "8.1.0",
"ip-range-check": "0.2.0",
"jsonwebtoken": "9.0.2",
"lru-cache": "11.1.0",
"mysql2": "3.14.0",
"pino": "10.0.0",
"pino-http": "10.5.0",
"rate-limiter-flexible": "9.0.1",
"resend": "6.0.1",
"sanitize-html": "2.17.0",
"telegraf": "4.16.3",
"ts-node": "10.9.2",
"zod": "^4.0.14"
},
"overrides": {
"pino": "^10.0.0",
"pino": "10.0.0",
"ua-parser-js": "2.0.3"
},
"devDependencies": {
"@rollup/plugin-terser": "^0.4.4",
"@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.3",
"@types/node": "^25.1.0",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "^4.0.18",
"dotenv": "^17.2.2",
"typedoc": "^0.28.13",
"typedoc-plugin-markdown": "^4.8.1",
"vitepress": "^1.6.4",
"vitest": "^4.0.18"
"@rollup/plugin-terser": "0.4.4",
"@types/jsonwebtoken": "9.0.9",
"@types/sanitize-html": "2.16.0",
"@types/date-fns": "2.5.3",
"@types/ejs": "3.1.5",
"@types/he": "1.2.3",
"@types/cookie-parser": "1.4.9",
"@types/express": "5.0.3",
"@types/node": "25.1.0",
"@vitest/coverage-v8": "4.0.18",
"@vitest/ui": "4.0.18",
"dotenv": "17.2.2",
"typedoc": "0.28.17",
"typedoc-plugin-markdown": "4.8.1",
"vitepress": "1.6.4",
"vitest": "4.0.18"
}
}
Loading
Loading