API Authentication¶
Salient's REST API uses OAuth 2.0 for authentication and JWT tokens for session management, with role-based access control (RBAC) on all mutation endpoints.
OAuth Providers¶
Two OAuth providers are supported:
| Provider | Callback URL | Setup |
|---|---|---|
| GitHub | /auth/github/callback | Create OAuth App in GitHub Developer Settings |
/auth/google/callback | Create OAuth Client in Google Cloud Console |
Users authenticate via the login page, which redirects to the chosen provider. After authorization, the backend issues a JWT token.
JWT Tokens¶
| Property | Value |
|---|---|
| Expiry | 7 days |
| Storage | httpOnly cookie (primary) + localStorage (fallback) |
| Algorithm | HS256 |
| Claims | user_id, email, role, org_id, exp |
SECRET_KEY
Set SECRET_KEY in .env for production. Without it, the key is randomly generated on each restart, invalidating all existing sessions.
Using the API¶
Cookie Authentication (Browser)¶
The frontend uses httpOnly cookies automatically. No manual token handling required.
Bearer Token (API/MCP)¶
For programmatic access and MCP tools, include the JWT as a Bearer token:
The MCP server reads the token from the SALIENT_TOKEN environment variable.
Role-Based Access Control¶
All mutation routes enforce RBAC via the require_role() dependency:
| Role | Permissions |
|---|---|
| Owner | Full access — all CRUD operations, settings, member management, data deletion |
| Admin | Exercise management, connector configuration, scoring, playbook generation |
| Member | Read access, participate in exercises, view reports |
# Backend enforcement example
@router.post("/api/connectors/okta/sync/")
async def okta_sync(user=Depends(require_role("admin"))):
...
Email Allowlist¶
Access can be restricted to specific email addresses via the ALLOWED_EMAILS environment variable:
Users not on the allowlist cannot create accounts, even with valid OAuth credentials.
IP Restriction¶
The Caddy reverse proxy enforces IP-based access control via ALLOWED_IPS in .env. Requests from non-allowed IPs are rejected before reaching the backend.
For dynamic IPs, use the self-service update endpoint: