Repository: mrparkers/terraform-provider-keycloak
Pull request #137 (2019-07-18 08:54:17)

custom config for custom oidc identity provider with test and example
see also: https://github.com/mrparkers/terraform-provider-keycloak/issues/135

tomrutsaert: @mrparkers, Do I still need to do anything for this PR?
mrparkers: Sorry, I forgot to come back to this after merging master.

Patch:
diff --git a/custom-user-federation-example/build.gradle b/custom-user-federation-example/build.gradle
index a48c3b8..d23febb 100644
--- a/custom-user-federation-example/build.gradle
+++ b/custom-user-federation-example/build.gradle
@@ -27,7 +27,9 @@ shadowJar {
 dependencies {
     compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}"
     compile "org.keycloak:keycloak-core:${keycloakVersion}"
+    compile "org.keycloak:keycloak-services:${keycloakVersion}"
     compile "org.keycloak:keycloak-server-spi:${keycloakVersion}"
+    compile "org.keycloak:keycloak-server-spi-private:${keycloakVersion}"
 }
 
 repositories {
diff --git a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProvider.kt b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProvider.kt
new file mode 100644
index 0000000..d26e7b8
--- /dev/null
+++ b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProvider.kt
@@ -0,0 +1,10 @@
+package com.github.mrparkers.keycloak
+
+import org.keycloak.broker.oidc.OIDCIdentityProvider
+import org.keycloak.models.KeycloakSession
+
+class CustomIdentityProvider(session: KeycloakSession, config: CustomIdentityProviderConfig) : OIDCIdentityProvider(session, config) {
+	override fun getConfig(): CustomIdentityProviderConfig {
+		return super.getConfig() as CustomIdentityProviderConfig
+	}
+}
diff --git a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderConfig.kt b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderConfig.kt
new file mode 100644
index 0000000..bf4606d
--- /dev/null
+++ b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderConfig.kt
@@ -0,0 +1,11 @@
+package com.github.mrparkers.keycloak
+
+import org.keycloak.broker.oidc.OIDCIdentityProviderConfig
+import org.keycloak.models.IdentityProviderModel
+
+class CustomIdentityProviderConfig(identityProviderModel: IdentityProviderModel) : OIDCIdentityProviderConfig(identityProviderModel) {
+
+	val dummyConfig: String
+		get() = getConfig().getOrDefault("dummyConfig", "")
+
+}
diff --git a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt
new file mode 100644
index 0000000..e60c4d7
--- /dev/null
+++ b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt
@@ -0,0 +1,46 @@
+package com.github.mrparkers.keycloak
+
+
+import org.keycloak.broker.oidc.OIDCIdentityProviderConfig
+import org.keycloak.broker.provider.AbstractIdentityProviderFactory
+import org.keycloak.models.IdentityProviderModel
+import org.keycloak.models.KeycloakSession
+import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation
+import org.keycloak.util.JsonSerialization
+import java.io.IOException
+import java.io.InputStream
+
+class CustomIdentityProviderFactory : AbstractIdentityProviderFactory<CustomIdentityProvider>() {
+	override fun getName(): String = "custom idp"
+	override fun getId(): String = "customIdp"
+
+	override fun create(session: KeycloakSession, model: IdentityProviderModel): CustomIdentityProvider {
+		return CustomIdentityProvider(session, CustomIdentityProviderConfig(model))
+	}
+
+	override fun parseConfig(session: KeycloakSession, inputStream: InputStream): Map<String, String> {
+		return parseOIDCConfig(session, inputStream)
+	}
+
+	private fun parseOIDCConfig(session: KeycloakSession, inputStream: InputStream): Map<String, String> {
+		val rep: OIDCConfigurationRepresentation
+		try {
+			rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation::class.java)
+		} catch (e: IOException) {
+			throw RuntimeException("failed to load openid connect metadata", e)
+		}
+
+		val config = OIDCIdentityProviderConfig(IdentityProviderModel())
+		config.issuer = rep.issuer
+		config.logoutUrl = rep.logoutEndpoint
+		config.authorizationUrl = rep.authorizationEndpoint
+		config.tokenUrl = rep.tokenEndpoint
+		config.userInfoUrl = rep.userinfoEndpoint
+		if (rep.jwksUri != null) {
+			config.isValidateSignature = true
+			config.isUseJwksUrl = true
+			config.jwksUrl = rep.jwksUri
+		}
+		return config.config
+	}
+}
diff --git a/custom-user-federation-example/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory b/custom-user-federation-example/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory
new file mode 100644
index 0000000..fa9948e
--- /dev/null
+++ b/custom-user-federation-example/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory
@@ -0,0 +1,1 @@
+com.github.mrparkers.keycloak.CustomIdentityProviderFactory
diff --git a/example/main.tf b/example/main.tf
index 3031a4f..ca0d053 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -414,6 +414,19 @@ resource keycloak_oidc_identity_provider oidc {
 	client_secret     = "example_token"
 }
 
+resource keycloak_oidc_identity_provider custom_oidc_idp {
+	realm             = "${keycloak_realm.test.id}"
+	provider_id		  = "customIdp"
+	alias             = "custom"
+	authorization_url = "https://example.com/auth"
+	token_url         = "https://example.com/token"
+	client_id         = "example_id"
+	client_secret     = "example_token"
+	extra_config = {
+		dummyConfig = "dummyValue"
+	}
+}
+
 resource keycloak_attribute_importer_identity_provider_mapper oidc {
 	realm                   = "${keycloak_realm.test.id}"
 	name                    = "attributeImporter"
diff --git a/keycloak/identity_provider.go b/keycloak/identity_provider.go
index 5b6ccd1..da7935b 100644
--- a/keycloak/identity_provider.go
+++ b/keycloak/identity_provider.go
@@ -1,40 +1,45 @@
 package keycloak
 
 import (
+	"encoding/json"
 	"fmt"
 	"log"
+	"reflect"
+	"strconv"
+	"strings"
 )
 
 type IdentityProviderConfig struct {
-	Key                              string             `json:"key,omitempty"`
-	HostIp                           string             `json:"hostIp,omitempty"`
-	UseJwksUrl                       KeycloakBoolQuoted `json:"useJwksUrl,omitempty"`
-	JwksUrl                          string             `json:"jwksUrl,omitempty"`
-	ClientId                         string             `json:"clientId,omitempty"`
-	ClientSecret                     string             `json:"clientSecret,omitempty"`
-	DisableUserInfo                  KeycloakBoolQuoted `json:"disableUserInfo"`
-	UserInfoUrl                      string             `json:"userInfoUrl,omitempty"`
-	HideOnLoginPage                  KeycloakBoolQuoted `json:"hideOnLoginPage"`
-	NameIDPolicyFormat               string             `json:"nameIDPolicyFormat,omitempty"`
-	SingleLogoutServiceUrl           string             `json:"singleLogoutServiceUrl,omitempty"`
-	SingleSignOnServiceUrl           string             `json:"singleSignOnServiceUrl,omitempty"`
-	SigningCertificate               string             `json:"signingCertificate,omitempty"`
-	SignatureAlgorithm               string             `json:"signatureAlgorithm,omitempty"`
-	XmlSignKeyInfoKeyNameTransformer string             `json:"xmlSignKeyInfoKeyNameTransformer,omitempty"`
-	PostBindingAuthnRequest          KeycloakBoolQuoted `json:"postBindingAuthnRequest,omitempty"`
-	PostBindingResponse              KeycloakBoolQuoted `json:"postBindingResponse,omitempty"`
-	PostBindingLogout                KeycloakBoolQuoted `json:"postBindingLogout,omitempty"`
-	ForceAuthn                       KeycloakBoolQuoted `json:"forceAuthn,omitempty"`
-	WantAuthnRequestsSigned          KeycloakBoolQuoted `json:"wantAuthnRequestsSigned,omitempty"`
-	WantAssertionsSigned             KeycloakBoolQuoted `json:"wantAssertionsSigned,omitempty"`
-	WantAssertionsEncrypted          KeycloakBoolQuoted `json:"wantAssertionsEncrypted,omitempty"`
-	BackchannelSupported             KeycloakBoolQuoted `json:"backchannelSupported,omitempty"`
-	ValidateSignature                KeycloakBoolQuoted `json:"validateSignature,omitempty"`
-	AuthorizationUrl                 string             `json:"authorizationUrl,omitempty"`
-	TokenUrl                         string             `json:"tokenUrl,omitempty"`
-	LoginHint                        string             `json:"loginHint,omitempty"`
-	UILocales                        KeycloakBoolQuoted `json:"uiLocales,omitempty"`
-	LogoutUrl                        string             `json:"logoutUrl,omitempty"`
+	Key                              string                 `json:"key,omitempty"`
+	HostIp                           string                 `json:"hostIp,omitempty"`
+	UseJwksUrl                       KeycloakBoolQuoted     `json:"useJwksUrl,omitempty"`
+	JwksUrl                          string                 `json:"jwksUrl,omitempty"`
+	ClientId                         string                 `json:"clientId,omitempty"`
+	ClientSecret                     string                 `json:"clientSecret,omitempty"`
+	DisableUserInfo                  KeycloakBoolQuoted     `json:"disableUserInfo"`
+	UserInfoUrl                      string                 `json:"userInfoUrl,omitempty"`
+	HideOnLoginPage                  KeycloakBoolQuoted     `json:"hideOnLoginPage"`
+	NameIDPolicyFormat               string                 `json:"nameIDPolicyFormat,omitempty"`
+	SingleLogoutServiceUrl           string                 `json:"singleLogoutServiceUrl,omitempty"`
+	SingleSignOnServiceUrl           string                 `json:"singleSignOnServiceUrl,omitempty"`
+	SigningCertificate               string                 `json:"signingCertificate,omitempty"`
+	SignatureAlgorithm               string                 `json:"signatureAlgorithm,omitempty"`
+	XmlSignKeyInfoKeyNameTransformer string                 `json:"xmlSignKeyInfoKeyNameTransformer,omitempty"`
+	PostBindingAuthnRequest          KeycloakBoolQuoted     `json:"postBindingAuthnRequest,omitempty"`
+	PostBindingResponse              KeycloakBoolQuoted     `json:"postBindingResponse,omitempty"`
+	PostBindingLogout                KeycloakBoolQuoted     `json:"postBindingLogout,omitempty"`
+	ForceAuthn                       KeycloakBoolQuoted     `json:"forceAuthn,omitempty"`
+	WantAuthnRequestsSigned          KeycloakBoolQuoted     `json:"wantAuthnRequestsSigned,omitempty"`
+	WantAssertionsSigned             KeycloakBoolQuoted     `json:"wantAssertionsSigned,omitempty"`
+	WantAssertionsEncrypted          KeycloakBoolQuoted     `json:"wantAssertionsEncrypted,omitempty"`
+	BackchannelSupported             KeycloakBoolQuoted     `json:"backchannelSupported,omitempty"`
+	ValidateSignature                KeycloakBoolQuoted     `json:"validateSignature,omitempty"`
+	AuthorizationUrl                 string                 `json:"authorizationUrl,omitempty"`
+	TokenUrl                         string                 `json:"tokenUrl,omitempty"`
+	LoginHint                        string                 `json:"loginHint,omitempty"`
+	UILocales                        KeycloakBoolQuoted     `json:"uiLocales,omitempty"`
+	LogoutUrl                        string                 `json:"logoutUrl,omitempty"`
+	ExtraConfig                      map[string]interface{} `json:"-"`
 }
 
 type IdentityProvider struct {
@@ -54,6 +59,60 @@ type IdentityProvider struct {
 	Config                    *IdentityProviderConfig `json:"config"`
 }
 
+func (f *IdentityProviderConfig) UnmarshalJSON(data []byte) error {
+	f.ExtraConfig = map[string]interface{}{}
+	err := json.Unmarshal(data, &f.ExtraConfig)
+	if err != nil {
+		return err
+	}
+	v := reflect.ValueOf(f).Elem()
+	for i := 0; i < v.NumField(); i++ {
+		structField := v.Type().Field(i)
+		jsonKey := strings.Split(structField.Tag.Get("json"), ",")[0]
+		if jsonKey != "-" {
+			value, ok := f.ExtraConfig[jsonKey]
+			if ok {
+				field := v.FieldByName(structField.Name)
+				if field.IsValid() && field.CanSet() {
+					if field.Kind() == reflect.String {
+						field.SetString(value.(string))
+					} else if field.Kind() == reflect.Bool {
+						boolVal, err := strconv.ParseBool(value.(string))
+						if err == nil {
+							field.Set(reflect.ValueOf(KeycloakBoolQuoted(boolVal)))
+						}
+					}
+					delete(f.ExtraConfig, jsonKey)
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (f *IdentityProviderConfig) MarshalJSON() ([]byte, error) {
+	out := map[string]interface{}{}
+
+	for k, v := range f.ExtraConfig {
+		out[k] = v
+	}
+	v := reflect.ValueOf(f).Elem()
+	for i := 0; i < v.NumField(); i++ {
+		jsonKey := strings.Split(v.Type().Field(i).Tag.Get("json"), ",")[0]
+		if jsonKey != "-" {
+			field := v.Field(i)
+			if field.IsValid() && field.CanSet() {
+				if field.Kind() == reflect.String {
+					out[jsonKey] = field.String()
+				} else if field.Kind() == reflect.Bool {
+					out[jsonKey] = KeycloakBoolQuoted(field.Bool())
+				}
+			}
+		}
+	}
+	return json.Marshal(out)
+}
+
 func (keycloakClient *KeycloakClient) NewIdentityProvider(identityProvider *IdentityProvider) error {
 	log.Printf("[WARN] Realm: %s", identityProvider.Realm)
 	_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/identity-provider/instances", identityProvider.Realm), identityProvider)
diff --git a/provider/resource_keycloak_oidc_identity_provider.go b/provider/resource_keycloak_oidc_identity_provider.go
index 9ff029d..b568b71 100644
--- a/provider/resource_keycloak_oidc_identity_provider.go
+++ b/provider/resource_keycloak_oidc_identity_provider.go
@@ -7,6 +7,12 @@ import (
 
 func resourceKeycloakOidcIdentityProvider() *schema.Resource {
 	oidcSchema := map[string]*schema.Schema{
+		"provider_id": {
+			Type:        schema.TypeString,
+			Optional:    true,
+			Default:     "oidc",
+			Description: "provider id, is always oidc, unless you have a custom implementation",
+		},
 		"backchannel_supported": {
 			Type:        schema.TypeBool,
 			Optional:    true,
@@ -72,6 +78,10 @@ func resourceKeycloakOidcIdentityProvider() *schema.Resource {
 			Default:     false,
 			Description: "Pass current locale to identity provider",
 		},
+		"extra_config": {
+			Type:     schema.TypeMap,
+			Optional: true,
+		},
 	}
 	oidcResource := resourceKeycloakIdentityProvider()
 	oidcResource.Schema = mergeSchemas(oidcResource.Schema, oidcSchema)
@@ -83,7 +93,15 @@ func resourceKeycloakOidcIdentityProvider() *schema.Resource {
 
 func getOidcIdentityProviderFromData(data *schema.ResourceData) (*keycloak.IdentityProvider, error) {
 	rec, _ := getIdentityProviderFromData(data)
-	rec.ProviderId = "oidc"
+	rec.ProviderId = data.Get("provider_id").(string)
+
+	extraConfig := map[string]interface{}{}
+	if v, ok := data.GetOk("extra_config"); ok {
+		for key, value := range v.(map[string]interface{}) {
+			extraConfig[key] = value
+		}
+	}
+
 	rec.Config = &keycloak.IdentityProviderConfig{
 		BackchannelSupported: keycloak.KeycloakBoolQuoted(data.Get("backchannel_supported").(bool)),
 		ValidateSignature:    keycloak.KeycloakBoolQuoted(data.Get("validate_signature").(bool)),
@@ -96,6 +114,7 @@ func getOidcIdentityProviderFromData(data *schema.ResourceData) (*keycloak.Ident
 		LoginHint:            data.Get("login_hint").(string),
 		JwksUrl:              data.Get("jwks_url").(string),
 		UserInfoUrl:          data.Get("user_info_url").(string),
+		ExtraConfig:          extraConfig,
 	}
 	_, useJwksUrl := data.GetOk("jwks_url")
 	rec.Config.UseJwksUrl = keycloak.KeycloakBoolQuoted(useJwksUrl)
@@ -124,5 +143,6 @@ func setOidcIdentityProviderData(data *schema.ResourceData, identityProvider *ke
 	data.Set("token_url", identityProvider.Config.TokenUrl)
 	data.Set("login_hint", identityProvider.Config.LoginHint)
 	data.Set("ui_locales", identityProvider.Config.UILocales)
+	data.Set("extra_config", identityProvider.Config.ExtraConfig)
 	return nil
 }


Test patch:
diff --git a/provider/resource_keycloak_oidc_identity_provider_test.go b/provider/resource_keycloak_oidc_identity_provider_test.go
index b731d03..95686bd 100644
--- a/provider/resource_keycloak_oidc_identity_provider_test.go
+++ b/provider/resource_keycloak_oidc_identity_provider_test.go
@@ -26,6 +26,27 @@ func TestAccKeycloakOidcIdentityProvider_basic(t *testing.T) {
 	})
 }
 
+func TestAccKeycloakOidcIdentityProvider_custom(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	oidcName := "terraform-" + acctest.RandString(10)
+	customConfigValue := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOidcIdentityProviderDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOidcIdentityProvider_custom(realmName, oidcName, customConfigValue),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakOidcIdentityProviderExists("keycloak_oidc_identity_provider.oidc"),
+					testAccCheckKeycloakOidcIdentityProviderHasCustomConfigValue("keycloak_oidc_identity_provider.oidc", customConfigValue),
+				),
+			},
+		},
+	})
+}
+
 func TestAccKeycloakOidcIdentityProvider_createAfterManualDestroy(t *testing.T) {
 	var oidc = &keycloak.IdentityProvider{}
 
@@ -155,6 +176,21 @@ func testAccCheckKeycloakOidcIdentityProviderFetch(resourceName string, oidc *ke
 	}
 }
 
+func testAccCheckKeycloakOidcIdentityProviderHasCustomConfigValue(resourceName, customConfigValue string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		fetchedOidc, err := getKeycloakOidcIdentityProviderFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		if fetchedOidc.Config.ExtraConfig["dummyConfig"].(string) != customConfigValue {
+			return fmt.Errorf("expected custom oidc provider to have config with a custom key 'dummyConfig' with a value %s, but value was %s", customConfigValue, fetchedOidc.Config.ExtraConfig["dummyConfig"].(string))
+		}
+
+		return nil
+	}
+}
+
 func testAccCheckKeycloakOidcIdentityProviderDestroy() resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		for _, rs := range s.RootModule().Resources {
@@ -213,6 +249,27 @@ resource "keycloak_oidc_identity_provider" "oidc" {
 	`, realm, oidc)
 }
 
+func testKeycloakOidcIdentityProvider_custom(realm, alias, customConfigValue string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_oidc_identity_provider" "oidc" {
+	realm             = "${keycloak_realm.realm.id}"
+	provider_id       = "customIdp"
+	alias             = "%s"
+	authorization_url = "https://example.com/auth"
+	token_url         = "https://example.com/token"
+	client_id         = "example_id"
+	client_secret     = "example_token"
+	extra_config      = {
+		dummyConfig = "%s"    
+	}
+}
+	`, realm, alias, customConfigValue)
+}
+
 func testKeycloakOidcIdentityProvider_basicFromInterface(oidc *keycloak.IdentityProvider) string {
 	return fmt.Sprintf(`
 resource "keycloak_realm" "realm" {