Authentication Patterns
Authentication Overview
Most APIs require authentication. Your provider must handle authentication transparently, securely storing credentials and adding them to every request.
API Key Authentication
Simple API Key Header
package client
type Client struct {
httpClient *http.Client
apiKey string
baseURL string
}
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
url := c.baseURL + path
var bodyReader io.Reader
if body != nil {
jsonBody, _ := json.Marshal(body)
bodyReader = bytes.NewReader(jsonBody)
}
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
if err != nil {
return nil, err
}
// Add API key to header
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
return c.httpClient.Do(req)
}
API Key in Query Parameter
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
// Parse URL
u, err := url.Parse(c.baseURL + path)
if err != nil {
return nil, err
}
// Add API key to query parameters
q := u.Query()
q.Set("api_key", c.apiKey)
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, method, u.String(), nil)
if err != nil {
return nil, err
}
return c.httpClient.Do(req)
}
Custom Header Name
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
req, _ := http.NewRequestWithContext(ctx, method, url, bodyReader)
// API-specific header name
req.Header.Set("X-API-Key", c.apiKey)
req.Header.Set("X-API-Secret", c.apiSecret)
return c.httpClient.Do(req)
}
Bearer Token Authentication
Static Token
type Client struct {
httpClient *http.Client
token string
baseURL string
}
func NewClient(token, baseURL string) (*Client, error) {
if token == "" {
return nil, fmt.Errorf("token is required")
}
return &Client{
httpClient: &http.Client{Timeout: 30 * time.Second},
token: token,
baseURL: baseURL,
}, nil
}
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
req, _ := http.NewRequestWithContext(ctx, method, url, bodyReader)
// Bearer token
req.Header.Set("Authorization", "Bearer "+c.token)
return c.httpClient.Do(req)
}
Token Refresh
type Client struct {
httpClient *http.Client
token string
refreshToken string
tokenExpiry time.Time
mutex sync.RWMutex
}
func (c *Client) getValidToken(ctx context.Context) (string, error) {
c.mutex.RLock()
// Check if token is still valid
if time.Now().Before(c.tokenExpiry) {
token := c.token
c.mutex.RUnlock()
return token, nil
}
c.mutex.RUnlock()
// Need to refresh
return c.refreshAccessToken(ctx)
}
func (c *Client) refreshAccessToken(ctx context.Context) (string, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
// Double-check after acquiring lock
if time.Now().Before(c.tokenExpiry) {
return c.token, nil
}
// Refresh token
body := map[string]string{
"grant_type": "refresh_token",
"refresh_token": c.refreshToken,
}
resp, err := c.post(ctx, "/oauth/token", body, nil)
if err != nil {
return "", fmt.Errorf("failed to refresh token: %w", err)
}
var tokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
json.Unmarshal(resp, &tokenResp)
// Update tokens
c.token = tokenResp.AccessToken
c.refreshToken = tokenResp.RefreshToken
c.tokenExpiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
return c.token, nil
}
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
// Get valid token (refreshes if needed)
token, err := c.getValidToken(ctx)
if err != nil {
return nil, err
}
req, _ := http.NewRequestWithContext(ctx, method, url, bodyReader)
req.Header.Set("Authorization", "Bearer "+token)
return c.httpClient.Do(req)
}
OAuth 2.0 Authentication
Client Credentials Flow
package client
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)
type Client struct {
httpClient *http.Client
baseURL string
}
func NewClient(clientID, clientSecret, tokenURL, baseURL string) (*Client, error) {
config := &clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: tokenURL,
Scopes: []string{"read", "write"},
}
// Create HTTP client with OAuth2 transport
httpClient := config.Client(context.Background())
return &Client{
httpClient: httpClient,
baseURL: baseURL,
}, nil
}
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
req, _ := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
// OAuth2 client automatically adds Authorization header
return c.httpClient.Do(req)
}
Authorization Code Flow (with PKCE)
func NewClient(ctx context.Context, clientID, clientSecret, authURL, tokenURL string) (*Client, error) {
config := &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
RedirectURL: "http://localhost:8080/callback",
Scopes: []string{"read", "write"},
}
// Generate PKCE verifier
verifier := oauth2.GenerateVerifier()
// Get authorization URL
authCodeURL := config.AuthCodeURL(
"state-token",
oauth2.S256ChallengeOption(verifier),
)
fmt.Printf("Visit this URL to authorize:\n%s\n", authCodeURL)
// Wait for authorization code (simplified)
var authCode string
fmt.Print("Enter authorization code: ")
fmt.Scanln(&authCode)
// Exchange code for token
token, err := config.Exchange(
ctx,
authCode,
oauth2.VerifierOption(verifier),
)
if err != nil {
return nil, fmt.Errorf("failed to exchange code: %w", err)
}
// Create HTTP client with token
httpClient := config.Client(ctx, token)
return &Client{
httpClient: httpClient,
}, nil
}
Basic Authentication
Username/Password
type Client struct {
httpClient *http.Client
username string
password string
baseURL string
}
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
req, _ := http.NewRequestWithContext(ctx, method, url, bodyReader)
// Add Basic Auth
req.SetBasicAuth(c.username, c.password)
return c.httpClient.Do(req)
}
JWT Authentication
Self-Signed JWT
import "github.com/golang-jwt/jwt/v5"
type Client struct {
httpClient *http.Client
privateKey *rsa.PrivateKey
issuer string
audience string
}
func (c *Client) generateJWT() (string, error) {
now := time.Now()
claims := jwt.MapClaims{
"iss": c.issuer,
"aud": c.audience,
"iat": now.Unix(),
"exp": now.Add(1 * time.Hour).Unix(),
"sub": "terraform-provider",
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
return token.SignedString(c.privateKey)
}
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
// Generate JWT
jwtToken, err := c.generateJWT()
if err != nil {
return nil, fmt.Errorf("failed to generate JWT: %w", err)
}
req, _ := http.NewRequestWithContext(ctx, method, url, bodyReader)
req.Header.Set("Authorization", "Bearer "+jwtToken)
return c.httpClient.Do(req)
}
Certificate Authentication
Mutual TLS (mTLS)
import (
"crypto/tls"
"crypto/x509"
)
func NewClientWithCertAuth(certFile, keyFile, caFile string) (*Client, error) {
// Load client certificate
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("failed to load client cert: %w", err)
}
// Load CA certificate
caCert, err := os.ReadFile(caFile)
if err != nil {
return nil, fmt.Errorf("failed to read CA cert: %w", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Configure TLS
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
return &Client{
httpClient: &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
},
}, nil
}
AWS Signature V4
Sign Requests
import (
"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
)
type Client struct {
httpClient *http.Client
signer *v4.Signer
credentials aws.Credentials
region string
service string
}
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
var bodyReader io.Reader
var payloadHash string
if body != nil {
jsonBody, _ := json.Marshal(body)
bodyReader = bytes.NewReader(jsonBody)
// Calculate payload hash
h := sha256.New()
h.Write(jsonBody)
payloadHash = hex.EncodeToString(h.Sum(nil))
} else {
payloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // Empty payload
}
req, _ := http.NewRequestWithContext(ctx, method, url, bodyReader)
req.Header.Set("Content-Type", "application/json")
// Sign request
err := c.signer.SignHTTP(
ctx,
c.credentials,
req,
payloadHash,
c.service,
c.region,
time.Now(),
)
if err != nil {
return nil, fmt.Errorf("failed to sign request: %w", err)
}
return c.httpClient.Do(req)
}
Authentication Transport
Reusable Auth Transport
type AuthTransport struct {
Transport http.RoundTripper
APIKey string
}
func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Clone request to avoid modifying original
req = req.Clone(req.Context())
// Add authentication
req.Header.Set("Authorization", "Bearer "+t.APIKey)
// Forward to underlying transport
return t.Transport.RoundTrip(req)
}
func NewClient(apiKey string) (*Client, error) {
baseTransport := &http.Transport{
MaxIdleConns: 100,
}
authTransport := &AuthTransport{
Transport: baseTransport,
APIKey: apiKey,
}
return &Client{
httpClient: &http.Client{
Transport: authTransport,
},
}, nil
}
Provider Configuration
Accept Multiple Auth Methods
// Provider schema
func (p *Provider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"api_key": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "API key for authentication",
},
"username": schema.StringAttribute{
Optional: true,
Description: "Username for basic auth",
},
"password": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "Password for basic auth",
},
"token": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "OAuth token",
},
"client_id": schema.StringAttribute{
Optional: true,
Description: "OAuth client ID",
},
"client_secret": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "OAuth client secret",
},
},
}
}
// Configure method
func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config ProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
var client *client.Client
var err error
// Determine auth method
if !config.APIKey.IsNull() {
client, err = client.NewClientWithAPIKey(config.APIKey.ValueString())
} else if !config.Token.IsNull() {
client, err = client.NewClientWithToken(config.Token.ValueString())
} else if !config.Username.IsNull() && !config.Password.IsNull() {
client, err = client.NewClientWithBasicAuth(
config.Username.ValueString(),
config.Password.ValueString(),
)
} else if !config.ClientID.IsNull() && !config.ClientSecret.IsNull() {
client, err = client.NewClientWithOAuth(
config.ClientID.ValueString(),
config.ClientSecret.ValueString(),
)
} else {
resp.Diagnostics.AddError(
"Missing Authentication",
"One of api_key, token, username/password, or client_id/client_secret must be provided",
)
return
}
if err != nil {
resp.Diagnostics.AddError("Failed to create client", err.Error())
return
}
resp.ResourceData = client
resp.DataSourceData = client
}
Best Practices
1. Store Credentials Securely
// ✅ Good: Sensitive attribute
"api_key": schema.StringAttribute{
Sensitive: true,
Optional: true,
}
// ❌ Bad: Not marked sensitive
"api_key": schema.StringAttribute{
Optional: true,
}
2. Support Environment Variables
func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config ProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
// Use environment variable as fallback
apiKey := config.APIKey.ValueString()
if apiKey == "" {
apiKey = os.Getenv("MYCLOUD_API_KEY")
}
if apiKey == "" {
resp.Diagnostics.AddError(
"Missing API Key",
"Set api_key in provider configuration or MYCLOUD_API_KEY environment variable",
)
return
}
client, err := client.NewClient(apiKey)
// ...
}
3. Validate Credentials Early
func NewClient(apiKey string) (*Client, error) {
client := &Client{
apiKey: apiKey,
httpClient: &http.Client{Timeout: 30 * time.Second},
}
// Validate credentials with test request
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := client.ValidateCredentials(ctx); err != nil {
return nil, fmt.Errorf("invalid credentials: %w", err)
}
return client, nil
}
func (c *Client) ValidateCredentials(ctx context.Context) error {
resp, err := c.get(ctx, "/api/v2/auth/validate", nil)
if err != nil {
return err
}
if resp.StatusCode == 401 {
return fmt.Errorf("invalid API key")
}
return nil
}
Key Takeaways
- ✅ Mark credentials as sensitive
- ✅ Support multiple authentication methods
- ✅ Validate credentials early
- ✅ Use environment variables as fallback
- ✅ Handle token refresh automatically
- ✅ Use standard libraries (OAuth2, JWT)
- ✅ Implement authentication as transport middleware
- ✅ Never log credentials
- ✅ Support mTLS for enterprise customers
- ✅ Document authentication requirements
What’s Next?
Learn how to handle API errors gracefully and create meaningful error messages.
Previous: ← HTTP Client Configuration
Last updated: July 2, 2026