This reference documents the technical specifications of Char’s security controls. For conceptual understanding of why these controls exist, see the Security Architecture explanation.
Token Validation
Required Claims
| Claim | Purpose | Validation |
|---|
iss | Issuer | Must match configured IDP issuer exactly |
sub | Subject | Must be present and non-empty |
aud | Audience | Must include configured client ID |
exp | Expiration | Must be in the future (60s clock skew tolerance) |
Optional Claims
| Claim | Purpose | When Used |
|---|
tid | Tenant ID (Azure AD) | Multi-tenant Azure configurations |
hd | Hosted domain (Google) | Google Workspace configurations |
org_id | Organization ID | Okta with organizations enabled |
nbf | Not before | Validated if present |
email | User email | Extracted for user identification |
Supported Algorithms
| Algorithm | Status |
|---|
| RS256, RS384, RS512 | Supported |
| ES256, ES384, ES512 | Supported |
| HS256, HS384, HS512 | Blocked (symmetric) |
none | Blocked |
Symmetric algorithms are blocked to prevent key confusion attacks. Only asymmetric (public key) algorithms are permitted.
JWKS Caching
| Parameter | Value |
|---|
| Cache TTL | 1 hour |
| Refresh on 401 | Immediate |
| Storage | In-memory |
Identity Provider Support
Supported IDPs
| Provider | IDP Type | Issuer Pattern |
|---|
| Okta | okta | https://{domain} |
| Azure AD | azure | https://login.microsoftonline.com/{tenant}/v2.0 |
| Auth0 | auth0 | https://{domain}/ |
| Google | google | https://accounts.google.com |
| AWS Cognito | custom_oidc | https://cognito-idp.{region}.amazonaws.com/{pool} |
| Firebase | custom_oidc | https://securetoken.google.com/{project} |
| Clerk | custom_oidc | https://{instance}.clerk.accounts.dev |
| Keycloak | custom_oidc | https://{host}/realms/{realm} |
| Custom OIDC | custom_oidc | Configurable |
OIDC Discovery
For each IDP, Char fetches the OpenID Connect discovery document at:
{issuer}/.well-known/openid-configuration
Required discovery fields:
issuer - Must match token iss claim
jwks_uri - Endpoint for public keys
Origin Validation
Allowed Domains
| Rule | Behavior |
|---|
| Exact match only | No wildcard support |
| Protocol required | https://app.example.com |
| Localhost always allowed | Development convenience |
| Empty list | All origins rejected |
Validation Order
- Extract
Origin header from request
- Look up organization by matching
allowed_domains
- If found, validate JWT against that organization’s IDP
- If not found, reject request
Localhost Fallback
When origin is localhost:
- Extract
iss claim from token (unverified)
- Match against organization’s
idp_issuer field
- If matched, use that organization’s configuration
Localhost fallback is for development only. Production deployments should configure explicit allowed domains.
Durable Object Isolation
Identity Scoping
| Durable Object | Keyed By | Contains |
|---|
| ThreadManager | {external_user_id} | Thread metadata, tool connections, active sessions |
| ThreadAgent | {thread_id} | Messages, tool selection, conversation state |
State Initialization
Each Durable Object is initialized with:
{
organizationId: string // Required
endUserId: string // Internal ID (from end_users table)
externalId: string // External user ID (sub claim)
}
Access Guards
Every operation calls requireMetadata() which:
- Verifies organization context is set
- Verifies user context is set
- Throws if either is missing
Cross-organization access is rejected in createThread() with an explicit check.
| Source | Description | Scope |
|---|
| Client tools | Registered via WebSocket from browser tabs | Per-connection |
| Skills | SKILL.md files stored in D1 | Per-organization |
| UI tools | Built-in tools (done, form) | Global |
Tools are stored per-connection in the ThreadManager:
connections: Map<connectionId, {
ws: WebSocket
tools: Map<toolName, ToolSchema>
tabId: string
origin: string
}>
When a connection closes, its tools are removed.
- Agent requests tool call with
tool_name and arguments
- ThreadManager finds connection that registered this tool
- Request is routed to that specific browser tab via WebSocket
- Result is returned through the same connection
Database Schema
Organization Scoping
All major tables include organization_id:
| Table | Organization Column | Purpose |
|---|
organizations | id (PK) | Organization identity |
organization_members | organization_id (FK) | Membership |
end_users | organization_id (FK) | Per-org end users |
threads | organization_id (FK) | Conversation threads |
organization_skills | organization_id (FK) | SKILL.md storage |
idp_configs | organization_id (FK) | IDP configuration |
Row-Level Filtering
All queries filter by organization_id. Example pattern:
SELECT * FROM threads
WHERE organization_id = ?
AND end_user_id = ?
ORDER BY updated_at DESC
Error Codes
Authentication Errors
| Code | HTTP Status | Meaning |
|---|
UNAUTHORIZED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Valid token, insufficient permissions |
MCP Error Mapping
| oRPC Error | MCP Error Code |
|---|
UNAUTHORIZED | InvalidRequest |
FORBIDDEN | InvalidRequest |
NOT_FOUND | InvalidRequest |
BAD_REQUEST | InvalidParams |
INTERNAL_SERVER_ERROR | InternalError |
Enterprise Features
The following security controls are available on the Enterprise plan:
| Feature | Description |
|---|
| Rate limiting | Per-user and per-endpoint throttling with configurable thresholds |
| Audit logging | Centralized logging of tool invocations for compliance |
| Content guardrails | PII detection, prompt injection filtering |
| Approval workflows | Human-in-the-loop for sensitive operations |
| Kill switches | Instant disable of tools or connectors |
| Tool risk classification | L0-L3 classification with configurable approval levels |
The Hub-mediated architecture makes these controls straightforward to enable—they plug into the existing request flow without changing tool implementations.
Need these features? Contact us to discuss Enterprise pricing.
Further Reading