Who Holds the Keys?

In Corsair, authentication is not a string attached to the end of a request; it is a lifecycle with more than one owner. The focus here is how a plugin separates shared OAuth configuration from one tenant’s connection, how Corsair protects the resulting secrets, and what happens when an access token expires or stops working. That model helps a builder decide what can be automated and where a connection still needs application-owned recovery.

Two credential scopes, not one bucket

The first distinction is between an integration and an account. An integration is the provider definition shared by the application: for an OAuth provider, its client ID and client secret are normally configured once. An account is a connection for a particular tenant and provider. It may contain an API key, bot token, access token, refresh token, provider identifier, or webhook-signing value. Treating those as one undifferentiated secret would make it difficult to explain which users share a configuration and which users must remain separate.

Corsair exposes the distinction through the plugin’s authType. The available modes vary by plugin, but the documented model includes managed OAuth through Hub, bring-your-own OAuth 2.0, API keys or personal access tokens, and bot tokens. The application declares the mode when it registers a plugin. Feature code then calls the typed provider operation without passing a token each time. The plugin’s key builder asks the appropriate account key manager for the credential when a request needs it.

This is why an API key in a single-tenant internal tool and an API key supplied by each customer are not the same deployment. In the first case, the application may use the default account scope. In the second, the account credential belongs to a tenant and must be reached through that tenant’s Corsair client. The same distinction applies to OAuth: the client ID and client secret can be shared application configuration, while access and refresh tokens represent individual connected accounts.

The scope also affects webhook verification. A provider’s signing secret may be stored as account-level data, and the plugin’s key builder can choose it when the request source is a webhook rather than an API endpoint. The credential that authorizes a live call is not automatically the credential that proves an incoming event is genuine. Keeping the lookup decision inside the plugin contract makes that difference explicit instead of encouraging feature code to reuse whichever secret happens to be nearby.

A connection can be provisioned before it is usable. Corsair’s setup logic can create the integration and account rows and issue their encryption keys, but OAuth tokens arrive only after the user completes the connection flow. API-key and bot-token paths still need the account-level value. This produces a useful state distinction: “the database has a place for this account” is not the same as “the account has a valid credential.” A production UI should expose those as different conditions.

Imagine a SaaS product with two workspaces that both connect GitHub. They may use the same application-level OAuth configuration, but they must resolve different account credentials and possibly different provider installation identifiers. A call made for the first workspace must never fall through to the second workspace simply because both use the same plugin.