Skip to content

Writing a provider

A provider is an adapter for one external service. It has three parts: a manifest that describes it as data, a validation that proves a credential works and names the account, and a way to authenticate a request.

type Provider interface {
    Manifest() Manifest
    Validate(ctx context.Context, hc *http.Client, m Material) (Result, error)
    Authenticate(r *http.Request, m Material) error
}

The manifest

The manifest is declarative so a user interface can render a connection form and an agent can read what a connection needs. It contains no code.

Field Meaning
Key Identifies the provider, for example cloudflare
DisplayName Shown to people
BaseURLs The only origins a request for this provider may reach
AuthMethods One or more ways to connect: Key, Fields, Scopes, AccountSelection, HelpURL
HelpURL Where a person learns to create the credential

A field is an input: Name, Label, Secret, Required and Help. A scope names a permission the credential should hold, with a description and whether it is required, so an interface can tell a person which permissions to grant.

BaseURLs are part of the security model: they are the egress allow-list. Give a provider with regions or editions one provider per host, or a constructor option that sets the base URL. Never let a stored connection carry its own URL.

A static-token service needs no code

For a service that issues an API key or token, configure the apikey provider:

p, err := apikey.New(apikey.Config{
    Manifest: provider.Manifest{
        Key: "cloudflare", DisplayName: "Cloudflare",
        BaseURLs: []string{"https://api.cloudflare.com"},
        HelpURL:  "https://developers.cloudflare.com/fundamentals/api/get-started/create-token/",
        AuthMethods: []provider.AuthMethod{{
            Key:    "api_token",
            Fields: []provider.Field{{Name: "token", Label: "API token", Secret: true, Required: true}},
            Scopes: []provider.Scope{{Name: "Zone:Read", Required: true}},
        }},
    },
    // Proves the token works.
    Verify: apikey.Request{Path: "/client/v4/user/tokens/verify"},
    // A second request discovers the account, when the first does not name it.
    Account: &apikey.Request{
        Path:             "/client/v4/accounts?per_page=50",
        AccountIDPath:    "result.0.id",
        AccountLabelPath: "result.0.name",
    },
})
Config field Default Meaning
Method api_token The auth method this provider implements
Field token The field that holds the token
Header Authorization The request header that carries it
Prefix Bearer for Authorization, else none Precedes the token in the header
Verify required A GET that validates the token
Account none A second GET that discovers the account

A Request has a Path and dot-separated paths into the JSON response, where a number indexes an array: AccountIDPath, AccountLabelPath, and ScopesPath for an array of strings.

Writing your own

Implement Provider when a service needs more than a header token: a signed request, a two-step handshake, or a different verification.

func (p *myProvider) Validate(ctx context.Context, hc *http.Client, m provider.Material) (provider.Result, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.base+"/me", nil)
    if err != nil {
        return provider.Result{}, err
    }
    if err := p.Authenticate(req, m); err != nil {
        return provider.Result{}, err
    }
    resp, err := hc.Do(req)
    if err != nil {
        return provider.Result{}, fmt.Errorf("%w: %w", cf.ErrTransient, err)
    }
    defer resp.Body.Close()
    if err := provider.Classify(p.Manifest().Key, resp); err != nil {
        return provider.Result{}, err
    }
    // decode resp.Body, return the account
}

Rules that keep a provider safe:

  • Use the client you are given. It is already restricted to the manifest's hosts. Never build your own for a request that carries a credential.
  • Classify with provider.Classify. It maps statuses to the typed errors the service understands and records the status and request ID, never a body.
  • Never put a body, a header or the token in an error. A provider may echo a credential in an error response.
  • Call Reveal only here. Secret.Reveal is for provider adapters and the broker. A test fails if it is called anywhere else in the module; clear the copy you take.
  • Do not make the account discovery depend on a human. If the credential names no account, leave AccountID empty and let the display name identify the connection.

Register it

reg := provider.NewRegistry()
if err := reg.Register(p); err != nil {
    return err // invalid manifest, or the key is already registered
}

reg.Manifests() returns every manifest ordered by key, which is what a connection form lists.

Test it

Run the provider against an httptest server that returns the service's documented response shape. Check the request path and header, the discovered account, and that a revoked token yields ErrInvalidCredential without the token in the error text. Use obviously fake tokens in fixtures.