Custom Identity Providers in Keycloak with Terraform: A Step-by-Step Guide
Tags: keycloak, terraform, idp, provisioner
Published: 2023-04-24 07:48:41

## Introduction

In this blog post, we will be discussing a Terraform code block for creating an custom identity provider (myIdp). 

The [Keycloak Terraform provider](https://registry.terraform.io/providers/mrparkers/keycloak/latest) does not have the resources to configure a custom identity provider or any social provider except Google, which was the main reason to create this blog post.

`Terraform` is an infrastructure-as-code tool that enables the provisioning of cloud resources using code instead of manual intervention. 

Using the `null resource`. We trigger the creation and deletion of myIdp using a `local-exec` provisioner.

First, let's take a look at the Terraform code block:


```terraform
resource "null_resource" "myidp_identity_provider" {

  triggers = {
    myidp_idp_signature      = filesha256("${path.module}/myidp_idp.json")
    environment            = var.environment
    realm_id               = var.realm_id
    kc_admin_client_id     = var.kc_admin_client_id
    kc_admin_client_secret = var.kc_admin_client_secret
    path_module            = path.module
  }

  lifecycle {
    ignore_changes = [triggers["environment"], triggers["realm_id"], triggers["kc_admin_client_id"], triggers["kc_admin_client_secret"], triggers["path_module"]]
  }

  provisioner "local-exec" {
    command = "${path.module}/scripts/create_myidp_idp.sh ${var.environment} ${var.realm_id} ${var.kc_admin_client_id} ${var.kc_admin_client_secret} ${var.myidp_idp_client_id} ${var.myidp_idp_client_secret} ${path.module}"
  }

  provisioner "local-exec" {
    when    = destroy
    command = "${path.module}/scripts/delete_myidp_idp.sh ${self.triggers.environment} ${self.triggers.realm_id} ${self.triggers.kc_admin_client_id} ${self.triggers.kc_admin_client_secret} ${self.triggers.path_module}"
  }
}
```

## Resource Explanation

`null resource` is a resource that doesn't create any actual infrastructure but can be used to trigger `provisioners`. The resource assigned the name `myidp_identity_provider`.

## Trigger Block

Next, we have a `triggers` block, which specifies that the resource should be triggered when certain conditions are met. In this case, the triggers are the SHA-256 hash of the `myidp_idp.json` file, which contains the configuration for the myidp identity provider, along with other variables such as environment, realm_id, kc_admin_client_id, and kc_admin_client_secret. The "path.module" variable is also included to ensure that the correct path is used as it will be used later to call the creation/deletion scripts.


## Lifecycle Block

After the trigger block, we have a "lifecycle" block that specifies which triggers to ignore when the resource is destroyed. This is important because Terraform can't reference attributes of a resource that's being destroyed. Other than the `myidp_idp_signature` trigger, ignore. The additional triggers were added to be used later in the Destroy-time provisioners as their connection configurations may only reference attributes of therelated resource, via 'self', 'count.index', or 'each.key'. 

## Provisioner Blocks

Finally, we have two "provisioner" blocks that use the `local-exec` provisioner to execute shell scripts. 

- The first provisioner block creates the myidp identity provider using the variables specified in the trigger block. 

- The second provisioner block is only executed when the resource is being destroyed and deletes the myidp identity provider using the triggers stored in the resource state.

### myidp_idp_signature

In the Terraform configuration file, `myidp_idp_signature` is a trigger that is set to the SHA-256 hash of the file located at `${path.module}/myidp_idp.json`. The purpose of this trigger is to signal to Terraform that the null_resource should be recreated if the `myidp_idp.json` file changes.

When Terraform detects a change in the SHA-256 hash of the `myidp_idp.json` file, it will destroy and recreate the null_resource. This is important because the null_resource is used to execute the `create_myidp_idp.sh` script, which generates an Identity Provider (IDP) in Keycloak based on the contents of the myidp_idp.json file.

By using the `myidp_idp_signature` trigger, we ensure that the `null_resource` is always recreated when the `myidp_idp.json` file changes, which guarantees that the IDP in Keycloak is always up-to-date with the latest version of the `myidp_idp.json` file.


### Scripts 
the scripts used in the provisioner blocks handle the authentication and creation/deletion of the myidp identity provider by using the Keycloak admin API. By separating the functionality into separate scripts, it makes the code more modular and easier to maintain.

There are three scripts used in the provisioner blocks: `get_kc_admin_token.sh`, `create_myidp_idp.sh` and `delete_myidp_idp.sh`. let's take a look at each:

## Get Keycloak Admin API Token

```bash
#!/usr/bin/env bash

environment=$1
kc_admin_client_id=$2
kc_admin_client_secret=$3
kc_url='https://keycloak-host.com'
kc_token_url="${kc_url}"'/realms/master/protocol/openid-connect/token'

# Get Access Token
response=$(curl -s -w "\n%{http_code}" "${kc_token_url}" \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'client_id='"${kc_admin_client_id}"'' \
  --data-urlencode 'client_secret='"${kc_admin_client_secret}"'' \
  --data-urlencode 'grant_type=client_credentials')

response_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | sed '$d')

#  Check the response code
if [[ $response_code -eq 200 ]]; then
  #  Extract access token
  access_token=$(echo "$response" | jq -r '.access_token')
  #  Return the access_token
  echo "$access_token"
  exit 0
else
  echo "Error: The server returned a response code of $response_code and response body: $response_body"
  exit 1
fi

```


The `get_kc_admin_token.sh` script is used to authenticate with the Keycloak admin API to obtain an access token. This script is called by both "create_myidp_idp.sh" and `delete_myidp_idp.sh` to ensure that the user has the necessary permissions to create or delete the myIdp identity provider. The script takes in the Keycloak admin URL, realm, client ID, and client secret as arguments and returns the access token as output.

#### Create myIdp Identity Provider 

```bash
#!/usr/bin/env bash

environment=$1
realm_id=$2
kc_admin_client_id=$3
kc_admin_client_secret=$4
myidp_idp_client_id=$5
myidp_idp_client_secret=$6
path_module=$7
kc_url='https://keycloak-host.com'
kc_create_idp_url="${kc_url}"'/admin/realms/'"${realm_id}"'/identity-provider/instances'

# Get Access Token
access_token=$(source "${path_module}"/scripts/get_kc_admin_token.sh "${environment}" "${kc_admin_client_id}" "${kc_admin_client_secret}")

# Evaluate the myidp_idp.json file
myidp_idp_data=$(cat "${path_module}"/myidp_idp.json | sed "s/\${myidp_idp_client_id}/${myidp_idp_client_id}/g" | sed "s/\${myidp_idp_client_secret}/${myidp_idp_client_secret}/g")

# Make the create request and store the response code and body in variables
response=$(curl -s -w "\n%{http_code}" "$kc_create_idp_url" \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer '"$access_token"'' \
  --data "$myidp_idp_data")

response_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | sed '$d')

# Check the response code
if [[ $response_code -eq 201 ]]; then
  echo "Success: myidp Idp created"
  exit 0
else
  echo "Error: The server returned a response code of $response_code and response body: $response_body"
  exit 1
fi

```

The `create_myidp_idp.sh` script is used to create the myIdp identity provider. It takes in the environment, realm ID, Keycloak admin client ID and secret, myIdp identity provider client ID and secret, and the path to the module as arguments. The script first calls `get_kc_admin_token.sh` to authenticate with the Keycloak admin API and obtain an access token. It then uses the access token to make an HTTP POST request to the Keycloak admin API to create the myidp identity provider.

#### Delete myIdp Identity Provider 

```bash
#!/usr/bin/env bash

environment=$1
realm_id=$2
kc_admin_client_id=$3
kc_admin_client_secret=$4
path_module=$5
kc_url='https://keycloak-host.com'
kc_delete_idp_url="${kc_url}"'/admin/realms/'"${realm_id}"'/identity-provider/instances/myidp'

# Get Access Token
access_token=$(source "${path_module}"/scripts/get_kc_admin_token.sh "${environment}" "${kc_admin_client_id}" "${kc_admin_client_secret}")

# Make the create request and store the response code and body in variables
response=$(curl -s --request DELETE -w "\n%{http_code}" "${kc_delete_idp_url}" \
  --header 'Authorization: Bearer '"$access_token"'')

response_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | sed '$d')

# Check the response code
if [[ $response_code -eq 204 ]]; then
  echo "Success: myidp Idp deleted"
  exit 0
elif [[ $response_code -eq 404 ]]; then
  echo "Success: myidp Idp is not exist"
  exit 0
else
  echo "Error: The server returned a response code of $response_code and response body: $response_body"
  exit 1
fi
```

The `delete_myidp_idp.sh` script is used to delete the myidp identity provider. It takes in the environment, realm ID, Keycloak admin client ID and secret, and the path to the module as arguments. The script again calls `get_kc_admin_token.sh` to authenticate with the Keycloak admin API and obtain an access token. It then uses the access token to make an HTTP DELETE request to the Keycloak admin API to delete the myIdp identity provider.

## Conclusion

In conclusion, this Terraform code block demonstrates how to use a null resource with triggers and local-exec provisioners to create and delete a custom identity provider. By using Terraform, we can ensure that our infrastructure is consistent, repeatable, and auditable, making it easier to manage and maintain over time.