PingPoint uses three distinct authentication models:
| Access Type | Authentication | Scope | Description |
|---|---|---|---|
| Broker Session | JWT cookie (pingpoint_session) |
Full broker account | Web console access, all CRUD operations |
| Driver Token | URL token (driverToken) |
Single load | Mobile-friendly load updates, location pings |
| Public Tracking | URL token (trackingToken) |
Single load, read-only | Customer-facing tracking link |
- JWT stored in HTTP-only, secure cookie
- 24-hour expiration
- SameSite: lax for CSRF protection
- All queries scoped by
brokerId
- Token embedded in URL (magic link sent via SMS/email)
- Token tied to specific load
- Can submit location pings and update stop status
- Cannot access other loads or broker data
- Read-only access token
- Shared with shippers/receivers
- Returns minimal data (no PII)
- Subject to TTL and rate limiting
- Entropy: 32 bytes of cryptographic randomness
- Format: Base64url encoded (URL-safe)
- Generation:
crypto.randomBytes(32).toString('base64url')
Public tracking links expire after delivery:
- Active loads: Always accessible
- Delivered loads: Accessible for
PUBLIC_TRACKING_TTL_DAYS(default: 7 days) after delivery - Expired: Returns HTTP 410 Gone
Public tracking endpoints are rate-limited:
- Limit:
PUBLIC_TRACKING_RPMrequests per minute per IP+token (default: 60) - Caching: Responses cached for 10 seconds to reduce database load
- Exceeded: Returns HTTP 429 Too Many Requests
Public tracking responses include only safe fields:
Included:
- Load number, status, creation date
- Stop locations (city/state only), scheduled times, actual arrival/departure
- Last known location (lat/lng), timestamp
Excluded:
- Broker ID, driver ID, driver contact info
- Rate/billing information
- Webhook configurations
- Internal notes
All GPS pings are validated:
| Check | Requirement | Action |
|---|---|---|
| Latitude range | -90 to +90 | Reject if out of range |
| Longitude range | -180 to +180 | Reject if out of range |
| Type validation | Must be finite numbers | Reject if invalid |
- Future limit: Reject if timestamp > now +
GPS_MAX_FUTURE_SKEW_SECONDS(default: 300s / 5 min) - Age limit: Reject if timestamp < now -
GPS_MAX_AGE_HOURS(default: 24 hours)
- Maximum accuracy:
GPS_MAX_ACCURACY_METERS(default: 5000m) - Pings with accuracy > threshold are rejected
- Geofence evaluation skipped for low-accuracy pings
Detects physically impossible location jumps:
- Fetch last accepted ping for same driver+load
- Calculate distance (Haversine formula)
- Calculate time delta
- Compute implied speed (mph)
- If speed >
GPS_MAX_SPEED_MPH(default: 120), reject with 422
Before accepting a ping:
- Verify driver is assigned to the load
- Verify load belongs to the broker context
- Reject with 403 if unauthorized
- Rejected pings logged with reason code (not raw PII)
- Format:
[TrackingPing] REJECTED reason=<code> token=<truncated> - Console logging for MVP; production should use structured logging
| Limiter | Key | Limit | Window |
|---|---|---|---|
| Per-load | load:{loadId}:{driverId} |
1 ping | 30 seconds |
| IP burst | IP address | TRACKING_PING_RPM (120) |
1 minute |
| Limiter | Key | Limit | Window |
|---|---|---|---|
| Token+IP | {ip}:{token} |
PUBLIC_TRACKING_RPM (60) |
1 minute |
- MVP uses in-memory rate limiting
- State cleared on server restart
- Production should use Redis for distributed rate limiting
PingPoint separates login from signup to prevent unauthorized account creation:
Signup Flow (new accounts):
1. User enters email on /signup page
2. POST /api/brokers/signup creates account
3. Server generates verification token (48h TTL)
4. Verification email sent automatically
5. User clicks link
6. Server verifies token, creates JWT session
7. JWT stored in HTTP-only cookie
Login Flow (existing accounts):
1. User enters email on /login page
2. POST /api/brokers/login validates account exists
3. If account not found → 404 ACCOUNT_NOT_FOUND (user redirected to signup)
4. If email not verified → 403 EMAIL_NOT_VERIFIED
5. If trusted device → instant login
6. If untrusted device → magic link sent, user clicks to verify
Feature Flag:
AUTH_AUTO_CREATE_BROKER(default: false)- When false:
/api/brokers/ensurereturns 404 for unknown emails - When true: legacy behavior (auto-creates accounts)
Email Normalization:
- All auth operations use
email.trim().toLowerCase() - Utility:
server/utils/normalizeEmail.ts
Rate Limiting:
| Endpoint | Limit | Window |
|---|---|---|
/api/brokers/signup |
5 requests | 60 seconds |
/api/brokers/login |
10 requests | 60 seconds |
/api/brokers/send-verification |
5 requests | 60 seconds |
All broker API calls:
- Extract JWT from cookie
- Verify signature and expiration
- Load broker from database
- Scope all queries by
brokerId
1. Broker assigns driver to load
2. System generates unique driverToken
3. Magic link sent to driver (SMS/email)
4. Driver accesses load via token URL
5. Token validated on each request
Driver API calls:
- Extract token from URL parameter
- Look up load by
driverToken - Verify load exists and has assigned driver
- Validate ownership before mutations
1. Load created with trackingToken
2. Broker shares tracking URL
3. Customer accesses via token
4. Read-only data returned
Public API calls:
- Extract token from URL parameter
- Look up load by
trackingToken - Check TTL (if delivered)
- Return safe response shape
Separate from broker authentication:
- Uses
ADMIN_EMAILandADMIN_PASSWORDenvironment variables - Separate cookie:
pingpoint_admin_session - 24-hour session expiration
- Not connected to broker accounts
| Variable | Default | Description |
|---|---|---|
JWT_SECRET |
Required | Secret for signing JWT tokens |
PUBLIC_TRACKING_TTL_DAYS |
7 | Days after delivery before tracking link expires |
PUBLIC_TRACKING_RPM |
60 | Rate limit for public tracking (requests/min) |
GPS_MAX_ACCURACY_METERS |
5000 | Maximum acceptable GPS accuracy |
GPS_MAX_FUTURE_SKEW_SECONDS |
300 | Maximum timestamp in future (seconds) |
GPS_MAX_AGE_HOURS |
24 | Maximum ping age (hours) |
GPS_MAX_SPEED_MPH |
120 | Maximum speed before anti-teleport triggers |
TRACKING_PING_RPM |
120 | Rate limit for ping endpoint (requests/min) |
Future security enhancements:
- Token hashing: Store token hashes instead of plaintext for new tokens
- PostGIS/Geofencing: Server-side coordinate validation against known locations
- WAF/CDN caching: Edge caching for public tracking responses
- Redis rate limiting: Distributed rate limiting for multi-instance deployments
- Anomaly scoring: ML-based detection of suspicious ping patterns
- Token rotation: Allow brokers to regenerate tracking tokens
- Audit logging: Comprehensive audit trail to database
Outgoing webhooks include HMAC signatures:
X-PingPoint-Signature: sha256=<signature>
Recipients should verify:
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(requestBody)
.digest('hex');
if (signature !== `sha256=${expectedSignature}`) {
throw new Error('Invalid signature');
}Stripe webhooks verified using:
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);- Passwords: Not stored (passwordless auth)
- Tokens: High-entropy, cryptographically random
- API keys: Environment variables only
- Payment data: Handled by Stripe (PCI compliant)
Sensitive data excluded from logs:
- Session tokens truncated
- Full tokens never logged
- API keys masked
All inputs validated using Zod schemas:
const loadSchema = z.object({
shipperName: z.string().min(1).max(255),
rateAmount: z.string().regex(/^\d+(\.\d{2})?$/),
// ...
});Invalid inputs return 400 Bad Request.
Drizzle ORM uses parameterized queries:
// Safe - parameterized
await db.query.loads.findFirst({
where: eq(loads.id, loadId)
});
// Never do this
// db.execute(`SELECT * FROM loads WHERE id = '${loadId}'`)React auto-escapes rendered content:
// Safe - content is escaped
<div>{userInput}</div>
// Dangerous - avoid unless sanitized
<div dangerouslySetInnerHTML={{ __html: userInput }} />API allows credentials from same origin:
app.use(cors({
origin: true,
credentials: true
}));Public tracking endpoints allow any origin (read-only data).
If you discover a security vulnerability:
- Do not open a public issue
- Email security concerns to the maintainers
- Include steps to reproduce
- Allow time for fix before disclosure