Authentication Methods
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.
Previous: ← Provider Schema Design
Last updated: June 30, 2026