Skip to content

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:

ProviderMethodBest For
LocalBcrypt password hashing + HS256 JWTDevelopment, small teams, self-hosted
OIDCRS256 JWT validation via JWKSProduction, 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):

MethodEndpointDescription
POST/api/v1/auth/registerCreate a new user account (local provider only)
POST/api/v1/auth/loginAuthenticate and receive a JWT
GET/api/v1/auth/meGet 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:

FieldValue
Emailadmin@localhost
Passwordadmin
Roleadmin

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/
VariableDescription
JWKS_URLURL to the provider's JWKS endpoint. Agentcy fetches and caches signing keys from this URL
JWT_AUDIENCEExpected aud claim in incoming JWTs. Must match what your OIDC provider is configured to issue
JWT_ISSUERExpected iss claim in incoming JWTs. Must match the provider's issuer URL

How It Works

  1. User authenticates with the OIDC provider (Auth0, Supabase, Keycloak, etc.) through the frontend login page
  2. The provider issues a JWT signed with RS256
  3. The frontend stores the JWT and sends it with API requests
  4. 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 by kid header d. Verifies the RS256 signature e. Validates aud and iss claims f. Extracts user identity from the token claims
  5. 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/v1

Supabase 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/agentcy

Middleware 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 Unauthorized

This dual-verification approach means you can:

  • Run with AUTH_PROVIDER=local and only use local credentials
  • Run with AUTH_PROVIDER=oidc and 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):

ColumnTypeDescription
idUUIDPrimary key
emailVARCHARUnique email address
nameVARCHARDisplay name
password_hashVARCHAR (nullable)Bcrypt hash (null for OIDC-only users)
auth_providerVARCHARlocal or oidc
email_verifiedBOOLEANWhether the email is verified
last_login_atTIMESTAMPLast successful login timestamp
roleVARCHARUser role (e.g., admin, member)
tenant_idUUIDOrganization/tenant reference

Frontend Auth Flow

The frontend implements authentication through:

  • AuthProvider context (components/auth/auth-provider.tsx) — manages auth state, token storage, and login/logout functions
  • AuthGuard component — 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 localStorage under a standard key
  • API client — the centralized request<T>() function in lib/api/client.ts automatically attaches the Authorization: Bearer header to every request

Token Refresh

Tokens have a configurable expiry (JWT_EXPIRY_SECS, default 24 hours). When a token expires:

  1. The API returns 401 Unauthorized
  2. The frontend's API client detects the 401
  3. The user is redirected to the login page
  4. For OIDC providers, the user may be silently re-authenticated if their provider session is still active

Security Best Practices

  1. Never use DEV_MODE=true in production — it completely bypasses authentication
  2. Use a strong JWT_SECRET — at least 32 random bytes (openssl rand -hex 32)
  3. Enable OIDC for team deployments — centralizes user management and enables SSO
  4. Change default admin credentials immediately — or delete the default account
  5. Set appropriate token expiry — shorter for high-security environments (e.g., 3600 for 1 hour)
  6. Use HTTPS in production — JWTs in transit must be encrypted
  7. Configure zero-trust policies — layer policy enforcement on top of authentication for fine-grained access control

Next Steps

Built by AgentcyLabs. For in-house deployment or Agentcy Cloud (PaaS) access, visit agentcylabs.com.