Connect Your Service to KDCube With OAuth/OIDC
A KDCube tool can call your service as the user — not with a shared system key, not with a platform login token, and not with a secret copied into code. The user connects their account once through Connection Hub; after that, tools and named services resolve the user's provider token only when the requested claim is approved.
This is the missing shape for custom services. Suppose there is a
service S1: it has its own users, its own OAuth/OIDC authority, and an API
that KDCube tools need to call. The user asks an agent to use S1. The tool
reaches the point where it needs an S1 token. If the token does not exist
yet, the tool does not fake it, fail as an anonymous request, or ask the developer for a
private secret. It returns a Connection Hub action: connect S1, approve the
claim, then retry.
The boundary
This is delegated to KDCube: an external provider issues a credential so KDCube can enter that provider on the user's behalf. It is one of three Connection Hub directions, and it is worth keeping them apart.
An external identity proves who the KDCube user is — a Cognito or Google-backed application login. This grants platform roles.
KDCube issues a bearer credential so automation can enter KDCube — an admin grants an automation scoped REST/MCP access.
An external provider issues a credential so KDCube can enter that provider — Gmail,
Slack, or a custom S1 OAuth/OIDC service.
That distinction matters. An S1 token is not a KDCube login. It does not
give the holder platform roles. It is only a provider credential that KDCube may use
after the current KDCube request is already authorized.
The three-layer model
Connection Hub separates the provider, the connector app, and the user's actual
connected account. One KDCube deployment configures a single S1 connector
app, while many users connect their own S1 accounts — and one user can
connect several accounts of the same provider (several Slack workspaces, or several
tenants of a custom service).
What the service must provide
For a generic OAuth/OIDC service, S1 must provide the normal OAuth
surface: an authorization endpoint, a token endpoint, an optional userinfo endpoint, a
client id and client secret, provider scopes, a stable account subject, and refresh-token
support for long-lived automation. It also registers KDCube's shared
delegated-to-KDCube callback URL:
https://<PUBLIC_HOST>/api/integrations/bundles/<TENANT>/<PROJECT>/connection-hub@1-0/public/delegated_to_kdcube_oauth_callback
The callback URL is exact. The host, tenant, project, application id, route, and
callback path all have to match the URL KDCube sends in the OAuth request. If
S1 is backed by Cognito, Auth0, Okta, or another OIDC provider, that is
fine — KDCube sees it as a provider account authority for S1, not as the
KDCube platform authority.
The Connection Hub config
The operator registers the provider and connector app under the Connection Hub application config.
bundles:
version: "1"
items:
- id: connection-hub@1-0
config:
connections:
delegated_to_kdcube:
enabled: true
oauth:
public_base_url: "https://<PUBLIC_HOST>"
providers:
s1:
label: S1
adapter: oidc.generic
enabled: true
oauth:
authorize_url: https://s1.example.com/oauth2/authorize
token_url: https://s1.example.com/oauth2/token
userinfo_url: https://s1.example.com/oauth2/userInfo
default_scopes: [openid, email, profile]
authorize_params:
audience: s1-api
profile:
subject: sub
email: email
display_name: name
workspace: custom.tenant
connector_apps:
default:
label: S1 connector
enabled: true
client_id: "<S1_CLIENT_ID>"
client_secret_ref: connections.delegated_to_kdcube.providers.s1.connector_apps.default.client_secret
allowed_claims: [s1:read, s1:write]
claims:
s1:read:
label: Read S1
description: Read S1 data for the approving user.
provider_scopes: [s1.read]
s1:write:
label: Write S1
description: Write S1 data for the approving user.
provider_scopes: [s1.write]
The secret lives in the secrets descriptor, not in the public config:
bundles:
version: "1"
items:
- id: connection-hub@1-0
secrets:
connections:
delegated_to_kdcube:
oauth_state_secret: "<RANDOM_HEX_32_BYTES>"
providers:
s1:
connector_apps:
default:
client_secret: "<S1_CLIENT_SECRET>"
The adapter is generic. Use oidc.generic when the provider has OIDC
identity fields such as sub and usually a userinfo endpoint; use
oauth2.generic when the provider is OAuth-only. Provider-specific adapters
still make sense for services with non-standard token responses, unusual refresh
behavior, or account metadata that cannot be mapped from token/userinfo JSON.
Claims are the contract between tools and provider access
KDCube claims are not provider scopes. A claim (s1:read) is the KDCube
consent unit a tool asks for; a scope (s1.read) is what the provider
understands. The connector app declares which claims this OAuth app may request — the
claim ceiling — and each tool declares the exact claims it needs.
connector_apps:
default:
allowed_claims: [s1:read, s1:write] # the ceiling
tool_claims:
read_s1_object:
connections:
delegated_to_kdcube:
connected_accounts:
- provider_id: s1
connector_app_id: default
claims: [s1:read] # exactly what this tool needs
There is no intermediate capability registry here. The connector app defines the claim ceiling, each tool declares the exact provider claims it needs, and Connection Hub enforces both sides.
The runtime flow
At runtime, the tool resolves the credential through the SDK, calls the service, and wraps the whole body so a rejected token becomes a retry rather than a dead end.
from kdcube_ai_app.apps.chat.sdk.integrations.connected_accounts import (
connected_account_auth_failure,
resolve_connected_account_claim,
run_with_connected_account_retry,
)
async def read_s1_object(source, object_id: str, account_id: str | None = None):
async def _run():
credential = await resolve_connected_account_claim(
source,
provider_id="s1",
connector_app_id="default",
claim="s1:read",
tool_name="s1.read_s1_object",
account_id=account_id,
)
if not credential.ok:
return credential.error_envelope(where="s1.read_s1_object")
response = await call_s1_api(
access_token=credential.access_token,
object_id=object_id,
)
if response.status_code in (401, 403):
return connected_account_auth_failure(
credential,
"S1 rejected the access token",
)
return response.json()
return await run_with_connected_account_retry(
source,
where="s1.read_s1_object",
run=_run,
)
The retry wrapper handles the practical OAuth failure mode: when the provider rejects
the token, it force-refreshes that credential and retries the body once. If the refresh
works, the provider result flows back; if the provider still rejects it, the account is
marked reconnect_required and the tool returns a Connection Hub reconnect
action. That is the difference between a provider integration and a raw API call — the
error becomes a user action: connect, approve a missing claim, pick an account, or
reconnect.
How this reaches named services and MCP
The same provider-account contract can sit behind a named-service namespace. An
external agent — Claude, or generated code — reaches
kdcube-services@1-0 under a KDCube delegated credential or a local runtime
identity, calls the s1 namespace, and the provider resolves the connected
account claim s1:read before calling the S1 API with the user's own token.
There are two boundaries:
- Client → KDCube. Is this client allowed to call this KDCube named-service operation?
- KDCube → S1. Has the current KDCube user connected S1 and approved the provider claim?
This is how a custom integration becomes agent-ready without giving the agent a
provider secret. An external client can connect to the KDCube named-services MCP surface,
but the actual S1 token remains a user-scoped connected account inside
Connection Hub.
What the user sees
The experience is demand-driven. The tool needs s1:read; no connected
account or approved claim exists; chat receives a managed consent envelope with a
Connection Hub URL. The user opens Connection Hub, signs into S1, and
approves the requested access; Connection Hub stores the account metadata and credential;
the user continues, and the tool resolves the S1 credential and succeeds. In
the widget this belongs under Delegated to KDCube — the external
accounts this user allows KDCube applications and automation to use.
S1 access to KDCube's
platform identity. They are granting KDCube permission to use their S1
account for specific KDCube claims.Why this is useful
This makes custom service integration repeatable — the service owner brings
OAuth/OIDC, the operator registers the connector app and claim mapping, the tool
developer declares tool_claims, and the user approves only when a tool
actually needs the provider account. Named services can then expose the integration to
agents through the same Connection Hub boundary.
Related documentation
- Custom OAuth/OIDC service: the delegated-account solution
- Recipe: connect a custom OAuth/OIDC service
- Delegated accounts: claims, credential health, reason codes
- Connection Hub: the canonical solution map
- Recipe: Google / Gmail integration
- Recipe: Slack integration
- Recipe: mail as a named service
- Namespace-service providers