跳转到内容

Connected services

此内容尚不支持你的语言。

A project’s users often have accounts at other services: a Google account, a GitHub account, a Slack workspace. Connected services let them connect those accounts to the project. The project registers an application at each provider. A person then connects their account from their account page, and the provider gives this server tokens for that account. The server stores the tokens encrypted.

An agent working for the person can then obtain the person’s token at that service and act there for them. The agent asks this server, never the provider, and needs two agreements: the administrator’s and the person’s. See Letting agents use connected accounts.

Three words are used on this page:

  • A connected service is the project’s registration at one provider: Google, GitHub, Slack, Microsoft, an OpenID Connect provider, or a plain OAuth 2.0 provider.
  • A connected account is one person’s account at one connected service.
  • An agent is an application of the project that the person signed in to and gave access, such as an AI agent or an MCP client.

A connected service is not a way to sign in

Section titled “A connected service is not a way to sign in”

A connected service never appears on the sign-in page. People sign in to the project as before: with a password, or through an upstream identity provider. They connect an account afterwards, from their account page.

A project may use Google both for sign-in and as a connected service. The two are configured separately, in different places in the console, and each has its own callback address. You can link them, so that signing in with Google also connects the person’s Google account: see Connecting an account when the person signs in.

The account page already has a list called Connected apps, at /account/apps. That list goes the other way: it shows the applications that hold tokens issued by this server. Connected services are the accounts that this server holds tokens for.

Connected accounts hold tokens, and the server never stores them unencrypted. So the instance must encrypt stored secrets with a keyring, a key that the operator holds outside the database. Setting one up is described in Encryption at rest: the keyring.

Without a keyring:

  • the Connected services tab shows a warning in place of the Add button;
  • the admin API and agents are refused with 409 and code: 'keyring_required';
  • people cannot connect an account.

If the keyring’s key is lost, the tokens cannot be read any more. The person sees the account as needing to be connected again, and the console shows it the same way. The service’s own client secret was encrypted with the same key, so an administrator enters the secret again first. Then each person connects the account again, and the new tokens replace the unreadable ones.

An administrator of the project declares a service. The administrators’ project cannot have connected services.

  1. Open the project and choose the Connected services tab. Choose Add.

  2. Under Provider, choose how the service is described: By name for Google, GitHub, Slack or Microsoft, OpenID Connect for any OpenID Connect provider, or OAuth 2.0 for a provider that speaks only OAuth 2.0.

  3. Register an application at the provider. The form links to the provider’s console and lists the steps. Give the provider the callback address that the tab shows (see The callback address).

  4. Paste the Client ID and Client secret that the provider issued. The secret is stored encrypted and is never shown again.

  5. Under Scopes, list what applications will be able to ask for at that provider. Name people see is the name on the person’s account page; for a service by name it defaults to the provider’s name.

  6. Save. The change is recorded in the audit trail.

You can later change a service’s scopes, its name, its secret and, for a plain OAuth 2.0 service, its endpoints. Its provider and its client ID never change, because the connected accounts’ tokens belong to that one registration. A different registration is a new service. When you add scopes, accounts that were already connected keep what they were granted. The scopes they lack are shown as not allowed, and the person can connect the account again to grant them.

Four providers are known by name. For each one you supply only the values that the provider issues:

Provider What you supply Where you register the application
Google Client ID, client secret Google Cloud console, Credentials
GitHub Client ID, client secret GitHub Developer settings
Slack Client ID, client secret Slack’s app management page, api.slack.com/apps
Microsoft Client ID, client secret, and a Directory Microsoft Entra admin center, App registrations

Each provider has something to know:

  • Google. Create an OAuth client ID of type “Web application” and add the callback address as an authorised redirect URI. Turn on the Google APIs your applications will call, and list their scopes here. The server always asks for openid and email. It also asks Google for offline access, so that Google issues a refresh token every time a person connects.
  • GitHub. Register an OAuth app, or a GitHub App with user authorization, and set its authorization callback URL to the callback address. The suggested scope is read:user.
  • Slack. Create a Slack app and add the callback address as a redirect URL. List the user token scopes you need; the suggested one is users:read. Turn on token rotation, or Slack issues no refresh token. Do not turn on PKCE for the app. In Slack, PKCE makes the app a public client, the change cannot be undone, and refresh tokens then last at most 30 days. The server never sends Slack a PKCE challenge. A Slack account is one person in one workspace, so the same person connecting two workspaces has two connected accounts.
  • Microsoft. The Directory says whose accounts may connect: common for work and personal accounts, organizations for work accounts only, consumers for personal accounts only, or one directory’s ID. Register an application with a “Web” redirect URI set to the callback address, and create a client secret under Certificates & secrets. The server always asks for openid, profile and offline_access, so that Microsoft issues a refresh token. The suggested scope is User.Read.

Apple is not offered. Apple issues no token that its own service APIs accept, so there is nothing to connect.

For any other OpenID Connect provider, give its Issuer, such as https://accounts.example.com. The server reads the provider’s endpoints from its discovery document. It identifies the account by the sub claim of the ID token. If the discovery document names a revocation endpoint, the server uses it on disconnect.

Many providers issue a refresh token only when asked for the scope offline_access. If yours does, add that scope.

A provider that does not speak OpenID Connect needs more values:

  • Authorization endpoint and Token endpoint: the provider’s two OAuth addresses.
  • Who the account is: where the server finds the id of the person’s account. This is required. Without it, a second account at the provider could not be told apart from the same account connected again. Choose one source:
    • From an endpoint: an Identity endpoint that the server calls with the new access token, such as https://example.com/api/me, and the path to the account id in its answer.
    • From the token answer: the path to the account id in the token endpoint’s own answer.
  • Path to the account id: a dotted path into the JSON answer, such as user.id. a|b takes the first of the two that is present.
  • Path to a name for people (optional): a path to a value the person recognises, such as a login or an address. It is shown beside the account.
  • Revocation endpoint (optional): an RFC 7009 token revocation endpoint. Without one, the provider is not asked to revoke anything on disconnect.

The server authenticates to the token endpoint with HTTP Basic, using the client ID and secret. It always sends a PKCE challenge; a provider that does not support PKCE ignores it.

Every connected service of a project uses one callback address: the project’s address (its issuer) followed by /account/services/callback. The Connected services tab shows it with a copy button. For an instance at https://auth.example.com and a project with the slug acme:

The project’s address Callback address
A path https://auth.example.com/acme/account/services/callback
A subdomain https://acme.auth.example.com/account/services/callback
A hostname of its own https://login.acme.com/account/services/callback
The root, for the default project https://auth.example.com/account/services/callback

Register this exact address at each provider. The callback address follows the project’s address. If the project’s address changes, register the new callback address at every provider as well. While the project has a connected service, only a super administrator may change its address. The address-change preview lists each connected service with its new callback address. Accounts already connected keep working; only new connections fail until the provider has the new address.

A person manages connected accounts on the project’s account page, at /account/services under the project’s address. It links to Connected apps, and Connected apps links back once the project offers at least one service.

  • Connect. The page lists the project’s services, each with a Connect button. The person confirms, goes to the provider, signs in there and allows access. Then they come back to the page, which says that the account is connected.
  • Several accounts. A person may connect up to 10 accounts at one service, for example a work and a personal GitHub account. Connecting the same account again replaces its tokens. It does not add a second copy.
  • What was granted. Each account shows the scopes the provider granted, and any requested scopes that it did not grant.
  • A recent sign-in. Connecting and disconnecting both need a sign-in within the last 15 minutes. Otherwise the person is asked to sign in again, and then comes back to the same confirmation.

If something goes wrong, the page says so in plain words: the person declined at the provider, connecting took longer than ten minutes, the limit of 10 accounts is reached, or the service cannot be connected right now.

Connecting an account when the person signs in

Section titled “Connecting an account when the person signs in”

People who sign in with Google need not connect Google a second time. Link the project’s Google sign-in provider to the Google service, and every sign-in through that provider also connects the person’s Google account at the service.

To link them, open the service on the Connected services tab and choose the provider under Keep tokens when people sign in with. Only a provider that matches can be chosen:

  • The same provider. Google with Google, Microsoft with Microsoft for the same tenant, GitHub with GitHub, or an OpenID Connect provider with the same issuer.
  • The same client ID. Tokens belong to the registration that asked for them, and the service renews them with its own registration. If the two were created with different registrations, declare the service again with the sign-in provider’s client ID and secret, and add the service’s callback address to that registration.
  • Not linked elsewhere. One sign-in provider keeps tokens for one service.

Slack cannot be linked: its tokens for people come from an endpoint that its sign-in never calls. As for declaring a service, a keyring must be in use. The sign-in provider’s page shows the link too.

A sign-in through a linked provider then works like this:

  • What it asks for. The service’s scopes as well as the sign-in’s own, and offline access where the provider needs it: Google’s access_type=offline, Microsoft’s offline_access. It never forces the provider’s consent screen. A provider that remembers what the person allowed does not ask again.
  • What it keeps. When the sign-in completes, the account is connected exactly as if the person had connected it on their account page, and the page says it was connected at sign-in. A sign-in that is refused, or that stops to ask for the password of an existing account, connects nothing. The next sign-in does.
  • Who may use it. Signing in allows no application. An agent still needs the person’s permission, as the next section describes.
  • No refresh token. Google sends a refresh token only the first time a person allows the scopes. When a sign-in brings none, the account keeps the refresh token it already has. A new account without one works until its access token expires, and then needs connecting again.
  • The person’s own disconnect. If the person disconnects the account themselves, later sign-ins do not connect it again until the person connects it from their account page or through an agent’s link. An account that an administrator removed is connected again by the next sign-in.

Some organisations do not let people allow access to APIs themselves; a Microsoft organisation, for example, may require an administrator’s consent. When the provider refuses the extra scopes and says so, the sign-in is sent to the provider once more without them, and the person signs in as before, with nothing connected. An organisation that blocks the scopes on the provider’s own page never sends the person back, so nothing can be retried: unlink the provider if your people belong to such an organisation. Whatever goes wrong while connecting, the person still signs in.

To unlink, clear the same field. Accounts already connected stay and keep working. Deleting the sign-in provider removes the link in the same way.

An agent obtains a person’s token at a connected service only when two people have agreed. The administrator says which applications may obtain a service’s tokens. The person says which of those applications may use each of their accounts. The server checks both on every request, so a change to either one applies to the next request.

An administrator of the project allows an application in either of two places:

  • in the service’s editor, on the Connected services tab, under Applications that may obtain tokens;
  • on the application’s own page, in the Connected services card, which has a checkbox per service.

Both change the same list, and each change is recorded in the audit trail. Only an application that can obtain tokens may be allowed. It must:

  • be an application of this project, created by an administrator rather than registered by itself;
  • authenticate to the token endpoint, with a client secret or a key. A public application, such as a single-page app or a mobile app, never obtains anyone’s tokens;
  • have the token exchange grant type. When it lacks the grant or does not authenticate, the application’s card says so and links to its settings.

The service’s editor also shows the service’s Identifier for agents, with a copy button. A developer sends this value to name the service (see Obtaining a person’s token).

Removing an application from the list refuses its next request at once. Tokens it already received stay valid at the provider until they expire, which is usually within an hour.

Sometimes the agent does not call the provider itself. It calls an API of the project, such as an MCP server declared on the APIs tab, and that API calls the provider for the person. The API receives the person’s access token from the agent and exchanges that token for the person’s token at the service.

For this, open the API’s page and choose an application under Application that exchanges tokens for this API. The API’s server authenticates as that application when it exchanges tokens. The same rules apply as above: the application must be allowed the service, must authenticate, and must have the token exchange grant. The person’s agreement is still the one they gave the agent, not the API.

The person agrees per account and per agent. They can do it in two places.

  • On the consent screen. When the person signs in to an agent that may use a service, the consent screen adds a group called Your accounts at other services. It has one row per account, such as “Use your GitHub account octocat”. A person with no account there yet sees a row for the service itself, marked as not connected yet. Every row starts unticked. The person is asked once: a row they answered is not shown again. The device flow’s consent screen has no room for these rows, so it never shows them.
  • On their account page, at /account/services. Each connected account lists the applications that may use it, and when each one last obtained a token. The person can allow another application there, or Withdraw one. Both need a sign-in within the last 15 minutes.

A person who ticked a service before connecting any account there gets that agreement for the first account they connect. After a withdrawal, the page tells the person that tokens the agent already received stay valid at the provider until they expire, usually within an hour. When the person removes an application’s access entirely, on the Connected apps page, every agreement given to that application goes with it.

Administrators see these agreements but cannot give or withdraw them. A person’s Connected accounts card shows, for each account, the applications allowed to use it and when each last did.

This section is for the developer of an agent, or of an API that exchanges tokens for one. The agent holds the person’s access token from this project. It asks the project’s token endpoint for the person’s token at a connected service, using OAuth 2.0 Token Exchange (RFC 8693). Token exchange is a standard way to trade one token for another at a token endpoint.

Send a POST to the project’s token endpoint, which the project’s discovery document lists as token_endpoint. The application authenticates as it does for any other token request. This example uses a client secret:

Terminal window
curl https://auth.example.com/acme/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d subject_token="$PERSON_ACCESS_TOKEN" \
-d subject_token_type=urn:ietf:params:oauth:token-type:access_token \
-d requested_token_type=urn:ietf:params:oauth:token-type:access_token \
-d audience="$SERVICE_IDENTIFIER" \
-d login_hint=octocat \
-d scope=read:user
Parameter Value
grant_type urn:ietf:params:oauth:grant-type:token-exchange
subject_token the person’s access token from this project
subject_token_type urn:ietf:params:oauth:token-type:access_token
requested_token_type urn:ietf:params:oauth:token-type:access_token
audience the service’s Identifier for agents, from the console; exactly one
login_hint optional: which of the person’s accounts there, by its name at the provider
scope optional: scopes at the provider that the token must carry, separated by spaces

The name in login_hint is the one the person’s account page shows, such as a GitHub login or an email address. Auth0 Token Vault takes the same parameter for the same choice. Without it, the server picks the one account the person allowed this agent to use. If the person allowed several, the request must name one.

The access token in subject_token must have been issued to the agent that sends it. The one exception is an API with an exchanging application: that application may send an access token issued for its API.

{
"access_token": "gho_EXAMPLE",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3412,
"scope": "read:user"
}
  • access_token is the person’s token at the provider. Call the provider’s API with it directly.
  • expires_in is how many seconds the token has left. It is absent when the provider’s token does not expire, as with a GitHub OAuth app.
  • scope lists the scopes the token carries at the provider.

The answer never contains the provider’s refresh token. It is sent with Cache-Control: no-store, so nothing on the way keeps a copy. Ask again when the token expires. The server returns the stored token while it has more than a minute left, and refreshes it otherwise.

A refusal is a standard OAuth error with one more member, reason, which says what to do next:

{
"error": "invalid_grant",
"error_description": "the person has not allowed this application to use their account at GitHub",
"reason": "consent_required"
}
reason error HTTP What to do
not_permitted invalid_target 400 Ask the project’s administrator to allow the application for this service.
not_permitted invalid_scope 400 The service does not declare a scope you asked for. Ask the administrator to add it.
consent_required invalid_grant 400 Show the person the link in connect_uri.
connect_required invalid_grant 400 Show the person the link in connect_uri.
account_selection_required invalid_grant 400 Ask the person which of accounts to use, and send it as login_hint.
reconnect_required invalid_grant 400 Show the person the link in connect_uri. error_description names any missing scopes.
temporarily_unavailable temporarily_unavailable 503 Retry later.

Other failures have no reason. An expired or revoked access token, a token from another project, an application that is not allowed to present the token, and a person who can no longer sign in are all refused as invalid_grant. An application that lacks the token exchange grant is refused as unauthorized_client. No refusal says anything about another person’s accounts. The only labels a refusal lists are those in accounts, the accounts the person allowed this application to use.

Three of those refusals are fixed by the person, not by you: connecting a service, allowing your application, or connecting an account again. Each one carries a link you show the person:

{
"error": "invalid_grant",
"error_description": "the person has not connected a GitHub account",
"reason": "connect_required",
"connect_uri": "https://auth.example.com/account/services/link/q3Jx…",
"connect_id": "c6m2…",
"connect_expires_in": 1800,
"connect_message": "Open this link to connect your GitHub account and let Repo Helper use it."
}
  • connect_uri is the address to show. It opens a page on this server that offers exactly one thing: connect the service and allow your application, allow your application to use an account, or connect an account again.
  • connect_message is a sentence for the person, in their language when their account has one, else English.
  • connect_id stays the same while the link does. Ask again and you get the same link until it is used, declined or replaced by a link for a different step.
  • connect_expires_in is how many seconds the link has left. A link lives thirty minutes.

The link works only for the person whose access token you presented. Opening it asks them to sign in to the project if they have not done so in the last fifteen minutes. Anyone else who opens it is told it was made for another account, and nothing changes. The address carries no token and nothing about the person.

The page ends by telling the person to return to your application. It does not send them back anywhere. Your application learns the outcome by repeating the exchange: once the person has done what the link asked, the next exchange returns the token.

An MCP server that exchanges tokens for its client passes the link on as a URL elicitation. Every field comes from the refusal.

For an MCP 2025-11-25 client, answer the tool call with this error:

{
"code": -32042,
"message": "Open this link to connect your GitHub account and let Repo Helper use it.",
"data": {
"elicitations": [
{
"mode": "url",
"elicitationId": "c6m2…",
"url": "https://auth.example.com/account/services/link/q3Jx…",
"message": "Open this link to connect your GitHub account and let Repo Helper use it."
}
]
}
}

For an MCP 2026-07-28 client, return an input_required result whose URL request carries connect_uri and connect_message. In both versions the client retries the tool call after the person has finished, and the tool repeats the exchange.

For account_selection_required, ask the person to choose with a form elicitation whose options are the labels in accounts, then repeat the exchange with the chosen label as login_hint.

The server takes care of the stored tokens, so that agents and people rarely see a broken account.

  • One refresh at a time. When the stored access token has expired, the server refreshes it at the provider. If many requests need the same account at once, exactly one refresh reaches the provider, and every request gets its result. This also holds across several server processes. It matters because GitHub and Slack accept each refresh token only once: a second refresh with the same token would break the account.
  • Keeping idle accounts alive. Google, Microsoft and GitHub discard a refresh token that nobody used for some months. Every 30 days, the server refreshes each account that an agent may use, as long as an agent obtained a token of it within the last 12 months. After a year without use, the account is left to lapse.
  • Telling the person. When the provider rejects an account’s refresh token, the account is marked as needing reconnection, and the person gets one mail with a link to their account page. The person also gets a mail when an administrator disconnects their account or deletes the service. Mail goes only to a verified address, and only when the instance can send mail. A person who disconnects an account themselves gets no mail.

Disconnecting, and what the provider is asked

Section titled “Disconnecting, and what the provider is asked”

A disconnect deletes the connected account here at once. Then the server asks the provider to revoke the tokens, and tells the person what the provider answered:

Provider What the server asks
Google Google revokes the refresh token, which ends the whole grant
GitHub GitHub deletes the grant the person gave the application
Slack Slack revokes the person’s token
OpenID Connect the revocation endpoint from the discovery document, if it names one
Plain OAuth 2.0 the revocation endpoint you declared (RFC 7009), if you declared one
Microsoft nothing: Microsoft offers no way to revoke one token

When the provider offers no revocation, as Microsoft does not, the person gets a link to the provider’s own page where they can remove the application’s access themselves. When the provider cannot be reached, or refuses, the person is told to remove the access at the provider themselves. Either way, the account is already gone here.

GitHub and Slack revoke with the access token, and an expired one cannot be revoked. So when the stored access token has expired, the server first refreshes it and then revokes with the new one. If that refresh fails, the account is deleted anyway, and the person is told to remove the access at the provider.

Locking or deactivating a person keeps their connected accounts. Both can be undone, and a person who is unlocked should not have to connect everything again. A lock already ends the person’s sessions and grants at this server. An agent’s request for the token of a person who cannot sign in is refused, so a lock also stops every agent from obtaining that person’s tokens.

Three deletions remove connected accounts, and the server asks each provider to revoke the tokens in the background:

  • deleting the person, whether in the console, through the admin API, by an agent, or by a SCIM directory;
  • deleting the connected service, which disconnects every account at it;
  • the final destruction of the project, after its 30 days or by Delete now.

The Connected services tab lists each service with its provider, its scopes and how many accounts people have connected at it. Deleting a service asks for confirmation and says how many accounts it will disconnect.

A person’s connected accounts are on their Sign-in tab, in the Connected accounts card. It shows each account’s service, its name at the provider, the scopes granted and whether it needs to be connected again. An administrator can disconnect one there. The provider is asked to revoke it, as for a person’s own disconnect, and the person can connect it again later, and gets a mail saying that an administrator of the project disconnected it. The card also shows, for each account, the applications the person allowed to use it and when each last obtained a token, when the server last refreshed it, and why it needs reconnecting if it does. No token, nor any part of one, is ever shown.

When plans are turned on, a plan limits how many connected services a project may declare. On the Free plan the limit is 10. A service beyond the limit is refused with 409 and code: 'plan_limit', with limit.name set to connectedServicesPerProject. The limit counts services, not the accounts people connect.

An AI agent that administers the instance does everything an administrator does here through these MCP tools:

Tool Does Consequence
connected_service_list Lists a project’s services, with the callback address and account counts Read
connected_service_get Reads one service Read
connected_service_catalogue_list Lists the providers known by name, with the values each needs Read
connected_service_create Declares a service Ordinary
connected_service_update Changes a service’s scopes, name, secret, endpoints, allowed applications or linked sign-in provider Ordinary
connected_service_delete Deletes a service and disconnects every account at it High
connected_account_list Lists one person’s connected accounts Read
connected_account_delete Disconnects one person’s account High

A high-consequence tool asks for confirmation before it runs. No tool ever returns a client secret or a token. An API’s exchanging application is set with resource_update. The person’s own agreements are theirs alone: no tool gives or withdraws them. The admin API routes behind them are under /admin/api/projects/:id/connected-services and /admin/api/projects/:id/users/:uid/connected-accounts.