EpsimoAI Backend — Authentication Guide

This document describes every authentication mechanism the backend supports, what each is for, and how to use it from a CLI or external code.

Backend base URL (production): https://backend.epsimoagents.com Frontend (production): https://chat.epsimoagents.com

Audience. Developers integrating with the EpsimoAI backend, building CLI tools, embedding assistants in third-party platforms, or wiring up SSO from another identity provider.


At a glance

# Mechanism Endpoint Token format Lifetime Best for
1 Email + password login POST /auth/login EpsimoAI JWT (HS256) 30 days Frontend login, personal CLI
2 Email + password signup POST /auth/signup + email verification n/a (then login) n/a New user registration
3 Google OAuth GET /auth/google/login/auth/google/callback EpsimoAI JWT (HS256) 30 days "Sign in with Google" flow
4 Project-scoped JWT GET /projects/{project_id} EpsimoAI JWT (HS256) 30 days Pinning a token to a single project workspace
5 Cognito → EpsimoAI exchange POST /auth/exchange EpsimoAI JWT (RS256) 1 hour Cross-app SSO from a Cognito-protected app
6 Per-assistant public token Set in assistant config Plain string secret Until rotated in config Embedding ONE assistant in Typebot, Zapier, etc.
7 Public no-auth assistant Set in assistant config None n/a Fully open assistant endpoint
8 Admin token ?admin_token=... query param Static env-var secret Until rotated Sysadmin cleanup endpoints only

There is no general-purpose, long-lived per-user "API key" feature today (revocable, scoped, no expiry). If you need that, see "Gaps" at the end.


1. Email + password login

Standard username/password authentication. Used by the regular frontend login page.

Endpoint

POST /auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "your-password"
}

Response

{
  "jwt_token": "eyJ...",
  "main_agent_id": "<assistant-id>",
  "main_thread_id": "<thread-id>"
}

What's in the JWT

{
  "user_id": "<uuid>",
  "project_id": "<uuid of user's main project>",
  "alg": "HS256",
  "iss": "<JWT_ISSUER env, e.g. LOCAL>",
  "aud": "<JWT_AUDIENCE env, e.g. epsimoai>",
  "exp": <30 days from now>
}

Using the token

Pass it in Authorization: Bearer <jwt_token> on every subsequent request:

curl https://backend.epsimoagents.com/threads \
  -H "Authorization: Bearer eyJ..."

Lifetime, revocation


2. Email + password signup

Endpoint

POST /auth/signup
Content-Type: application/json

{
  "email": "newuser@example.com",
  "password": "...",
  "first_name": "...",
  "last_name": "..."
}

Flow

  1. Account created in pending state.
  2. Verification email sent (subject contains a 6-digit code).
  3. User submits the code via POST /auth/verify-email → account activated.
  4. After activation, normal login at POST /auth/login works.

Signup does NOT return a JWT directly — the user must verify email first, then log in.


3. Google OAuth ("Sign in with Google")

Used by the frontend's Se connecter avec Google button. Browser-based flow only — not designed for CLI use.

Endpoints

Method Path Purpose
GET /auth/google/login Redirects browser to Google's consent screen
GET /auth/google/callback Receives Google's authorization code, mints a JWT, redirects to the frontend

Flow

  1. User clicks "Sign in with Google" → browser navigates to /auth/google/login.
  2. Backend redirects to Google with a redirect_uri of <BACKEND_URL>/auth/google/callback.
  3. User consents.
  4. Google redirects back to /auth/google/callback?code=....
  5. Backend exchanges the code with Google, gets the user's email/name.
  6. Backend looks up or creates an EpsimoAI user with provider="google".
  7. Backend mints the same kind of HS256 JWT as POST /auth/login.
  8. Backend redirects browser to: <FRONTEND_URL>/auth-callback?jwt_token=<...>&main_agent_id=<...>&main_thread_id=<...>
  9. Frontend extracts and stores the JWT.

Required server config

Env var Purpose
BACKEND_URL Used to build the Google redirect URI; must match what's registered in the OAuth client
FRONTEND_URL Where to redirect the browser after success (e.g. https://chat.epsimoagents.com)
GOOGLE_OAUTH_CLIENT_ID OAuth 2.0 client ID from Google Cloud Console
GOOGLE_OAUTH_SECRET_ID OAuth 2.0 client secret (store in Secrets Manager — DO NOT log)

Required Google Cloud Console config

On the OAuth 2.0 Client (in APIs & Services → Credentials):

The OAuth consent screen must be configured (app name, privacy policy, ToS) and either published or have the user as a test user.

Conflict with email-password accounts

If a user already exists with the same email but provider="email", the backend refuses Google login for that email and shows a "use password instead" page. There is no automatic account merging.


4. Project-scoped JWT

For users with access to multiple projects, you can mint a JWT pinned to a specific project. Routes that operate on threads/assistants/etc. read the project_id claim from this token and scope DB queries accordingly.

Endpoint

GET /projects/{project_id}
Authorization: Bearer <existing user JWT>

Response

{ "jwt_token": "eyJ..." }

The new token is identical to a fresh login token, except the project_id claim is fixed to the requested project.

Access control

The endpoint calls storage.get_project(user_id, project_id). If the calling user does not own the project, it returns 404 Not Found — no privilege escalation.

Listing available projects first

curl https://backend.epsimoagents.com/projects/ \
  -H "Authorization: Bearer <user_jwt>"

Example end-to-end CLI

# Step 1: Login
JWT=$(curl -s -X POST https://backend.epsimoagents.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"..."}' \
  | jq -r .jwt_token)

# Step 2: List projects, pick one
curl -s https://backend.epsimoagents.com/projects/ \
  -H "Authorization: Bearer $JWT" | jq

# Step 3: Switch to that project
PROJECT_JWT=$(curl -s "https://backend.epsimoagents.com/projects/<project-id>" \
  -H "Authorization: Bearer $JWT" \
  | jq -r .jwt_token)

# Step 4: Use the project-scoped JWT
curl https://backend.epsimoagents.com/threads \
  -H "Authorization: Bearer $PROJECT_JWT"

Caveats


5. Cognito → EpsimoAI exchange

For SSO from an external app that authenticates users against an AWS Cognito user pool. The external app obtains a Cognito ID token, exchanges it for an EpsimoAI JWT, and uses that to call the backend on behalf of the user.

Endpoint

POST /auth/exchange
Authorization: Bearer <cognito-id-token>

Response

{ "token": "eyJ..." }

What the backend does

  1. Reads the Cognito ID token from the Authorization: Bearer ... header.
  2. Verifies it cryptographically against Cognito's JWKS (https://cognito-idp.<region>.amazonaws.com/<pool-id>/.well-known/jwks.json):
  3. signature with the right kid
  4. iss matches the Cognito issuer URL
  5. aud matches COGNITO_APP_CLIENT_ID
  6. not expired
  7. Extracts email and sub from the verified payload.
  8. Looks up or creates a user with provider="cognito" in the EpsimoAI DB.
  9. Ensures the user has a main project (creates it if missing).
  10. Mints an EpsimoAI JWT signed with RS256 (different algorithm from login/Google), 1-hour expiry: json { "user_id": "<uuid>", "project_id": "<main project uuid>", "email": "<email>", "iss": "<EPSIMOAI_JWT_ISSUER>", "aud": "<EPSIMOAI_JWT_AUDIENCE>", "exp": <1 hour from now>, "iat": <now> }

Required server config

Env var Purpose
COGNITO_USER_POOL_ID e.g. us-east-1_AbCdEfGhI
COGNITO_APP_CLIENT_ID The Cognito app client whose tokens you want to accept
AWS_REGION Region of the user pool (defaults to us-east-1)
EPSIMOAI_JWT_ISSUER Issuer claim on the minted EpsimoAI token
EPSIMOAI_JWT_AUDIENCE Audience claim (defaults to issuer if unset)
EPSIMOAI_JWT_PRIVATE_KEY RSA private key, PEM format, for RS256 signing

The corresponding public key must be configured in JWT_DECODE_KEY_B64 (or the auth handler must accept multiple algorithms/keys) so the rest of the API can verify the RS256 tokens this endpoint mints. The default deployment uses HS256 for login tokens, so accepting both algorithms requires either:

Status. The endpoint exists in code but the production deployment does not currently set the Cognito or RS256 env vars, and the auth handler is configured for HS256 only. Calling the endpoint today returns 500 Cognito configuration error.

Use cases


6. Per-assistant public token (OpenAI-compatible)

For embedding a single AI assistant in an external platform that expects an OpenAI-compatible API (Typebot, Zapier, no-code tools, etc.). Per-assistant, not per-user. Acts like a per-assistant API key.

Configuring an assistant for public-token access

Edit the assistant's config (via the frontend or PATCH /assistants/<id>) to set:

Config key Value
type==agent/access_mode public_token
type==agent/public_token a secret string you generate (e.g. pubkey_<random>)

This token is what callers will use as the API key.

Calling — OpenAI-compatible style

This is the easiest path because Typebot, OpenAI client SDKs, etc. all support it natively:

curl https://backend.epsimoagents.com/public/openai/v1/chat/completions \
  -H "Authorization: Bearer <your-public-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role":"user","content":"Hello"}],
    "stream": false
  }'

The token alone identifies which assistant to use — there's no separate assistant_id field. The backend looks up the assistant whose type==agent/public_token matches.

In Typebot: - Base URL: https://backend.epsimoagents.com/public/openai/v1 - API Key: the public token you set in the assistant config

Calling — native streaming endpoint

For finer control over input format, use the EpsimoAI-specific streaming endpoint:

curl https://backend.epsimoagents.com/public/runs/stream \
  -H "X-Public-Bot-Token: <your-public-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "assistant_id": "<assistant-id>",
    "input": { ... }
  }'

The token can be passed in the X-Public-Bot-Token header or in the JSON body's token field.

Properties


7. Public no-auth assistant

For assistants that should be fully open with no token at all (e.g. a public demo bot).

Configuration

Config key Value
type==agent/access_mode public_no_auth

Calling

Same endpoints as #6 above, but no Authorization header / no X-Public-Bot-Token is required.

curl https://backend.epsimoagents.com/public/runs/stream \
  -H "Content-Type: application/json" \
  -d '{ "assistant_id": "<id>", "input": { ... } }'

When to use

Caveats


8. Admin token

For low-level sysadmin endpoints — not part of the regular API. Used by maintenance scripts (setup_admin.sh, daily_stats_curl.py).

How it works

Set ADMIN_TOKEN as an env var on the backend, then pass it as a query parameter:

curl "https://backend.epsimoagents.com/admin/system-stats?admin_token=<ADMIN_TOKEN>"
curl "https://backend.epsimoagents.com/admin/inactive-users?admin_token=<ADMIN_TOKEN>&min_days_old=90"

Surface

/admin/... endpoints only — currently: - GET /admin/system-stats — usage statistics - GET /admin/inactive-users — list inactive users - POST /admin/cleanup-inactive-users — bulk-delete inactive users (supports dry_run=true) - and similar cleanup operations

Not for general API access

The admin token does not authorize any of the regular /threads, /assistants, /runs, etc. endpoints.


How requests are authorized

Most endpoints (everything that uses AuthedUser as a FastAPI dependency) require:

Authorization: Bearer <epsimoai-jwt>

The handler in app/auth/handlers.py: 1. Extracts the bearer token. 2. Verifies signature, iss, aud, exp against JWT_DECODE_KEY_B64 (or JWT_SECRET_KEY) using JWT_ALGORITHM (HS256 by default). 3. Loads the user record from the DB by user_id. Creates one if missing (legacy behaviour to support imported tokens). 4. Adds project_id from the token to the user object. 5. Passes the resulting User object to the route handler.

If the token is missing, malformed, expired, or signed with the wrong key, the handler returns 401 Unauthorized.

Server-side env vars driving JWT validation

Env var Purpose
JWT_ALGORITHM (or JWT_ALG) Signing algorithm. Default HS256.
JWT_ISSUER (or JWT_ISS) Required issuer claim.
JWT_AUDIENCE (or JWT_AUD) Required audience claim.
JWT_SECRET_KEY HS256 signing key (also used as decode key for symmetric algorithms).
JWT_DECODE_KEY_B64 Base64-encoded decode key (asymmetric or symmetric). Takes precedence over JWT_SECRET_KEY.

The same key is used for signing tokens minted by /auth/login, /auth/google/callback, and /projects/{id}.


Public vs authenticated endpoints

Path prefix Auth required? Notes
/ok No Health check.
/auth/login, /auth/signup, /auth/verify-email, /auth/google/*, /auth/exchange No Auth flows themselves don't need an existing token.
/auth/thread-info, /auth/me, etc. Yes User-scoped.
/projects/* Yes User JWT required.
/assistants/*, /threads/*, /runs/*, /user/*, /tools/* Yes User JWT required.
/public/runs/stream, /public/openai/v1/chat/completions Per-assistant Authorization governed by the assistant's access_mode config.
/admin/* Admin token (query param) NOT JWT.
/checkout/*, /price/* Yes (most) Stripe-related.

CORS

The backend allows credentialed requests from a fixed list of origins. Configured via:

If you call the API from a browser-based app on a domain not in this list, the request will be blocked. Add the origin to CORS_ORIGINS in the ECS task definition (or to the default list in server.py if you want it baked into the image).


CLI quick-start recipes

Recipe A — Personal CLI access (simplest)

# 1) One-time login (or do this when token expires every 30 days)
JWT=$(curl -s -X POST https://backend.epsimoagents.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"..."}' \
  | jq -r .jwt_token)

# 2) Save it
echo "$JWT" > ~/.epsimo-jwt

# 3) Use it in scripts
JWT=$(cat ~/.epsimo-jwt)
curl https://backend.epsimoagents.com/threads -H "Authorization: Bearer $JWT"

Recipe B — CLI scoped to a single project

JWT=$(cat ~/.epsimo-jwt)

# List your projects
curl -s https://backend.epsimoagents.com/projects/ \
  -H "Authorization: Bearer $JWT" | jq

# Get a project-scoped token (replace <project-id>)
PROJECT_JWT=$(curl -s "https://backend.epsimoagents.com/projects/<project-id>" \
  -H "Authorization: Bearer $JWT" | jq -r .jwt_token)

# Use the project-scoped token from now on
curl https://backend.epsimoagents.com/threads \
  -H "Authorization: Bearer $PROJECT_JWT"

Recipe C — Embedding an assistant in an external platform

  1. In the frontend, pick or create an assistant.
  2. Edit its config: set access_mode = public_token and public_token = <generate-a-secret>.
  3. In the external platform, configure it like an OpenAI integration:
  4. Base URL: https://backend.epsimoagents.com/public/openai/v1
  5. API Key: the secret you set
  6. The platform will call POST /chat/completions and your assistant responds.

Recipe D — Cross-app SSO via Cognito

(Requires server-side config — see Section 5.)

# Your other app authenticates the user against Cognito and obtains an ID token.
COGNITO_TOKEN="eyJ..."

# Exchange for an EpsimoAI token
EPSIMO_JWT=$(curl -s -X POST https://backend.epsimoagents.com/auth/exchange \
  -H "Authorization: Bearer $COGNITO_TOKEN" \
  | jq -r .token)

# Use it for 1 hour
curl https://backend.epsimoagents.com/threads \
  -H "Authorization: Bearer $EPSIMO_JWT"

Token lifetimes summary

Token type Algorithm Lifetime Renewable?
/auth/login JWT HS256 30 days Re-login
Google callback JWT HS256 30 days Re-login
/projects/{id} JWT HS256 30 days Re-call the endpoint
/auth/exchange JWT RS256 1 hour Re-exchange a fresh Cognito token
Per-assistant public token n/a (raw secret) Until rotated in config Edit the assistant
Admin token n/a (raw secret) Until env var is rotated Update env var, redeploy

Security considerations

Secrets storage

JWT key rotation

Rotating JWT_SECRET_KEY / JWT_DECODE_KEY_B64 invalidates every outstanding token (login, Google, project-scoped). All users will be forced to log in again. This is currently the only revocation mechanism.

Token leakage

A leaked EpsimoAI JWT is valid for the rest of its lifetime (up to 30 days for login tokens, 1 hour for Cognito-exchange tokens) and cannot be selectively revoked. Mitigation: - Don't store JWTs in browser localStorage for high-value applications. Use httpOnly cookies if possible. - Don't print JWTs to logs. - Don't put JWTs in URLs (the Google callback intentionally puts the JWT in a URL because that's the only way to get it from the OAuth callback to the SPA — the SPA should remove it from window.location immediately).

CORS

allow_credentials=True plus a wildcard origin would be a CSRF risk. The backend correctly uses an explicit allowlist.


Gaps / future work

The following are NOT supported today and would require new code:

  1. Long-lived per-user API keys. A separate api_keys DB table, endpoints to create/list/revoke, and middleware to accept Authorization: Bearer epsimo_<...> alongside JWTs. Useful for service accounts, CI integrations, and personal CLIs that don't want to re-login every 30 days.

  2. Refresh tokens. Currently the only "renewal" is to re-login or re-exchange. A refresh-token flow would let clients swap a short-lived access token for a new one without re-auth.

  3. JWT revocation. Stateful revocation (e.g. a denylist or a token-version column on the user row) so individual leaked tokens can be killed without rotating the entire signing key.

  4. Cognito environment wiring. The /auth/exchange endpoint is implemented but the production env vars for it are not set, and the auth handler doesn't currently accept the RS256 tokens it would mint.

  5. Per-token quotas / rate limits for public assistants.

  6. Multi-Factor Authentication.

  7. Session listing / revocation UI. "Show me where I'm logged in, log out everywhere."

If you need any of these, they're real feature builds — happy to scope them.


File map (where things live in the code)

File Responsibility
app/api/authentication.py /auth/login, /auth/signup, /auth/verify-email, /auth/google/*, /auth/exchange, /auth/thread-info
app/api/projects.py /projects/* including the project-scoped JWT endpoint
app/api/public_runs.py /public/runs/* and /public/openai/v1/* (per-assistant public token logic)
app/api/openai_v1.py OpenAI-compatible wrapper around public_runs
app/api/admin_cleanup.py /admin/* endpoints (admin-token-protected)
app/auth/handlers.py JWT validation middleware (AuthedUser dependency)
app/auth/settings.py Loads JWT config from env

Last updated: 2026-05-20. Verified against backend revision deployed at https://backend.epsimoagents.com (ECS task definition default-epsimoai-backend-604b:16).