This document explains how LinkedIn Post Bot handles security, authentication, and sensitive data.
- Secrets Handling
- OAuth Flow
- API Key Storage
- Rate Limiting
- Responsible Usage
- Reporting Vulnerabilities
| Data | Storage | Encryption |
|---|---|---|
| LinkedIn OAuth tokens | SQLite (backend_tokens.db) |
At rest (DB-level) |
| User API keys (Groq, Unsplash) | SQLite (user_settings.db) |
At rest (DB-level) |
| LinkedIn Client ID/Secret | User settings DB | At rest (DB-level) |
- Never log secrets — Access tokens, API keys, and client secrets are never printed to console or logs
- Never expose secrets via API — Settings endpoint returns masked versions (e.g.,
gsk_xxxx...xxxx) - Never transmit secrets unnecessarily — Tokens are only sent to their respective API providers
- Never store passwords — We use OAuth; users authenticate directly with LinkedIn
All sensitive configuration is loaded from environment variables:
# Backend (.env)
LINKEDIN_CLIENT_ID=...
LINKEDIN_CLIENT_SECRET=...
GROQ_API_KEY=...
UNSPLASH_ACCESS_KEY=...
# Frontend (web/.env.local)
CLERK_SECRET_KEY=...Important: Never commit
.envfiles. The.gitignoreexcludes all environment files and database files.
The application uses LinkedIn's official OAuth 2.0 authorization code flow:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │ │ App │ │ LinkedIn │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. Click "Connect" │ │
│───────────────────>│ │
│ │ │
│ │ 2. Redirect to │
│ │ LinkedIn OAuth │
│<───────────────────│────────────────────>│
│ │ │
│ 3. User grants │ │
│ permission │ │
│───────────────────────────────────────-->│
│ │ │
│ │ 4. LinkedIn sends │
│ │ auth code │
│<─────────────────────────────────────────│
│ │ │
│ 5. Callback to app │ │
│───────────────────>│ │
│ │ 6. Exchange code │
│ │ for token │
│ │<───────────────────>│
│ │ │
│ │ 7. Store token │
│ │ securely │
│ │ │
| Scope | Purpose | Required |
|---|---|---|
openid |
OpenID Connect user info | ✅ Yes |
profile |
Basic profile information | ✅ Yes |
email |
Email address | Optional |
w_member_social |
Create posts on behalf of user | ✅ Yes |
- Acquisition: Token obtained via OAuth callback
- Storage: Encrypted in SQLite with expiration timestamp
- Refresh: Automatic refresh before expiry (60-second buffer)
- Revocation: Users can disconnect via LinkedIn settings
Each user stores their own:
- LinkedIn Client ID & Secret (from their own LinkedIn Developer App)
- Groq API Key
- Unsplash Access Key
This ensures:
- Multi-tenant isolation
- No shared API quotas
- Users control their own credentials
┌───────────────────────────────────────────────────────────────────┐
│ MULTI-TENANT ISOLATION GUARANTEES │
├───────────────────────────────────────────────────────────────────┤
│ │
│ ✅ User A can ONLY access: ❌ User A can NEVER access: │
│ - Their own OAuth tokens - User B's tokens │
│ - Their own GitHub activity - User B's activity │
│ - Their own LinkedIn posts - User B's posts │
│ - Their own settings - User B's settings │
│ │
└───────────────────────────────────────────────────────────────────┘
Database Level:
- Every query includes
WHERE user_id = ? - User ID is the Clerk authentication ID
- No admin endpoints return all users' data
API Level:
- User ID extracted from JWT claims
- All endpoints scoped to authenticated user
- Cross-user access returns 404/403
Service Level:
- GitHub activity: Scoped by username/token
- AI generation: Receives only user's activity
- LinkedIn posting: Uses only user's OAuth token
Before any operation that requires an OAuth token:
# 1. Verify user is authenticated (Clerk JWT)
# 2. Retrieve token by user_id (tenant isolation)
# 3. Check token exists
# 4. Check token not expired
# 5. Proceed or return errorGraceful Failure Handling:
- Missing token → "Please connect your account"
- Expired token → "Please reconnect your account"
- Invalid token → "Authentication failed, please reconnect"
- Rate limited → "Too many requests, please wait"
- No token enumeration — Tokens keyed by user_id, not sequential IDs
- No URN guessing — LinkedIn URN not exposed externally
- Parameterized queries — SQL injection prevented
- JWT validation — User identity verified on every request
- All API calls use HTTPS
- JWT tokens verified on each request (Clerk authentication)
- CORS restricts origins to authorized frontends
- SQLite databases stored locally
- Database files excluded from Git
- Production: Use encrypted volumes or managed databases
The settings endpoint returns masked API keys:
{
"groq_api_key": "gsk_xxxx...xxxx",
"linkedin_client_secret": "••••••••"
}Full keys are never sent back to the client after initial save.
The application implements rate limiting to prevent abuse:
| Endpoint Category | Limit | Window |
|---|---|---|
Post Generation (/generate-preview) |
10 requests | 1 hour |
Publishing (/api/publish/full) |
5 requests | 1 hour |
| General API | 100 requests | 1 hour |
Rate limits are enforced per-user (by Clerk user ID).
See services/middleware.py for the RateLimiter class:
from services.middleware import post_generation_limiter, rate_limit
@rate_limit(post_generation_limiter)
async def generate_preview(user_id: str, ...):
...✅ Generating LinkedIn posts from your own GitHub activity
✅ Publishing content to your own LinkedIn profile
✅ Saving time on content creation
❌ Automated mass posting or spam
❌ Posting on behalf of others without consent
❌ Scraping LinkedIn data
❌ Bypassing LinkedIn rate limits
❌ Any activity that violates LinkedIn's Terms of Service
This application:
- Uses only LinkedIn's official APIs
- Requires explicit user authorization (OAuth)
- Does not automate engagement (likes, comments)
- Does not use browser automation or scraping
- Respects LinkedIn's API rate limits
Warning: Excessive posting or suspicious activity may result in LinkedIn restricting your account. Use responsibly.
If you discover a security vulnerability, please:
- Do NOT open a public issue
- Email the maintainer directly with details
- Include steps to reproduce if possible
- Allow reasonable time for a fix before disclosure
We take security seriously and will respond promptly.
When contributing code, ensure:
- No secrets logged to console (use masked versions if needed)
- No secrets returned in API responses (mask or omit)
- Input validation on all user-provided data
- Rate limiting on resource-intensive endpoints
- CORS configuration restricts to known origins
- SQL queries use parameterization (no string concatenation)
- OAuth tokens stored with expiration handling
Last updated: December 2024