Skip to content

Commit

Permalink
Add mTLS support for keycloak client
Browse files Browse the repository at this point in the history
  • Loading branch information
lucdew committed Dec 21, 2024
1 parent b35eb9f commit 82c19d1
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 20 deletions.
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,7 @@ The following arguments are supported:
- `client_timeout` - (Optional) Sets the timeout of the client when addressing Keycloak, in seconds. Defaults to the environment variable `KEYCLOAK_CLIENT_TIMEOUT`, or `15` if the environment variable is not specified.
- `tls_insecure_skip_verify` - (Optional) Allows ignoring insecure certificates when set to `true`. Defaults to `false`. Disabling this security check is dangerous and should only be done in local or test environments.
- `root_ca_certificate` - (Optional) Allows x509 calls using an unknown CA certificate (for development purposes)
- `tls_client_certificate` - (Optional) The TLS client certificate in PEM format when the keycloak server is configured with TLS mutual authentication.
- `tls_client_private_key` - (Optional) The TLS client pkcs1 private key in PEM format when the keycloak server is configured with TLS mutual authentication.
- `base_path` - (Optional) The base path used for accessing the Keycloak REST API. Defaults to the environment variable `KEYCLOAK_BASE_PATH`, or an empty string if the environment variable is not specified. Note that users of the legacy distribution of Keycloak will need to set this attribute to `/auth`.
- `additional_headers` - (Optional) A map of custom HTTP headers to add to each request to the Keycloak API.
18 changes: 13 additions & 5 deletions keycloak/keycloak_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"crypto/x509"
"encoding/json"
"fmt"
"github.com/hashicorp/terraform-plugin-log/tflog"
"io/ioutil"
"net/http"
"net/http/cookiejar"
Expand All @@ -17,6 +16,8 @@ import (
"strings"
"time"

"github.com/hashicorp/terraform-plugin-log/tflog"

"github.com/hashicorp/go-version"

"golang.org/x/net/publicsuffix"
Expand Down Expand Up @@ -60,7 +61,7 @@ var redHatSSO7VersionMap = map[int]string{
4: "9.0.17",
}

func NewKeycloakClient(ctx context.Context, url, basePath, clientId, clientSecret, realm, username, password string, initialLogin bool, clientTimeout int, caCert string, tlsInsecureSkipVerify bool, userAgent string, redHatSSO bool, additionalHeaders map[string]string) (*KeycloakClient, error) {
func NewKeycloakClient(ctx context.Context, url, basePath, clientId, clientSecret, realm, username, password string, initialLogin bool, clientTimeout int, caCert string, tlsClientCert string, tlsClientPrivateKey string, tlsInsecureSkipVerify bool, userAgent string, redHatSSO bool, additionalHeaders map[string]string) (*KeycloakClient, error) {
clientCredentials := &ClientCredentials{
ClientId: clientId,
ClientSecret: clientSecret,
Expand All @@ -79,7 +80,7 @@ func NewKeycloakClient(ctx context.Context, url, basePath, clientId, clientSecre
}
}

httpClient, err := newHttpClient(tlsInsecureSkipVerify, clientTimeout, caCert)
httpClient, err := newHttpClient(tlsInsecureSkipVerify, clientTimeout, caCert, tlsClientCert, tlsClientPrivateKey)
if err != nil {
return nil, fmt.Errorf("failed to create http client: %v", err)
}
Expand Down Expand Up @@ -170,7 +171,6 @@ func (keycloakClient *KeycloakClient) login(ctx context.Context) error {
serverVersion = strings.ReplaceAll(info.SystemInfo.ServerVersion, ".GA", "")
} else {
regex, err := regexp.Compile(`\.redhat-\w+`)

if err != nil {
fmt.Println("Error compiling regex:", err)
return err
Expand Down Expand Up @@ -499,7 +499,7 @@ func (keycloakClient *KeycloakClient) marshal(body interface{}) ([]byte, error)
return json.Marshal(body)
}

func newHttpClient(tlsInsecureSkipVerify bool, clientTimeout int, caCert string) (*http.Client, error) {
func newHttpClient(tlsInsecureSkipVerify bool, clientTimeout int, caCert string, tlsClientCert string, tlsClientPrivateKey string) (*http.Client, error) {
cookieJar, err := cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
Expand All @@ -518,6 +518,14 @@ func newHttpClient(tlsInsecureSkipVerify bool, clientTimeout int, caCert string)
transport.TLSClientConfig.RootCAs = caCertPool
}

if tlsClientCert != "" && tlsClientPrivateKey != "" {
clientKeyPairCert, err := tls.X509KeyPair([]byte(tlsClientCert), []byte(tlsClientPrivateKey))
if err != nil {
return nil, err
}
transport.TLSClientConfig.Certificates = []tls.Certificate{clientKeyPairCert}
}

retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 1
retryClient.RetryWaitMin = time.Second * 1
Expand Down
5 changes: 3 additions & 2 deletions keycloak/keycloak_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package keycloak

import (
"context"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"os"
"strconv"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
)

var requiredEnvironmentVariables = []string{
Expand Down Expand Up @@ -48,7 +49,7 @@ func TestAccKeycloakApiClientRefresh(t *testing.T) {
t.Fatal("KEYCLOAK_CLIENT_TIMEOUT must be an integer")
}

keycloakClient, err := NewKeycloakClient(ctx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), os.Getenv("KEYCLOAK_USER"), os.Getenv("KEYCLOAK_PASSWORD"), true, clientTimeout, "", false, "", false, map[string]string{
keycloakClient, err := NewKeycloakClient(ctx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), os.Getenv("KEYCLOAK_USER"), os.Getenv("KEYCLOAK_PASSWORD"), true, clientTimeout, "", "", "", false, "", false, map[string]string{
"foo": "bar",
})
if err != nil {
Expand Down
16 changes: 15 additions & 1 deletion provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
Description: "Allows ignoring insecure certificates when set to true. Defaults to false. Disabling security check is dangerous and should be avoided.",
Default: false,
},
"tls_client_certificate": {
Optional: true,
Type: schema.TypeString,
Description: "TLS client certificate as PEM string for mutual authentication",
Default: "",
},
"tls_client_private_key": {
Optional: true,
Type: schema.TypeString,
Description: "TLS client private key as PEM string for mutual authentication",
Default: "",
},
"red_hat_sso": {
Optional: true,
Type: schema.TypeBool,
Expand Down Expand Up @@ -210,6 +222,8 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
initialLogin := data.Get("initial_login").(bool)
clientTimeout := data.Get("client_timeout").(int)
tlsInsecureSkipVerify := data.Get("tls_insecure_skip_verify").(bool)
tlsClientCertificate := data.Get("tls_client_certificate").(string)
tlsClientPrivateKey := data.Get("tls_client_private_key").(string)
rootCaCertificate := data.Get("root_ca_certificate").(string)
redHatSSO := data.Get("red_hat_sso").(bool)
additionalHeaders := make(map[string]string)
Expand All @@ -221,7 +235,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {

userAgent := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin SDK/%s", provider.TerraformVersion, meta.SDKVersionString())

keycloakClient, err := keycloak.NewKeycloakClient(ctx, url, basePath, clientId, clientSecret, realm, username, password, initialLogin, clientTimeout, rootCaCertificate, tlsInsecureSkipVerify, userAgent, redHatSSO, additionalHeaders)
keycloakClient, err := keycloak.NewKeycloakClient(ctx, url, basePath, clientId, clientSecret, realm, username, password, initialLogin, clientTimeout, rootCaCertificate, tlsClientCertificate, tlsClientPrivateKey, tlsInsecureSkipVerify, userAgent, redHatSSO, additionalHeaders)
if err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Expand Down
27 changes: 15 additions & 12 deletions provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
"github.com/keycloak/terraform-provider-keycloak/keycloak"
"log"
"os"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
"github.com/keycloak/terraform-provider-keycloak/keycloak"
)

var testAccProviderFactories map[string]func() (*schema.Provider, error)
var testAccProvider *schema.Provider
var keycloakClient *keycloak.KeycloakClient
var testAccRealm *keycloak.Realm
var testAccRealmTwo *keycloak.Realm
var testAccRealmUserFederation *keycloak.Realm
var testCtx context.Context
var (
testAccProviderFactories map[string]func() (*schema.Provider, error)
testAccProvider *schema.Provider
keycloakClient *keycloak.KeycloakClient
testAccRealm *keycloak.Realm
testAccRealmTwo *keycloak.Realm
testAccRealmUserFederation *keycloak.Realm
testCtx context.Context
)

var requiredEnvironmentVariables = []string{
"KEYCLOAK_CLIENT_ID",
Expand Down Expand Up @@ -56,7 +59,7 @@ func init() {
}
}

keycloakClient, err = keycloak.NewKeycloakClient(testCtx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), "", "", true, 5, "", false, userAgent, false, map[string]string{
keycloakClient, err = keycloak.NewKeycloakClient(testCtx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), "", "", true, 5, "", "", "", false, userAgent, false, map[string]string{
"foo": "bar",
})
if err != nil {
Expand Down

0 comments on commit 82c19d1

Please sign in to comment.