External Credentials
The external credentials provider is the Gestalt provider responsible for connection credentials. When a user completes OAuth, enters a manual API key, or a credential is provisioned for a service account, Gestalt uses the configured external credentials provider to validate, exchange, store, and resolve the runtime credential for that connection.
External credentials are separate from Gestalt authentication, which identifies the caller, and authorization, which decides what the caller may access. An external credential is the credential Gestalt uses to call an upstream API for a configured connection.
Core concepts
Subject scoping
External credentials are scoped to a canonical subject. Connections resolve credentials for the effective credential subject of the current request.
Integration and connection
A credential belongs to a specific configured connection:
- Provider is the configured provider name, such as
githuborslack - Connection is the named connection within that provider, such as
defaultorenterprise - Instance is an optional sub-identifier for multi-account integrations
This lets the same subject connect more than one account for the same provider when the provider supports instances.
Connection modes
Connection mode controls credential ownership:
| Mode | Credential ownership |
|---|---|
none | No upstream credential is required. |
subject | Credentials are owned by the effective subject of the request. |
Use subject for SaaS APIs that should act as the caller. For shared service
accounts, service-to-service credentials, and model/API credentials, provision
the credential for a service account subject and execute
with runAs.
Credential lifecycle
External credentials move through three phases:
- Connect time. Gestalt validates the connection auth config, completes OAuth or manual auth, optionally exchanges submitted credentials, and stores the resulting credential.
- Runtime. When an app, workflow, or agent-bound connection needs to call an upstream service, Gestalt asks the external credentials provider to resolve a usable runtime credential for the selected connection. Resolution may refresh an expiring access token on demand when the connection auth supports it.
- Maintenance. Connections with
credentialRefreshopt into provider-owned background maintenance. Gestalt passes the resolved connection metadata to the external credentials provider; a supporting provider can refresh stored credentials before first use.
Credential material can include access tokens, refresh tokens, API keys, and
provider-specific token responses. The exact shape depends on the connection’s
auth block and the configured external credentials provider.
How external credentials work
External credentials are driven by connection config. An app or agent provider declares a connection, the deployer binds or overrides that connection in server config, and callers select a connection and instance when needed.
connections:
github-user:
mode: subject
auth:
type: oauth2
authorizationUrl: https://github.com/login/oauth/authorize
tokenUrl: https://github.com/login/oauth/access_token
scopes:
- repo
apps:
github:
source: ./apps/github/manifest.yaml
connections:
default:
ref: github-userFor automation-owned credentials, define a normal subject-scoped connection and provision the credential for the service account subject that will run it:
connections:
model-api:
mode: subject
auth:
type: bearer
params:
baseUrl: https://models.example.com/v1
providers:
agent:
default:
source: ./providers/agent/example/manifest.yaml
connections:
model:
ref: model-apiThe credential material is stored in the external credentials provider under
the service account subject, such as service_account:model-runner. Workflows
and agents select that subject with runAs.
At runtime, providers that support connection bindings receive resolved runtime connection material from Gestalt. They should treat that material as the connection contract and avoid depending on how the credential was obtained.
First-party external credentials providers
First-party external credentials providers live under
valon-technologies/gestalt-providers/externalcredentials.
| Provider | Use case |
|---|---|
github.com/valon-technologies/gestalt-providers/externalcredentials/default | Stores credentials in the host IndexedDB provider |
Configuring providers.externalCredentials
server:
providers:
indexeddb: main
externalCredentials: default
providers:
indexeddb:
main:
source:
package: github.com/valon-technologies/gestalt-providers/indexeddb/relationaldb
version: 0.0.1-alpha.1
config:
dsn: ${DATABASE_URL}
externalCredentials:
default:
source:
package: github.com/valon-technologies/gestalt-providers/externalcredentials/default
version: 0.0.1-alpha.1
config:
indexeddb: mainThe first-party externalcredentials/default provider stores credentials in
the selected host IndexedDB provider.
If you omit providers.externalCredentials, Gestalt uses the first-party
externalcredentials/default provider with the selected host IndexedDB
provider.
server.providers.externalCredentials selects the host external credentials
provider. If more than one entry exists under providers.externalCredentials,
set server.providers.externalCredentials explicitly or mark one entry
default: true.
providers.externalCredentials is a deployment-wide provider selection. Plugin,
workflow, and agent providers do not select their own external credentials
provider; they declare or bind connections and let Gestalt resolve those
connections through the configured external credentials provider.
Building your own external credentials provider
Manifest
An external credentials provider manifest declares kind: externalcredentials:
kind: externalcredentials
source: github.com/your-org/external-credentials/vault
version: 0.0.1
displayName: Vault External Credentials
description: Stores Gestalt connection credentials in Vault.
spec:
configSchemaPath: ./externalcredentials_config.jsonProvider contract
The Go SDK external credential provider contract has three responsibilities:
| Area | Methods |
|---|---|
| Stored credentials | UpsertCredential, GetCredential, ListCredentials, DeleteCredential |
| Connection config | ValidateCredentialConfig |
| Runtime material | ResolveCredential, ExchangeCredential |
ResolveCredential is on the hot path for app and agent-provider calls. It
receives the configured connection, credential subject, and instance, then
returns the token or connection parameters that Gestalt should pass to the
target provider.
package vaultcredentials
import (
"context"
"fmt"
"sync"
"time"
gestalt "github.com/valon-technologies/gestalt/sdk/go"
)
type Provider struct {
mu sync.Mutex
credentials map[string]*gestalt.ExternalCredential
lookupByID map[string]string
}
func New() *Provider {
return &Provider{
credentials: map[string]*gestalt.ExternalCredential{},
lookupByID: map[string]string{},
}
}
func (p *Provider) Configure(_ context.Context, _ string, _ map[string]any) error {
return nil
}
func (p *Provider) UpsertCredential(
_ context.Context,
req *gestalt.UpsertExternalCredentialRequest,
) (*gestalt.ExternalCredential, error) {
credential := req.GetCredential()
if credential == nil {
return nil, fmt.Errorf("credential is required")
}
value := cloneCredential(credential)
if value.GetId() == "" {
value.ID = value.GetSubjectId() + ":" + value.GetConnectionId() + ":" + value.GetInstance()
}
p.mu.Lock()
defer p.mu.Unlock()
key := lookupKey(value.GetSubjectId(), value.GetConnectionId(), value.GetInstance())
p.credentials[key] = value
p.lookupByID[value.GetId()] = key
return cloneCredential(value), nil
}
func (p *Provider) GetCredential(
_ context.Context,
req *gestalt.GetExternalCredentialRequest,
) (*gestalt.ExternalCredential, error) {
lookup := req.GetLookup()
if lookup == nil {
return nil, fmt.Errorf("lookup is required")
}
p.mu.Lock()
defer p.mu.Unlock()
credential, ok := p.credentials[lookupKey(
lookup.GetSubjectId(),
lookup.GetConnectionId(),
lookup.GetInstance(),
)]
if !ok {
return nil, gestalt.ErrExternalCredentialNotFound
}
return cloneCredential(credential), nil
}
func (p *Provider) ListCredentials(
_ context.Context,
req *gestalt.ListExternalCredentialsRequest,
) (*gestalt.ListExternalCredentialsResponse, error) {
p.mu.Lock()
defer p.mu.Unlock()
response := &gestalt.ListExternalCredentialsResponse{}
for _, credential := range p.credentials {
if req.GetSubjectId() != "" && credential.GetSubjectId() != req.GetSubjectId() {
continue
}
if req.GetConnectionId() != "" && credential.GetConnectionId() != req.GetConnectionId() {
continue
}
if req.GetInstance() != "" && credential.GetInstance() != req.GetInstance() {
continue
}
response.Credentials = append(
response.Credentials,
cloneCredential(credential),
)
}
return response, nil
}
func (p *Provider) DeleteCredential(
_ context.Context,
req *gestalt.DeleteExternalCredentialRequest,
) error {
p.mu.Lock()
defer p.mu.Unlock()
key, ok := p.lookupByID[req.GetId()]
if !ok {
return gestalt.ErrExternalCredentialNotFound
}
delete(p.lookupByID, req.GetId())
delete(p.credentials, key)
return nil
}
func (p *Provider) ValidateCredentialConfig(
context.Context,
*gestalt.ValidateExternalCredentialConfigRequest,
) error {
return nil
}
func (p *Provider) ResolveCredential(
ctx context.Context,
req *gestalt.ResolveExternalCredentialRequest,
) (*gestalt.ResolveExternalCredentialResponse, error) {
credential, err := p.GetCredential(ctx, &gestalt.GetExternalCredentialRequest{
Lookup: &gestalt.ExternalCredentialLookup{
SubjectID: req.GetCredentialSubjectId(),
ConnectionID: req.GetConnectionId(),
Instance: req.GetInstance(),
},
})
if err != nil {
return nil, err
}
return &gestalt.ResolveExternalCredentialResponse{
Token: credential.GetAccessToken(),
ExpiresAt: credential.GetExpiresAt(),
Credential: credential,
}, nil
}
func (p *Provider) ExchangeCredential(
context.Context,
*gestalt.ExchangeExternalCredentialRequest,
) (*gestalt.ExchangeExternalCredentialResponse, error) {
return &gestalt.ExchangeExternalCredentialResponse{}, nil
}
func lookupKey(subjectID, connectionID, instance string) string {
return subjectID + "\x00" + connectionID + "\x00" + instance
}
func cloneCredential(in *gestalt.ExternalCredential) *gestalt.ExternalCredential {
if in == nil {
return nil
}
out := *in
out.ExpiresAt = cloneTime(in.ExpiresAt)
out.LastRefreshedAt = cloneTime(in.LastRefreshedAt)
out.CreatedAt = cloneTime(in.CreatedAt)
out.UpdatedAt = cloneTime(in.UpdatedAt)
return &out
}
func cloneTime(in *time.Time) *time.Time {
if in == nil {
return nil
}
out := *in
return &out
}This example keeps credentials in memory to show the SDK shape. Production providers should persist credentials durably, encrypt at rest, preserve created and updated timestamps, and implement refresh or exchange behavior that matches the supported connection auth modes.
What to read next
For the full server and provider config structure, read
Configuration. For the exact providers.externalCredentials
reference, continue to Config File. To understand
app connection declarations, read Apps and
Provider Manifests. To understand agent
connection bindings, read Agent.