Connected accounts
Let users connect their GitHub, Google Drive, Slack or other accounts once, and let your APIs and MCP servers obtain those accounts’ tokens through Authio.
Agents and APIs often need to act in another service as the user: open an issue on GitHub, read a file in Google Drive. Connected accounts keep that access in Authio. The user connects their account once, Authio stores and renews its tokens, and your resource server exchanges the token it received from the user for the third-party access token whenever it needs one. The third-party refresh token never leaves Authio.
A connection provider is the OAuth app your realm registers at the external service. Each realm uses its own app, so what the users of one realm authorize never reaches another.
How it works
You register a connection provider
The OAuth app you created at GitHub, Google or another service, registered in the realm.
The user connects their account
On Authio’s hosted pages, signed in to your application, the user authorizes the service. Authio stores the tokens encrypted.
Your resource server exchanges the user’s token
When the user calls your API or MCP server, it presents the access token it received to Authio’s token endpoint with a token exchange request.
Authio returns the provider’s access token
Renewed first if it is about to expire. Your server calls the service with it, on the user’s behalf.
Register a connection provider
Create the app at the provider
At the external service, create an OAuth app and register this callback (redirect) URL on it:
Callback URLPlain texthttps://authio.api.hyperio.tech/realms/acme/connections/callbackRegister it in Authio
Open Connection Providers, choose New connection provider, pick the type and fill in the app’s details:
Field Rules Alias 2 to 64 lowercase letters, digits and hyphens, such as google-drive. Unique in the realm and fixed once created, because connect links and exchange requests use it.Display name Up to 128 characters, shown to users. Client ID and secret Those of the app you created. The secret is stored encrypted and never shown again. Scopes What a connection asks the service for. Left empty, the type’s default scopes apply. Separate them with spaces; commas are accepted too. Allowed resource servers The resource servers that may exchange tokens from this provider. Empty means every resource server of the realm, including ones created later. Keep it active
Only an active provider accepts new connections and exchanges. Deactivating one keeps the accounts already connected through it.
Provider types
| Type | Default scopes | Token renewal |
|---|---|---|
| GitHub App | None: permissions are set on the app | When the app uses expiring tokens |
| GitHub OAuth App | read:user user:email | Not needed: its tokens do not expire |
| GitLab | read_api read_user | Yes |
| Google Drive | openid email https://www.googleapis.com/auth/drive.file | Yes |
| Microsoft OneDrive | offline_access User.Read Files.Read | Yes |
| Dropbox | account_info.read files.metadata.read files.content.read | Yes |
| Slack | channels:read users:read | Yes |
| Notion | None: the user picks pages when consenting | Yes |
| Atlassian | read:me read:jira-work offline_access | Yes |
| Linear | read | Yes |
| Custom OAuth 2.0 | Yours. Requires https authorization, token and user info endpoints; revocation is optional. | Yes |
Let users connect their accounts
Connecting happens on Authio’s hosted pages, for a user signed in to the realm through your client. Link them to one of two addresses on https://authio.api.hyperio.tech:
| Address | What it does |
|---|---|
/realms/acme/clients/acme-web/account/connections | The user’s connected accounts page: the realm’s active providers, the accounts already connected, the default account of each provider, and buttons to connect and disconnect. |
/realms/acme/clients/acme-web/connections/google-drive/connect | Starts connecting one provider right away. Accepts return_to, a path under /realms/acme/ to land on afterwards, and context, the ID of an organization the connection is made for. |
- Without a session for the realm and the client, the user lands on an error page asking them to sign in first.
- The flow uses PKCE (S256) with every provider that supports it, and a single-use
statevalid for 15 minutes. - Back from the provider, the landing page receives
connected=google-driveon success, orconnection_error=ConnectionDeniedwhen the user refused. - Reconnecting the same external account renews its tokens and puts it back in force. The first account connected for a provider becomes the default.
Get the third-party token
Only a resource server can exchange. The caller authenticates as the introspection client of an active resource server — a confidential client with the Token Exchange grant enabled — and the user’s access token must be addressed (aud) to one of its resource servers.
curl -X POST https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/token \
-u "acme-api:YOUR_CLIENT_SECRET" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "subject_token=$USER_ACCESS_TOKEN" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
-d "requested_token_type=urn:authio:params:oauth:token-type:connection-access-token" \
-d "connection_provider=google-drive"- Pass
connection_idto pick one connection when the user has several. Without it, Authio uses the default connection, or the only active one. - A token that expires within 60 seconds is renewed with the provider before it is returned, and concurrent requests never spend the same refresh token twice.
expires_inis left out when the provider’s token does not expire. The provider’s refresh token is never part of the response.
When there is no token to return
Errors come in the OAuth format, with error_code and, when they help, connect_uri, connection_provider and connections:
{
"error": "connection_required",
"error_code": "ConnectionRequired",
"connection_provider": "google-drive",
"connect_uri": "https://authio.api.hyperio.tech/realms/acme/clients/acme-web/connections/google-drive/connect"
}| Error | What to do |
|---|---|
connection_required | The user has no usable connection — never connected, or the provider refused a renewal. Send them to connect_uri, which points at the client they signed in with. |
connection_selection_required | Several active connections and no default. Pick one from connections (id, label, external_login) and repeat with connection_id. |
connection_policy_violation | The account no longer meets the organization’s policy. The user has to connect an account the policy accepts. |
invalid_target | The provider does not exist, is inactive or does not allow this resource server. |
invalid_grant | The user’s token is not active, is not addressed to one of your resource servers or names no user. |
unauthorized_client | The caller is not the introspection client of an active resource server. |
access_denied | The plan does not include connected accounts, or the month’s exchanges are used up. |
temporarily_unavailable | The provider failed or did not answer in time. The connection is left as it was; try again later. |
Organizations and connection policies
Every connection is made for a context: the user personally, or one of their organizations. A token with org_id only reaches connections made for that organization; a token without it only reaches personal ones.
Each organization can set connection policies on its People tab, saying which external accounts it accepts:
- Allowed email domains, matched exactly:
acme.comdoes not acceptbr.acme.com. Google accounts are checked by their Workspace domain, so a@gmail.comaccount is refused. - Allowed GitHub organizations, for GitHub providers: the account must be an active member of one of them.
- A provider’s own policy replaces the organization’s general one; the two never add up.
- With Require the organization’s context off, a token for the organization may fall back to the user’s personal account of that provider, as long as the policy accepts it.
Policies are applied when the account is connected and again on every exchange. A connection that stops meeting its policy is marked as a policy violation and stays unusable until the user connects an account the policy accepts — relaxing the policy later does not bring it back.
Disconnect and revoke
- Users disconnect an account on their connected accounts page. Authio asks the provider to revoke the tokens, as far as each provider allows, and deletes the connection.
- Administrators revoke a user’s connection from the Credentials tab of the user in Users. The tokens are discarded and the connection stays listed as revoked until the user connects the same account again.
- Deleting a provider revokes and deletes every connection made through it.
| Connection state | Meaning |
|---|---|
Active | Has a token that can be used or renewed. |
NeedsReauth | The provider refused a renewal, or the token expired with no way to renew it. The user has to connect again. |
PolicyViolation | The account no longer meets the organization’s connection policy. |
Revoked | An administrator revoked it. |
Security notes
- Access and refresh tokens are encrypted at rest. The admin API never returns them; it shows dates, granted scopes and the external account.
- Only the introspection client of a resource server can exchange, and only a user token addressed to one of its servers.
- Restrict each provider to the resource servers that need it: an empty list means all of them.
- Every connection keeps its own trail — connected, used, refreshed, refresh failed, disconnected, revoked, policy violation — on the provider’s Access tab, and every exchange, delivered or refused, is recorded in the access log as a token exchange.