Virtual Event
Join us for the next HashiConf Digital October 12-15, 2020 Register for Free

Call APIs with Terraform Providers

Add Authentication to a Provider

In this tutorial, you will add authentication to a Terraform provider that interacts with the API of a fictional coffee-shop application, HashiCups, and access protected API endpoints. To do this, you will:

  1. Update provider schema.
    You will add username and password parameters to your provider schema. In addition, you will set the ConfigureContextFunc property to the the name of the function that will configure your provider, providerConfigure.
  2. Define providerConfigure
    This function actually configures your provider. If both the username and password are not empty, the function returns an authenticated API client; else, the function returns an unauthenticated API client. The authenticated API client is able to access protected endpoints, the unauthenticated one is not.

»Prerequisites

To follow this tutorial, you need:

Navigate to your terraform-provider-hashicups directory. Then, checkout the implement-read branch. This step is optional but recommended to insure that you've accurately completed the previous steps.

$ git checkout implement-read

Your directory should have the following structure.

$ tree -L 2
.
├── Makefile
├── README.md
├── docker_compose
│   ├── conf.json
│   └── docker-compose.yml
├── examples
│   ├── coffee
│   ├── main.tf
├── go.mod
├── go.sum
├── hashicups
│   ├── data_source_coffee.go
│   └── provider.go
├── main.go
└── vendor

If you’re stuck, refer to the auth-configuration branch to see the changes implemented in this tutorial.

»Update provider schema

HashiCups requires a username and password to generate an JSON web token (JWT) which is used to authenticate against protected endpoints. You will use this user to authenticate to the HashiCups provider to manage your orders.

Create a user on HashiCups named education with the password test123. If you already did this during the provider usage tutorial, you can skip signing up and reuse your previous token.

$ curl -X POST localhost:19090/signup -d '{"username":"education", "password":"test123"}'
{"UserID":1,"Username":"education","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTEwNzgwODUsInVzZXJfaWQiOjIsInVzZXJuYW1lIjoiZWR1Y2F0aW9uIn0.CguceCNILKdjOQ7Gx0u4UAMlOTaH3Dw-fsll2iXDrYU"}

Authenticate to HashiCups. This will return the userID, username, and a JWT token.

$ curl -X POST localhost:19090/signin -d '{"username":"education", "password":"test123"}'
{"UserID":1,"Username":"education","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTEwNzgwODUsInVzZXJfaWQiOjIsInVzZXJuYW1lIjoiZWR1Y2F0aW9uIn0.CguceCNILKdjOQ7Gx0u4UAMlOTaH3Dw-fsll2iXDrYU"}

Set HASHICUPS_TOKEN to the token you retrieved from invoking the /signin endpoint. You will use this later tutorials to verify your HashiCups order has been created, updated and deleted.

$ export HASHICUPS_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTEwNzgwODUsInVzZXJfaWQiOjIsInVzZXJuYW1lIjoiZWR1Y2F0aW9uIn0.CguceCNILKdjOQ7Gx0u4UAMlOTaH3Dw-fsll2iXDrYU

In your hashicups/provider.go file, replace the Provider() function with the code snippet below. This defines the provider schema (username, password) and the ConfigureContextFunc.

// Provider -
func Provider() *schema.Provider {
  return &schema.Provider{
    Schema: map[string]*schema.Schema{
      "username": &schema.Schema{
        Type:        schema.TypeString,
        Optional:    true,
        DefaultFunc: schema.EnvDefaultFunc("HASHICUPS_USERNAME", nil),
      },
      "password": &schema.Schema{
        Type:        schema.TypeString,
        Optional:    true,
        Sensitive:   true,
        DefaultFunc: schema.EnvDefaultFunc("HASHICUPS_PASSWORD", nil),
      },
    },
    ResourcesMap: map[string]*schema.Resource{},
    DataSourcesMap: map[string]*schema.Resource{
      "hashicups_coffees":     dataSourceCoffees(),
    },
    ConfigureContextFunc: providerConfigure,
  }
}

Notice the DefaultFunc for both the username and password parameters attempts to use the respective environment variables as the default values. This is useful for automated provider testing.

»Define providerConfigure

Import the context, API client and diag libraries into the provider.go file. The providerConfigure function will use these libraries.

import (
+ "context"
  
+ "github.com/hashicorp-demoapp/hashicups-client-go"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
  "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

Then, add the providerConfigure function below your Provider() function. This function retrieves the username and password from the provider schema to authenticate and configure your provider.

func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
  username := d.Get("username").(string)
  password := d.Get("password").(string)
  
  // Warning or errors can be collected in a slice type
  var diags diag.Diagnostics
  
  if (username != "") && (password != "") {
    c, err := hashicups.NewClient(nil, &username, &password)
    if err != nil {
      return nil, diag.FromErr(err)
    }
    
    return c, diags
  }
  
  c, err := hashicups.NewClient(nil, nil, nil)
  if err != nil {
    return nil, diag.FromErr(err)
  }
  
  return c, diags
}

Notice that the function is able to retrieve the username and password values from the *schema.ResourceData. The HashiCups API client is a simple API wrapper for the HashiCups API.

By returning the HashiCups API client, the provider will be able to access the API client as a meta input parameter. In the Implement Complex Read tutorial, you will use the meta input parameter to access a protected endpoint, /orders.

Save your provider.go file, then run go mod vendor to download the API client library into your /vendor directory.

$ go mod vendor

»Test the provider

Now that you implemented authentication, verify that it works.

First, navigate to the terraform-provider-hashicups root directory.

Then, build the binary and move it into your user Terraform plugins directory. This allows you to sideload and test your custom providers.

$ make install
go build -o terraform-provider-hashicups
mv terraform-provider-hashicups ~/.terraform.d/plugins/hashicorp.com/edu/hashicups

Navigate to the examples directory. This contains a sample Terraform configuration for the Terraform HashiCups provider.

$ cd examples

Then, authenticate your provider. You can either set them via environment variables (recommended) or update your provider block.

Set HASHICUPS_USERNAME and HASHICUPS_PASSWORD to education and test123 respectively.

$ export HASHICUPS_USERNAME=education
$ export HASHICUPS_PASSWORD=test123

Finally, initialize your workspace to refresh your HashiCups provider, then apply.

$ terraform init && terraform apply --auto-approve
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Check the terminal containing your HashiCups logs for the recorded operations invoked by the HashiCups provider.

api_1  | 2020-07-22T09:26:23.349Z [INFO]  Handle User | signin
api_1  | 2020-07-22T09:26:23.357Z [INFO]  Handle Coffee
api_1  | 2020-07-22T09:26:23.488Z [INFO]  Handle User | signin
api_1  | 2020-07-22T09:26:23.606Z [INFO]  Handle User | signin

The provider should have invoked a request to the signin endpoint.

»Next steps

Congratulations! You have added authentication to your HashiCups provider. If you were stuck during this tutorial, checkout the auth-configuration branch to see the changes implemented in this tutorial.