Authentication Patterns

7 minute read

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.

Next: Error Handling →


Previous: ← HTTP Client Configuration

Last updated: July 2, 2026

Updated: