Appearance
Authentication
Agentcy uses a provider-agnostic authentication system that supports both local credentials and external OIDC providers. The system is configured through environment variables and requires no code changes to switch between providers.
Overview
The auth system has two providers:
| Provider | Method | Best For |
|---|---|---|
| Local | Bcrypt password hashing + HS256 JWT | Development, small teams, self-hosted |
| OIDC | RS256 JWT validation via JWKS | Production, enterprise, SSO |
The authentication middleware tries local verification first (HS256), then falls back to OIDC verification (RS256 via JWKS). This means both providers can coexist — you can have a local admin account alongside OIDC-authenticated team members.
Auth Routes
All auth endpoints are outside the JWT middleware (they do not require an existing token):
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/auth/register | Create a new user account (local provider only) |
POST | /api/v1/auth/login | Authenticate and receive a JWT |
GET | /api/v1/auth/me | Get the current user (requires valid JWT) |
Local Authentication
Local auth uses bcrypt for password hashing and HS256 for JWT signing. It is the default provider and requires no external services.
Configuration
bash
AUTH_PROVIDER=local
JWT_SECRET=change-me-in-production-use-a-long-random-string
JWT_EXPIRY_SECS=86400 # 24 hours (optional, this is the default)JWT_SECRET Security
The JWT_SECRET is used to sign and verify all JWTs. In production:
- Use a long, random string (at least 32 characters). Generate one with:
openssl rand -hex 32 - Never commit it to version control
- Rotate it periodically (all existing tokens are invalidated on rotation)
Default Admin Account
On first startup, database migration 005_auth_providers.sql seeds a default admin account:
| Field | Value |
|---|---|
admin@localhost | |
| Password | admin |
| Role | admin |
Change Immediately
The default admin credentials are widely known. Change the password after first login, or delete this account and create a new admin through the API:
bash
# Register a new admin
curl -X POST http://localhost:18080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "you@example.com",
"password": "a-strong-password",
"name": "Your Name"
}'Login Flow
bash
# 1. Login to get a JWT
curl -X POST http://localhost:18080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@localhost",
"password": "admin"
}'Response:
json
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "uuid",
"email": "admin@localhost",
"name": "Admin",
"role": "admin"
}
}bash
# 2. Use the token for authenticated requests
curl http://localhost:18080/api/v1/sources \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."The frontend stores the JWT in localStorage and attaches it to every API request via the centralized request<T>() function in lib/api/client.ts.
OIDC Authentication
OIDC auth validates JWTs signed by an external identity provider using RS256 and JWKS (JSON Web Key Sets). This integrates with any standards-compliant OIDC provider.
Configuration
bash
AUTH_PROVIDER=oidc
JWKS_URL=https://your-provider/.well-known/jwks.json
JWT_AUDIENCE=https://api.agentcy.dev
JWT_ISSUER=https://your-provider/| Variable | Description |
|---|---|
JWKS_URL | URL to the provider's JWKS endpoint. Agentcy fetches and caches signing keys from this URL |
JWT_AUDIENCE | Expected aud claim in incoming JWTs. Must match what your OIDC provider is configured to issue |
JWT_ISSUER | Expected iss claim in incoming JWTs. Must match the provider's issuer URL |
How It Works
- User authenticates with the OIDC provider (Auth0, Supabase, Keycloak, etc.) through the frontend login page
- The provider issues a JWT signed with RS256
- The frontend stores the JWT and sends it with API requests
- Agentcy's auth middleware: a. Tries to verify as an HS256 token (local provider) — fails b. Fetches the JWKS from
JWKS_URL(cached) c. Finds the matching key bykidheader d. Verifies the RS256 signature e. Validatesaudandissclaims f. Extracts user identity from the token claims - The request proceeds with the authenticated user context
Auth0 Setup
1. Create an API in Auth0:
- Go to Applications > APIs in the Auth0 dashboard
- Click Create API
- Set the Identifier (this becomes your
JWT_AUDIENCE):https://api.agentcy.dev - Select RS256 as the signing algorithm
2. Create an Application:
- Go to Applications > Applications
- Click Create Application > Single Page Application
- In Settings, add your frontend URL to Allowed Callback URLs:
http://localhost:3000/login/callback - Add to Allowed Logout URLs:
http://localhost:3000 - Add to Allowed Web Origins:
http://localhost:3000
3. Configure Agentcy:
bash
AUTH_PROVIDER=oidc
JWKS_URL=https://your-tenant.auth0.com/.well-known/jwks.json
JWT_AUDIENCE=https://api.agentcy.dev
JWT_ISSUER=https://your-tenant.auth0.com/4. Configure the frontend:
The frontend login page detects AUTH_PROVIDER=oidc and redirects to the OIDC provider's authorization endpoint instead of showing the email/password form.
Supabase Auth Setup
1. Get your project settings:
In the Supabase dashboard, go to Settings > API to find your project URL and JWT settings.
2. Configure Agentcy:
bash
AUTH_PROVIDER=oidc
JWKS_URL=https://your-project.supabase.co/auth/v1/.well-known/jwks.json
JWT_AUDIENCE=authenticated
JWT_ISSUER=https://your-project.supabase.co/auth/v1Supabase JWT Audience
Supabase uses authenticated as the default audience for authenticated users. Check your Supabase JWT settings if this differs.
Keycloak Setup
1. Create a realm and client:
- Create a new realm (e.g.,
agentcy) - Create a client with Access Type: public and valid redirect URIs
2. Configure Agentcy:
bash
AUTH_PROVIDER=oidc
JWKS_URL=https://keycloak.example.com/realms/agentcy/protocol/openid-connect/certs
JWT_AUDIENCE=agentcy-api
JWT_ISSUER=https://keycloak.example.com/realms/agentcyMiddleware Behavior
The JWT authentication middleware (agentcy-auth) runs on all API routes except the auth endpoints (/auth/login, /auth/register, /auth/me) and the health check (/health).
Verification Order
Incoming request with Authorization: Bearer <token>
│
├─ 1. Try HS256 verification (local)
│ ├─ Success → extract user, continue
│ └─ Failure → try next
│
├─ 2. Try RS256 verification (OIDC)
│ ├─ Fetch JWKS from JWKS_URL (cached)
│ ├─ Match kid from token header
│ ├─ Verify signature + claims
│ ├─ Success → extract/create user, continue
│ └─ Failure → 401 Unauthorized
│
└─ No token → 401 UnauthorizedThis dual-verification approach means you can:
- Run with
AUTH_PROVIDER=localand only use local credentials - Run with
AUTH_PROVIDER=oidcand only use external providers - Run with both configured and accept tokens from either source
User Creation on First OIDC Login
When a user authenticates via OIDC for the first time, Agentcy automatically creates a user record in PostgreSQL using the claims from the JWT (email, name, etc.). The auth_provider column is set to oidc and email_verified reflects the provider's verification status.
Dev Mode
When DEV_MODE=true, the JWT middleware is bypassed entirely. All requests are treated as coming from a default tenant with admin permissions. This is useful for development but must never be enabled in production.
Database Schema
The auth system uses the users table with these relevant columns (added by migration 005_auth_providers.sql):
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
email | VARCHAR | Unique email address |
name | VARCHAR | Display name |
password_hash | VARCHAR (nullable) | Bcrypt hash (null for OIDC-only users) |
auth_provider | VARCHAR | local or oidc |
email_verified | BOOLEAN | Whether the email is verified |
last_login_at | TIMESTAMP | Last successful login timestamp |
role | VARCHAR | User role (e.g., admin, member) |
tenant_id | UUID | Organization/tenant reference |
Frontend Auth Flow
The frontend implements authentication through:
AuthProvidercontext (components/auth/auth-provider.tsx) — manages auth state, token storage, and login/logout functionsAuthGuardcomponent — wraps the dashboard layout, redirecting unauthenticated users to/login- Login page (
/login) — renders either a local login form or an OIDC redirect based on configuration - Token storage — JWTs are stored in
localStorageunder a standard key - API client — the centralized
request<T>()function inlib/api/client.tsautomatically attaches theAuthorization: Bearerheader to every request
Token Refresh
Tokens have a configurable expiry (JWT_EXPIRY_SECS, default 24 hours). When a token expires:
- The API returns
401 Unauthorized - The frontend's API client detects the 401
- The user is redirected to the login page
- For OIDC providers, the user may be silently re-authenticated if their provider session is still active
Security Best Practices
- Never use
DEV_MODE=truein production — it completely bypasses authentication - Use a strong
JWT_SECRET— at least 32 random bytes (openssl rand -hex 32) - Enable OIDC for team deployments — centralizes user management and enables SSO
- Change default admin credentials immediately — or delete the default account
- Set appropriate token expiry — shorter for high-security environments (e.g., 3600 for 1 hour)
- Use HTTPS in production — JWTs in transit must be encrypted
- Configure zero-trust policies — layer policy enforcement on top of authentication for fine-grained access control
Next Steps
- Configuration — all environment variables
- Security & Policies — zero-trust policy enforcement
- Getting Started — initial setup