Add user:pass Basic auth mode for API-key connectors

The API-key connection transport could present a key as a Bearer token,
an x-api-key header, a custom scheme (SSWS/Token), or HTTP Basic with an
empty password (Cursor). None of these can carry a real password, which
providers such as ClickHouse Cloud (keyId:keySecret) and Langfuse
(publicKey:secretKey) require.

Add a fourth mode, APIKeyBasicAuthUserPass, that base64-encodes the
stored "username:password" credential verbatim into Authorization: Basic.
SetBasicAuth cannot express this -- it re-appends a ":" and corrupts the
credential. The mode is wired generically through the registry and the
create-connector resolver and is mutually exclusive with the other
API-key auth modes.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-06-12 19:52:58 +02:00
parent 897ca031e2
commit bd6a470d6d
6 changed files with 125 additions and 9 deletions

View File

@@ -16,6 +16,7 @@ package connector
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
@@ -41,6 +42,15 @@ type APIKeyConnection struct {
// It is mutually exclusive with Header and is populated from the
// provider Registration at connector creation time.
BasicAuth bool `json:"basic_auth,omitempty"`
// BasicAuthUserPass, when true, presents the API key as a complete HTTP
// Basic credential (`Authorization: Basic base64(<key>)`), with the
// stored key already holding the `username:password` pair — required
// by providers such as ClickHouse Cloud (keyId:keySecret) and
// Langfuse (publicKey:secretKey) whose Basic credential carries a real
// password, which BasicAuth (empty password) cannot express. It is
// mutually exclusive with the other modes and is populated from the
// provider Registration at connector creation time.
BasicAuthUserPass bool `json:"basic_auth_user_pass,omitempty"`
// Scheme selects a non-Bearer Authorization scheme: when non-empty
// the key is sent as `Authorization: <Scheme> <key>` instead of
// `Authorization: Bearer <key>` — required by providers such as Okta
@@ -72,6 +82,15 @@ func (c *APIKeyConnection) Client(ctx context.Context) (*http.Client, error) {
}, nil
}
if c.BasicAuthUserPass {
return &http.Client{
Transport: &basicAuthUserPassTransport{
credential: c.APIKey,
underlying: underlying,
},
}, nil
}
if c.Header != "" {
return &http.Client{
Transport: &apiKeyHeaderTransport{
@@ -153,6 +172,25 @@ func (t *basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error
return t.underlying.RoundTrip(req2)
}
// basicAuthUserPassTransport presents a complete HTTP Basic credential whose
// `username:password` pair is already encoded in the stored key
// (`Authorization: Basic base64(<credential>)`). Providers such as
// ClickHouse Cloud (keyId:keySecret) and Langfuse (publicKey:secretKey)
// authenticate with a real password, which basicAuthTransport's empty
// password cannot carry; SetBasicAuth would also re-append a ":" and
// corrupt the credential, so the value is base64-encoded verbatim.
type basicAuthUserPassTransport struct {
credential string
underlying http.RoundTripper
}
func (t *basicAuthUserPassTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := req.Clone(req.Context())
req2.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(t.credential)))
return t.underlying.RoundTrip(req2)
}
func (c APIKeyConnection) MarshalJSON() ([]byte, error) {
type Alias APIKeyConnection