Authentication Methods

9 minute read

Authentication Overview

Most APIs require authentication. Your provider must support the authentication methods your API offers while following Terraform security best practices.

In this chapter, you’ll learn how to implement various authentication strategies in your provider.

Common Authentication Methods

1. API Key

  • Simple, widely used
  • Single credential
  • Usually passed as header or query parameter

2. API Key + Secret

  • Two-part authentication
  • More secure than single key
  • Common in cloud providers

3. Username + Password

  • Basic HTTP authentication
  • Legacy systems
  • Less secure, avoid when possible

4. Bearer Tokens

  • OAuth 2.0, JWT
  • Time-limited
  • Stateless authentication

5. Certificate-Based

  • mTLS (mutual TLS)
  • Client certificates
  • Highest security

API Key Authentication

Schema

func (p *MyCloudProvider) 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,
                MarkdownDescription: "API key for authentication. Can be set via `MYCLOUD_API_KEY` environment variable.",
            },
        },
    }
}

Model

type MyCloudProviderModel struct {
    APIKey types.String `tfsdk:"api_key"`
}

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    diags := req.Config.Get(ctx, &config)
    resp.Diagnostics.Append(diags...)
    
    // Get API key from config or environment
    apiKey := config.APIKey.ValueString()
    if apiKey == "" {
        apiKey = os.Getenv("MYCLOUD_API_KEY")
    }
    
    if apiKey == "" {
        resp.Diagnostics.AddError(
            "Missing API Key",
            "API key must be provided via api_key attribute or MYCLOUD_API_KEY environment variable",
        )
        return
    }
    
    // Create HTTP client with API key
    httpClient := &http.Client{
        Timeout: 30 * time.Second,
        Transport: &apiKeyTransport{
            APIKey:    apiKey,
            Transport: http.DefaultTransport,
        },
    }
    
    client := api.NewClient(httpClient)
    
    // Share with resources
    resp.ResourceData = client
    resp.DataSourceData = client
}

// Custom transport that adds API key header
type apiKeyTransport struct {
    APIKey    string
    Transport http.RoundTripper
}

func (t *apiKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    req.Header.Set("X-API-Key", t.APIKey)
    return t.Transport.RoundTrip(req)
}

Usage

# Option 1: Explicit API key
provider "mycloud" {
  api_key = "sk-abc123..."
}

# Option 2: From variable
variable "mycloud_api_key" {
  type      = string
  sensitive = true
}

provider "mycloud" {
  api_key = var.mycloud_api_key
}

# Option 3: From environment
# export MYCLOUD_API_KEY="sk-abc123..."
provider "mycloud" {
  # Reads from environment
}

API Key + Secret Authentication

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "api_key": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "API key. Can be set via `MYCLOUD_API_KEY`.",
        },
        "api_secret": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "API secret. Can be set via `MYCLOUD_API_SECRET`.",
        },
    },
}

Model

type MyCloudProviderModel struct {
    APIKey    types.String `tfsdk:"api_key"`
    APISecret types.String `tfsdk:"api_secret"`
}

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Get credentials from config or environment
    apiKey := config.APIKey.ValueString()
    if apiKey == "" {
        apiKey = os.Getenv("MYCLOUD_API_KEY")
    }
    
    apiSecret := config.APISecret.ValueString()
    if apiSecret == "" {
        apiSecret = os.Getenv("MYCLOUD_API_SECRET")
    }
    
    if apiKey == "" || apiSecret == "" {
        resp.Diagnostics.AddError(
            "Missing Credentials",
            "Both api_key and api_secret must be provided",
        )
        return
    }
    
    // Create client with key + secret
    httpClient := &http.Client{
        Transport: &hmacTransport{
            APIKey:    apiKey,
            APISecret: apiSecret,
            Transport: http.DefaultTransport,
        },
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

// HMAC signing transport
type hmacTransport struct {
    APIKey    string
    APISecret string
    Transport http.RoundTripper
}

func (t *hmacTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // Sign request with HMAC
    timestamp := time.Now().Unix()
    message := fmt.Sprintf("%s\n%s\n%d", req.Method, req.URL.Path, timestamp)
    
    h := hmac.New(sha256.New, []byte(t.APISecret))
    h.Write([]byte(message))
    signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
    
    req.Header.Set("X-API-Key", t.APIKey)
    req.Header.Set("X-Signature", signature)
    req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
    
    return t.Transport.RoundTrip(req)
}

Usage

provider "mycloud" {
  api_key    = var.api_key
  api_secret = var.api_secret
}

Username + Password Authentication

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "username": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Username for authentication. Can be set via `MYCLOUD_USERNAME`.",
        },
        "password": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "Password for authentication. Can be set via `MYCLOUD_PASSWORD`.",
        },
    },
}

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    username := config.Username.ValueString()
    if username == "" {
        username = os.Getenv("MYCLOUD_USERNAME")
    }
    
    password := config.Password.ValueString()
    if password == "" {
        password = os.Getenv("MYCLOUD_PASSWORD")
    }
    
    if username == "" || password == "" {
        resp.Diagnostics.AddError(
            "Missing Credentials",
            "Both username and password must be provided",
        )
        return
    }
    
    // Create client with basic auth
    httpClient := &http.Client{
        Transport: &basicAuthTransport{
            Username:  username,
            Password:  password,
            Transport: http.DefaultTransport,
        },
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

type basicAuthTransport struct {
    Username  string
    Password  string
    Transport http.RoundTripper
}

func (t *basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    req.SetBasicAuth(t.Username, t.Password)
    return t.Transport.RoundTrip(req)
}

Bearer Token Authentication

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "token": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "Bearer token for authentication. Can be set via `MYCLOUD_TOKEN`.",
        },
    },
}

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    token := config.Token.ValueString()
    if token == "" {
        token = os.Getenv("MYCLOUD_TOKEN")
    }
    
    if token == "" {
        resp.Diagnostics.AddError(
            "Missing Token",
            "Token must be provided via token attribute or MYCLOUD_TOKEN environment variable",
        )
        return
    }
    
    httpClient := &http.Client{
        Transport: &bearerTokenTransport{
            Token:     token,
            Transport: http.DefaultTransport,
        },
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

type bearerTokenTransport struct {
    Token     string
    Transport http.RoundTripper
}

func (t *bearerTokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    req.Header.Set("Authorization", "Bearer "+t.Token)
    return t.Transport.RoundTrip(req)
}

Certificate-Based Authentication

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "client_cert": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Path to client certificate file",
        },
        "client_key": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "Path to client key file",
        },
        "ca_cert": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Path to CA certificate file",
        },
    },
}

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    clientCertPath := config.ClientCert.ValueString()
    clientKeyPath := config.ClientKey.ValueString()
    caCertPath := config.CACert.ValueString()
    
    if clientCertPath == "" || clientKeyPath == "" {
        resp.Diagnostics.AddError(
            "Missing Certificate Configuration",
            "Both client_cert and client_key must be provided",
        )
        return
    }
    
    // Load client certificate and key
    cert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
    if err != nil {
        resp.Diagnostics.AddError(
            "Failed to Load Certificate",
            fmt.Sprintf("Could not load client certificate: %s", err.Error()),
        )
        return
    }
    
    // Load CA certificate if provided
    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{cert},
    }
    
    if caCertPath != "" {
        caCert, err := os.ReadFile(caCertPath)
        if err != nil {
            resp.Diagnostics.AddError(
                "Failed to Load CA Certificate",
                fmt.Sprintf("Could not load CA certificate: %s", err.Error()),
            )
            return
        }
        
        caCertPool := x509.NewCertPool()
        caCertPool.AppendCertsFromPEM(caCert)
        tlsConfig.RootCAs = caCertPool
    }
    
    // Create HTTP client with mTLS
    httpClient := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: tlsConfig,
        },
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

Usage

provider "mycloud" {
  client_cert = "/path/to/client.crt"
  client_key  = "/path/to/client.key"
  ca_cert     = "/path/to/ca.crt"
}

Multiple Authentication Methods

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        // Method 1: API Key
        "api_key": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "API key for authentication",
        },
        
        // Method 2: Username + Password
        "username": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Username for authentication",
        },
        "password": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "Password for authentication",
        },
        
        // Method 3: Token
        "token": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "Bearer token for authentication",
        },
    },
}

Implementation with Validation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Determine which authentication method to use
    hasAPIKey := !config.APIKey.IsNull() && config.APIKey.ValueString() != ""
    hasUserPass := !config.Username.IsNull() && !config.Password.IsNull()
    hasToken := !config.Token.IsNull() && config.Token.ValueString() != ""
    
    // Count how many methods provided
    methodCount := 0
    if hasAPIKey {
        methodCount++
    }
    if hasUserPass {
        methodCount++
    }
    if hasToken {
        methodCount++
    }
    
    // Validate: exactly one method must be provided
    if methodCount == 0 {
        resp.Diagnostics.AddError(
            "Missing Authentication",
            "One of the following must be provided: api_key, username+password, or token",
        )
        return
    }
    
    if methodCount > 1 {
        resp.Diagnostics.AddError(
            "Multiple Authentication Methods",
            "Only one authentication method can be used at a time",
        )
        return
    }
    
    var httpClient *http.Client
    
    // Configure based on method
    if hasAPIKey {
        httpClient = createAPIKeyClient(config.APIKey.ValueString())
    } else if hasUserPass {
        httpClient = createBasicAuthClient(
            config.Username.ValueString(),
            config.Password.ValueString(),
        )
    } else if hasToken {
        httpClient = createBearerTokenClient(config.Token.ValueString())
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

Usage

# Method 1: API Key
provider "mycloud" {
  api_key = var.api_key
}

# Method 2: Username/Password
provider "mycloud" {
  username = "admin"
  password = var.password
}

# Method 3: Token
provider "mycloud" {
  token = var.token
}

OAuth 2.0 Flow

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "client_id": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "OAuth client ID",
        },
        "client_secret": schema.StringAttribute{
            Optional:            true,
            Sensitive:           true,
            MarkdownDescription: "OAuth client secret",
        },
        "token_url": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "OAuth token URL",
        },
    },
}

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    clientID := config.ClientID.ValueString()
    clientSecret := config.ClientSecret.ValueString()
    tokenURL := config.TokenURL.ValueString()
    
    if clientID == "" || clientSecret == "" {
        resp.Diagnostics.AddError(
            "Missing OAuth Configuration",
            "client_id and client_secret must be provided",
        )
        return
    }
    
    if tokenURL == "" {
        tokenURL = "https://auth.mycloud.com/oauth/token"
    }
    
    // Create OAuth2 config
    oauthConfig := &oauth2.Config{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        Endpoint: oauth2.Endpoint{
            TokenURL: tokenURL,
        },
    }
    
    // Get token using client credentials flow
    token, err := oauthConfig.Token(ctx)
    if err != nil {
        resp.Diagnostics.AddError(
            "OAuth Authentication Failed",
            fmt.Sprintf("Failed to obtain OAuth token: %s", err.Error()),
        )
        return
    }
    
    // Create HTTP client with OAuth token
    httpClient := oauthConfig.Client(ctx, token)
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

Testing Authentication

Connection Test

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    // ... authentication setup ...
    
    client := api.NewClient(httpClient)
    
    // Test connection
    if err := client.TestConnection(ctx); err != nil {
        resp.Diagnostics.AddError(
            "Authentication Failed",
            fmt.Sprintf("Could not authenticate with API: %s", err.Error()),
        )
        return
    }
    
    resp.ResourceData = client
}

Best Practices

1. Support Environment Variables

// ✅ Good: Check config, then environment
apiKey := config.APIKey.ValueString()
if apiKey == "" {
    apiKey = os.Getenv("MYCLOUD_API_KEY")
}

2. Mark Credentials as Sensitive

// ✅ Always mark credentials as sensitive
"api_key": schema.StringAttribute{
    Optional:  true,
    Sensitive: true,  // Hidden from logs
}

3. Validate Authentication Method

// ✅ Ensure exactly one method provided
if methodCount == 0 {
    resp.Diagnostics.AddError("Missing Authentication", ...)
}
if methodCount > 1 {
    resp.Diagnostics.AddError("Multiple Authentication Methods", ...)
}

4. Test Authentication Early

// ✅ Test connection during Configure
if err := client.TestConnection(ctx); err != nil {
    resp.Diagnostics.AddError("Authentication Failed", ...)
}

5. Provide Clear Error Messages

// ✅ Good: Specific error message
resp.Diagnostics.AddError(
    "Missing API Key",
    "API key must be provided via api_key attribute or MYCLOUD_API_KEY environment variable",
)

// ❌ Bad: Vague error
resp.Diagnostics.AddError("Error", "Authentication failed")

6. Document All Methods

// ✅ Document in schema
"api_key": schema.StringAttribute{
    Optional:            true,
    Sensitive:           true,
    MarkdownDescription: "API key for authentication. Can be set via `MYCLOUD_API_KEY` environment variable. Mutually exclusive with username/password.",
}

Security Considerations

Don’t Log Credentials

// ❌ Never log credentials
tflog.Debug(ctx, fmt.Sprintf("API Key: %s", apiKey))

// ✅ Log without credentials
tflog.Debug(ctx, "Configuring API client")

Use Secure Storage

# ✅ Use variables, not hardcoded
variable "api_key" {
  type      = string
  sensitive = true
}

provider "mycloud" {
  api_key = var.api_key
}

# ❌ Don't hardcode credentials
provider "mycloud" {
  api_key = "sk-abc123..."  # Bad!
}

Validate Credentials Format

// ✅ Validate format
"api_key": schema.StringAttribute{
    Optional: true,
    Sensitive: true,
    Validators: []validator.String{
        stringvalidator.RegexMatches(
            regexp.MustCompile(`^sk-[a-zA-Z0-9]{32}$`),
            "API key must start with 'sk-' followed by 32 alphanumeric characters",
        ),
    },
}

Key Takeaways

  • ✅ Support multiple authentication methods when API offers them
  • ✅ Always mark credentials as sensitive
  • ✅ Support environment variables for all credentials
  • ✅ Validate that exactly one method is provided
  • ✅ Test authentication during Configure
  • ✅ Provide clear error messages
  • ✅ Never log credentials
  • ✅ Document all authentication options
  • ✅ Use custom HTTP transports for auth headers

What’s Next?

Now let’s learn about configuring HTTP clients with timeouts and retry logic.

Next: Client Configuration →


Previous: ← Provider Schema Design

Last updated: June 30, 2026

Updated: