OAuth user verification

Verify the pending end user on your server before activating an OAuth connection.

In Project settings → OAuth user verification, optionally set a callback URL on your server. Live and Sandbox have separate settings. Leave the URL blank to disable verification. Project owner/admin access is required in the console.

Saving generates a new signing key, shown once. Store it securely on your server before allowing new connections. Saving an existing URL also rotates the key; in-flight authorizations using old settings fail and require a new connection link. The key is encrypted at rest and never returned by settings reads.

Business keys can read GET /v1/oauth-user-verification with connect_settings:read, or save PUT /v1/oauth-user-verification with connect_settings:manage and { "url": "https://your-server.example/arc0/verify" } (or {"url": null} to disable). The management response wraps settings in data: {url, revision}; saving an enabled URL also returns signingSecret once. These settings are not exposed as management MCP tools. A key authorized to manage Connect settings can configure this callback.

Request and response contract

After a successful OAuth exchange and provider identity check, Arc0 sends one HTTPS POST before storing/activating the new connection or updating a reconnect. The callback runs server to server. It only proves that your server still approves this externalId. It does not prove which browser finished OAuth, or bind that browser to your application's signed-in user. Arc0 sends the connector's linked provider identity (identity.id and identity.name) so your server can compare accounts. Each field is null when unavailable; do not treat a missing identity as proof of an account match. No OAuth code or provider token is sent.

{
  "version": "1",
  "projectId": "project-id",
  "connectionId": "pending-connection-id",
  "attemptId": "authorization-attempt-id",
  "externalId": "your-user-id",
  "providerIdentity": {"id": "provider-account-id", "name": "Account name"},
  "challenge": "random-single-attempt-challenge"
}

Headers are Content-Type: application/json, Arc0-Timestamp (Unix seconds), Arc0-Challenge, and Arc0-Signature (lowercase hex HMAC-SHA256). Use the signing key as its UTF-8 string, without base64 decoding it. The exact UTF-8 string signed is:

arc0.user-verification.v1.request.<timestamp>.<challenge>.<raw-request-body>

Validate the HMAC with a constant-time comparison, require a timestamp within 30 seconds, check the header challenge equals the body's challenge, and reject challenges/attempts already accepted (store them for at least 60 seconds). Resolve the pending external ID against your own authoritative user/session records and connection policy. Merely echoing the ID without checking it does not establish user identity. Never trust an unsigned external ID or a caller-supplied user ID.

To approve, return HTTP 200 with exactly:

{"externalId":"your-user-id","challenge":"the-request-challenge"}

Include Arc0-Signature, using the same key, timestamp, challenge and your exact raw response body with a separate response prefix:

arc0.user-verification.v1.response.<timestamp>.<challenge>.<raw-response-body>

Arc0 checks the signed response, challenge, and exact case-sensitive external ID match. A response from a different attempt cannot approve this one. Any other status rejects the connection. Group-owned OAuth has no end-user ID and fails when verification is enabled. Static API-key/basic/two-step credential flows are unaffected. Existing active connections and refreshes do not invoke this hook.

Limits and failures

URLs must use HTTPS on port 443, without credentials or fragments. Loopback, private, link-local and reserved destinations are rejected. Dispatch checks all DNS answers and pins a public address while preserving TLS hostname verification; redirects are rejected. The total verification budget is five seconds, including DNS and the response body. Approved response bodies are limited to 4 KiB.

Arc0 fails closed and does not retry verification. Start a new connection flow after fixing the error. A rejected new connection has no active credential; failed reconnects preserve the previous connection. The authorization attempt records one of these error codes:

CodeMeaning
user_verification_unsafe_urlInvalid URL or unsafe DNS destination
user_verification_user_requiredNo active end user for the pending connection
user_verification_timeoutFive-second deadline exceeded
user_verification_unavailableDNS, TLS or transport failure
user_verification_rejectedCustomer returned a status other than 200
user_verification_invalid_signatureMissing or forged response signature
user_verification_invalid_responseMalformed, oversized or wrong-challenge response
user_verification_user_mismatchCustomer returned another external ID
user_verification_settings_changedProject configuration changed before activation

Audit records

connection.user_verified records a successful signed approval, with the pending connection ID, attempt ID and verification revision. It records approval, not connection activation: a later authority or settings check can still reject the connection. connection.authorization_failed includes the failure code and attempt ID. Settings updates record enabled and previousEnabled so operators can identify toggles. Signing keys, provider identity and callback bodies are not retained in these audit records.

On this page