Skip to main content
This is a thought experiment: what if embedded services could just be plopped in your frontend with zero configuration while still being secure? This isn’t something I plan to implement, but I thought it was interesting and worth sharing.
What if embedded services never needed manual SSO configuration? No dashboard. No shared secrets. Customers host a metadata file declaring their identity provider, and any service that implements the spec can validate their users’ tokens automatically. This document explains the problem, proposes a solution, and sketches a specification.

Part 1: Why Self-Hosted Bootstrap Matters

What Char Does Today

Char validates tokens from your identity provider. It doesn’t initiate authentication. It receives tokens your app already obtained and verifies them using public keys. This is different from “Login with Google” flows. When Slack implements Google login, Slack acts as an OAuth Client. Slack needs its own Client ID registered with Google. Every vendor needs their own registration. Char acts as a Passive Validator. We don’t initiate auth. We don’t need our own Client ID with your IDP. We validate tokens that were already issued to your app: the tokens your users already have. The question Char needs answered: which IDP issued this token? Once we know that, validation is standard OIDC: fetch the IDP’s public keys, verify the signature, check the issuer and audience claims.

Manual Configuration Today

You tell us which IDP to validate against by configuring it in our dashboard:
  1. Log into Char dashboard
  2. Navigate to Settings → Integration
  3. Select your IDP type (Okta, Google, Auth0…)
  4. Copy your Client ID from your IDP
  5. Paste it into our form
  6. Copy your IDP domain
  7. Paste it into our form
  8. Add your application’s domain to our allowlist
  9. Click Save
This works. But it’s friction. Every form field is a potential mistake. Every context-switch between your IDP console and our dashboard is a chance to abandon the integration entirely. The deeper problem: this configuration is redundant. You already know your IDP configuration (it’s in your IDP). You already control your domain (that’s where you’re embedding the agent). We’re asking you to re-state facts that already exist elsewhere, in a format we invented, in a system you have to learn. The dashboard step disappears. Configuration moves from our system to yours.

Declarative Configuration

What if you declared your IDP configuration in a file you control? You host a JSON document at a well-known URL. When a request arrives from your domain, we fetch the file, learn which IDP to validate against, and proceed. No dashboard. No copy-paste. The configuration lives in your infrastructure, under your version control. This isn’t a new idea. Client ID Metadata Documents (CIMD) emerged from a similar frustration in the OAuth world. The MCP ecosystem faced a scaling problem: AI clients connecting to thousands of servers, each requiring registration. CIMD inverts the model. Instead of clients registering with servers, clients publish their own metadata at a URL they control. The URL is the identity. Domain ownership becomes the trust anchor. The MCP spec adopted CIMD in November 2025. Auth0, Stytch, WorkOS, and others have published guidance and product support for CIMD in the MCP context (availability varies by provider).
This isn’t OIDC Federation. OIDC Federation solves a different problem: “Should this IDP issue tokens to an unknown Relying Party?” That requires trust chains, signed Entity Statements, and complex machinery, because the IDP is making a consequential decision.We’re solving a narrower problem: “Which IDP should I validate this token against?” We don’t ask the IDP for permission. We just fetch its public keys (which are public) and verify signatures. Domain ownership is sufficient trust for that question.

CIMD Extension

Extend CIMD with IDP configuration. A customer hosts a file at /.well-known/oauth-client that includes:
{
  "client_id": "https://crm.acme.com/.well-known/oauth-client",
  "client_name": "Acme CRM",

  "token_issuer": {
    "issuer": "https://acme.okta.com/oauth2/default",
    "expected_audience": "0oa..."
  }
}
When Char receives a request from crm.acme.com, we fetch this file, learn that tokens should be validated against the declared issuer with that expected audience, and proceed. The customer declared their truth in a file they control. We read it. No dashboard required. This isn’t just about reducing clicks. It’s about putting configuration where it belongs: with the customer, in their infrastructure, under their version control.

The Trust Model

The elegance of this approach is that it doesn’t require new trust mechanisms. It composes existing ones: Domain ownership is already how the web establishes identity. If you can host files at crm.acme.com, you control that domain. HTTPS and DNS provide that guarantee. CIMD simply uses it. IDP tokens are already how enterprises establish user identity. The customer’s IDP signs tokens with keys only it possesses. We validate signatures against published public keys. This is federated authentication, and we already do it. Audience claims already prevent token confusion. A token for one application can’t be used for another. We already validate this. CIMD-based discovery just connects these existing mechanisms. The customer’s domain proves they’re legitimate. Their CIMD declares their IDP. Their IDP’s tokens prove user identity. No new cryptography. No new trust assumptions. Just composition.

What This Enables

Instant trials. Embed the agent, host a CIMD, it works. No account required. Conversion happens when customers hit limits, not before they’ve seen why the product matters. Configuration-as-code. The CIMD is a file in the repository. It goes through code review. It deploys through CI/CD. Staging and production differ by environment, not by dashboard. Self-service onboarding. If you can host a valid CIMD, you’re onboarded. No waiting for us to approve anything. Single source of truth. Configuration lives in one place: yours. We don’t have a stale copy that disagrees with your IDP.

Broader Applicability

This pattern isn’t specific to Char. Any embedded SaaS that needs to verify user identity faces the same problem. Today, services like Intercom and Zendesk use HMAC shared secrets: the customer generates a secret, passes user data server-side, and the service validates the signature. This works, but it requires server-side integration and secret management. Every vendor has their own approach. Token Issuer Metadata eliminates this. If your app already authenticates users via OIDC (most do), you’re already issuing tokens. Third-party services embedded in your app can validate those tokens directly. One CIMD file, hosted once, works for any service that implements the spec. The network effect: once a customer hosts a CIMD with token_issuer, every third-party validator benefits. The first vendor to push adoption creates value for the whole ecosystem.

The Tradeoffs

This model has genuine limitations worth acknowledging: GitOps vs. ClickOps. This model shifts configuration from “ClickOps” (admins pasting values into dashboards) to “GitOps” (engineers committing config to code). For engineering-led organizations, this is a feature: version control, code review, reproducible environments. For non-technical teams who can’t easily host a JSON file, it’s a barrier. The hybrid approach (supporting both CIMD and dashboard) bridges this gap. No pre-flight validation. With dashboard configuration, customers can click “Test Connection” before users arrive. With CIMD discovery, the first real user is the test. Errors surface in production. A char cimd validate CLI tool or web-based validator could help customers catch errors before deploying. Bootstrap, not runtime. CIMD fetch happens once, when we first see requests from a new origin. After that, the configuration lives in our infrastructure. This means CIMD availability isn’t a runtime concern: if your app loads, we can reach your CIMD. (Char is embedded in your page. If your CDN is down, the page doesn’t load, and we never receive a request.) What does matter is config drift. If you rotate your Okta client ID but forget to update the CIMD file, tokens will fail validation. Domain compromise scope. If an attacker gains control of a customer’s domain, they can modify the CIMD. This is true of any domain-based trust model (it’s the same threat model as HTTPS itself), but worth noting explicitly. Standardization. The token_issuer extension doesn’t exist as a standard yet. This document proposes one. Adoption depends on whether other vendors see value in a shared approach.

Part 2: Technical Specification

The following sketches what a specification for CIMD-based IDP discovery might look like.

Extended CIMD Document

A standard CIMD document contains OAuth client metadata. We extend it with identity provider configuration:
{
  "client_id": "https://crm.acme.com/.well-known/oauth-client",
  "client_name": "Acme CRM",
  "client_uri": "https://crm.acme.com",
  "redirect_uris": ["https://crm.acme.com/callback"],

  "token_issuer": {
    "issuer": "https://accounts.google.com",
    "expected_audience": "123456789.apps.googleusercontent.com"
  }
}
The token_issuer object declares how to validate tokens from users of this application. Validators fetch {issuer}/.well-known/openid-configuration to discover the JWKS endpoint, then verify that the token’s aud claim matches expected_audience.
On standardization: The token_issuer field is a proposed extension to CIMD. While CIMD itself is an IETF draft with growing adoption, there is no standard for declaring IDP configuration inside a client metadata document. This proposal suggests one. Char is an early implementer, but the pattern is designed to be vendor-neutral and adoptable by any service that validates tokens passively.

Token Issuer Fields

FieldRequiredDescription
issuerYesFull issuer URL. Must match the token’s iss claim. JWKS discovered via {issuer}/.well-known/openid-configuration.
expected_audienceYesExpected value of the token’s aud claim. This is typically the OAuth client ID of the customer’s application.
typeNoOptional hint: google, okta, azure, auth0. Used for IDP-specific validation quirks (e.g., Google accepts both https://accounts.google.com and accounts.google.com as issuer).
Examples by IDP:
// Google
"token_issuer": {
  "issuer": "https://accounts.google.com",
  "expected_audience": "123...apps.googleusercontent.com",
  "type": "google"  // optional: handles dual-issuer quirk
}

// Okta (note: issuer includes the authorization server path)
"token_issuer": {
  "issuer": "https://acme.okta.com/oauth2/default",
  "expected_audience": "0oa..."
}

// Azure AD (note: issuer includes tenant ID)
"token_issuer": {
  "issuer": "https://login.microsoftonline.com/{tenant-id}/v2.0",
  "expected_audience": "..."
}

// Auth0
"token_issuer": {
  "issuer": "https://acme.auth0.com/",
  "expected_audience": "..."
}

// Any OIDC-compliant provider
"token_issuer": {
  "issuer": "https://auth.acme.com",
  "expected_audience": "..."
}

Optional Admin Configuration

To enable dashboard access for auto-provisioned organizations:
{
  "token_issuer": { ... },
  "admin_contact": {
    "email": "[email protected]"
  }
}
The first user whose token contains this email becomes the organization owner.

Discovery Flow

Auto-Provisioning Behavior

When Char receives a request from an unknown origin with a valid-looking token: Discovery: Fetch {origin}/.well-known/oauth-client. If not found or invalid, reject the request (fall back to requiring dashboard configuration). Validation: Parse the token_issuer extension. Validate the token against the declared IDP: fetch JWKS, verify signature, check issuer and audience claims. Provisioning: If validation succeeds, create an organization record with:
  • The origin as the primary allowed domain
  • IDP configuration from the CIMD
  • Free tier defaults
  • The authenticated user as the first end-user
Persistence: The organization record lives in Char’s infrastructure (Durable Objects). Subsequent requests validate against the stored config. We don’t re-fetch CIMD on every auth. CIMD is the bootstrap mechanism, not a runtime dependency. Periodic re-fetching could detect config changes, but token validation doesn’t depend on it.

Dashboard Access Flow

For customers to access the dashboard after auto-provisioning: Matching logic:
  • For Google: match by hd (hosted domain) claim
  • For Okta/Auth0: match by issuer URL
  • For Azure: match by tid (tenant ID) claim
  • Fallback: match by email domain

Security Considerations

Core Validations

CIMD URL validation: The client_id field in the CIMD must exactly match the URL it was fetched from. This prevents an attacker from hosting a CIMD that claims to be a different domain. HTTPS required: CIMD must be served over HTTPS. HTTP URLs are rejected. Issuer validation: The token’s iss claim must match the expected issuer for the declared IDP type. A CIMD declaring type: google cannot be used to validate tokens from Okta. Audience validation: The token’s aud claim must match the expected_audience declared in the CIMD. This prevents tokens intended for other applications from being accepted. Origin binding (preventing token replay): A natural concern: “If you accept tokens meant for other apps, can’t I steal a token from App A and use it on App B?” No, because the CIMD is fetched from the request’s origin:
  1. Attacker steals token from app-a.com (where aud: "client-id-A")
  2. Attacker sends token to Char from app-b.com
  3. Char fetches CIMD from app-b.com
  4. CIMD declares expected_audience: "client-id-B"
  5. Token’s aud doesn’t match → Rejected
The token is bound to the origin that declares it. A token valid for one domain cannot be used from another domain, even if both use the same IDP.
On Origin headers: Origin can be spoofed by non-browser clients. Origin binding is a routing hint, not a cryptographic proof. The actual security boundary is audience validation: the token’s aud claim must match the CIMD’s declared expected_audience. If an attacker could forge a valid token with the correct audience, they wouldn’t need to spoof Origin in the first place.

SSRF Hardening (Normative)

Fetching customer-hosted metadata creates SSRF risk. Implementations MUST:
  • Reject non-HTTPS URLs. No http://, file://, or other schemes.
  • Block private/loopback addresses. Resolve DNS before connecting and reject private ranges (10.x, 172.16-31.x, 192.168.x, 127.x, ::1, link-local). Re-check after any redirects.
  • Cap response size. 5 KB maximum for metadata documents.
  • Use aggressive timeouts. 5 seconds connect, 10 seconds total.
  • Rate limit per origin. Prevent abuse from repeated fetch attempts.

Domain Compromise Scope

A natural concern: “What if an attacker gains control of a customer’s domain?” The blast radius is more contained than it might appear. Even with full domain control, an attacker must still present tokens from a valid IDP. They could modify the CIMD to point to an IDP they control, but tokens from that IDP would only grant access to the organization associated with that domain. They cannot use a compromised domain to attack other organizations. Each org is isolated by its origin. The attacker is essentially attacking themselves. They control the domain, they control the CIMD, they control the IDP, and they get access to… an organization scoped to their own domain. This is the same trust model as HTTPS itself: domain ownership is the root of trust.

Token Lifecycle as Defense

The short-lived nature of OAuth tokens provides defense-in-depth:
  • ID tokens expire quickly. Best practice is 5-60 minutes. Even if a CIMD is compromised, existing user sessions expire naturally within this window.
  • Refresh tokens rotate. Modern IDPs issue new refresh tokens with each use. A stolen refresh token becomes invalid after one use. Many IDPs also detect reuse and revoke the entire token family.
  • CIMD changes invalidate sessions. If a domain changes hands and the new owner modifies the CIMD, existing tokens fail validation immediately. The issuer or audience no longer matches. There’s no persistent access that survives a CIMD change.
The attack window is bounded by token lifetime, not by how long it takes someone to notice the compromise.

Edge Cases Worth Noting

Subdomain takeover: If a customer has dangling DNS records pointing to decommissioned services (e.g., old Heroku or S3 deployments), an attacker could claim that subdomain and host a malicious CIMD. This is a real attack vector, but it’s a general web security issue, not specific to CIMD. The mitigation is the same: customers should audit their DNS records and remove dangling entries. Domain expiration: If a customer’s domain expires and someone else registers it, the new owner could claim the auto-provisioned organization. This is concerning for abandoned deployments. Mitigation options include:
  • Periodic re-validation of domain ownership
  • Alerting on CIMD changes (especially IDP changes)
  • Requiring explicit confirmation for significant configuration changes
First-user-as-admin race: If using admin_contact with an email, an attacker who knows that email and can obtain a valid token for it could beat the legitimate admin to claiming the org. This is a narrow window (only during initial provisioning) and requires the attacker to have legitimate IDP access. Low risk, but worth documenting.

What We’re Trusting

To summarize the trust model:
We TrustBecause
Domain ownership proves legitimacyHTTPS + DNS (same as all web security)
CIMD content is authoritativeOnly the domain owner can host it
IDP tokens prove user identityCryptographic signatures we verify
Token audience prevents confusionStandard OIDC claim validation
Short token lifetimes limit exposureIndustry best practice, IDP-enforced
This isn’t a novel trust model. It’s composition of existing, battle-tested mechanisms.

Relationship to OpenID Connect

This proposal occupies similar territory to existing OIDC specs, but it’s solving a narrower problem, which is why it can be simpler.

What OIDC Federation Solves

OpenID Connect Federation 1.0 addresses a broad challenge: how can an Identity Provider accept authentication requests from Relying Parties it has never seen before? The answer is sophisticated. Trust anchors issue signed statements about entities. Intermediate authorities form chains. RPs publish Entity Statements (signed JWTs) containing their metadata. When an RP initiates auth, the IDP walks the trust chain, validates signatures, applies federation policies, and decides whether to proceed. This machinery exists because OIDC Federation enables the full authentication flow: authorization requests, token issuance, consent, scopes. The IDP is actively participating. It needs strong guarantees before issuing tokens to an unknown party.

What We’re Actually Solving

Our problem is narrower: how can a third party know which IDP to validate tokens against, without pre-registration? We’re not participating in the auth flow. The customer’s IDP already issued the token. The user already authenticated. We just need to validate what we received. We’re asking a simpler question: “which IDP does this domain use?” The IDP doesn’t know we exist. We don’t ask it for permission. We just fetch its public keys (which are public) and verify signatures. This is standard OIDC token validation. The only question is which IDP to validate against.

Why Our Approach Is Simpler

The simplicity isn’t a shortcut. It’s appropriate for the scope:
OIDC FederationThis Proposal
Question answered”Should I issue tokens to this RP?""Which IDP issued this token?”
Who’s decidingThe IDP (active participant)Us (passive validator)
What happens nextFull auth flow, token issuanceToken validation only
Trust requirementsIDP must trust RP before issuing tokensWe just need to know which IDP to check
Failure modeAuth blockedValidation fails, user re-authenticates
OIDC Federation needs signed statements and trust chains because the IDP is making a consequential decision: issuing tokens that grant access. Getting that wrong has security implications. We’re making a simpler decision: validating tokens that were already issued. If we get it wrong, the token fails validation. The user re-authenticates. There’s no token leakage, no unauthorized access, just a failed validation.

What We Build On

To be precise about lineage: We build on CIMD. The core pattern (publish metadata at your domain, domain ownership = trust) comes directly from OAuth Client ID Metadata Documents. We’re extending CIMD with IDP configuration fields. We use OIDC. Token validation is standard OIDC. We fetch the IDP’s JWKS via /.well-known/openid-configuration, verify signatures, check issuer and audience claims. Nothing novel here. We occupy similar space as OIDC Federation. Both enable dynamic discovery without pre-registration. But we’re not building on Federation’s trust chain model because we don’t need it.

Summary

OIDC Federation is the right tool when IDPs need to trust unknown RPs before issuing tokens. For token validation by third parties, that machinery is overkill. Domain ownership is sufficient trust. The token itself carries cryptographic proof of the IDP’s involvement. We’re not asking the IDP to do anything, just reading its public keys. This proposal achieves dashboard-free SSO bootstrap for embedded agents by recognizing that token validation is a narrower problem than token issuance.

Current Status

This is exploratory work. CIMD itself is an IETF draft with growing adoption, but the token_issuer extension described here doesn’t exist as a standard. I’m not actively building this, just documenting an idea that seemed worth sharing. If it resonates, or if you see problems with the approach, I’d be curious to hear.

Further Reading

References