Repository: mrparkers/terraform-provider-keycloak
Pull request #376 (2020-09-03 06:24:06)

add cache policy parameter to ldap user federation

mrparkers: Hey @cgroschupp, thanks for the PR.  These changes look good.  What would you think about deprecating the existing `cache_policy` attribute in favor of this new schema you've created?  I figure the updated version could look something like this:

```hcl
cache {
  policy          = "EVICT_WEEKLY"
  eviction_day    = 2
  eviction_hour   = 0
  eviction_minute = 0
}
```

If you like this, I can push some changes to your branch to get it implemented, or you could take a stab at it yourself.
cgroschupp: @mrparkers thank you for your feedback. I have deprecated cache_policy and added it to the cache schema.
cgroschupp: @mrparkers Is the change ok?

Patch:
diff --git a/docs/data-sources/authentication_execution.md b/docs/data-sources/authentication_execution.md
new file mode 100644
index 0000000..182a2bb
--- /dev/null
+++ b/docs/data-sources/authentication_execution.md
@@ -0,0 +1,33 @@
+---
+page_title: "keycloak_authentication_execution Data Source"
+---
+
+# keycloak\_authentication\_execution Data Source
+
+This data source can be used to fetch the ID of an authentication execution within Keycloak.
+
+## Example Usage
+
+```hcl
+resource "keycloak_realm" "realm" {
+    realm   = "my-realm"
+    enabled = true
+}
+
+data "keycloak_authentication_execution" "browser_auth_cookie" {
+  realm_id          = keycloak_realm.realm.id
+  parent_flow_alias = "browser"
+  provider_id       = "auth-cookie"
+}
+```
+
+## Argument Reference
+
+- `realm_id` - (Required) The realm the authentication execution exists in.
+- `parent_flow_alias` - (Required) The alias of the flow this execution is attached to.
+- `provider_id` - (Required) The name of the provider. This can be found by experimenting with the GUI and looking at HTTP requests within the network tab of your browser's development tools. This was previously known as the "authenticator".
+
+## Attributes Reference
+
+- `id` - (Computed) The unique ID of the authentication execution, which can be used as an argument to other resources supported by this provider.
+
diff --git a/docs/data-sources/realm.md b/docs/data-sources/realm.md
index c1f7567..494ba03 100644
--- a/docs/data-sources/realm.md
+++ b/docs/data-sources/realm.md
@@ -29,4 +29,4 @@ resource "keycloak_role" "group" {
 
 ## Attributes Reference
 
-See the docs for the `keycloak_realm` resource for details on the exported attributes.
+See the docs for the [`keycloak_realm` resource](https://registry.terraform.io/providers/mrparkers/keycloak/latest/docs/resources/realm) for details on the exported attributes.
diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md
new file mode 100644
index 0000000..9b59fd8
--- /dev/null
+++ b/docs/data-sources/user.md
@@ -0,0 +1,44 @@
+---
+page_title: "keycloak_user Data Source"
+---
+
+# keycloak\_user Data Source
+
+This data source can be used to fetch properties of a user within Keycloak.
+
+## Example Usage
+
+```hcl
+data "keycloak_realm" "master_realm" {
+  realm = "master"
+}
+
+// use the keycloak_user data source to grab the admin user's ID
+data "keycloak_user" "default_admin_user" {
+  realm_id = data.keycloak_realm.master_realm.id
+  name     = "keycloak"
+}
+
+output "keycloak_user_id" {
+  value = data.keycloak_user.default_admin_user.id
+}
+```
+
+## Argument Reference
+
+- `realm_id` - (Required) The realm this user belongs to.
+- `username` - (Required) The unique username of this user.
+
+## Attributes Reference
+
+- `id` - (Computed) The unique ID of the user, which can be used as an argument to other resources supported by this provider.
+- `enabled` - (Computed) When false, this user cannot log in. Defaults to `true`.
+- `email` - (Computed) The user's email.
+- `email_verified` - (Computed) Whether the email address was validated or not. Default to `false`.
+- `first_name` - (Computed) The user's first name.
+- `last_name` - (Computed) The user's last name.
+- `attributes` - (Computed) A map representing attributes for the user
+- `federated_identity` - (Computed) The user's federated identities, if applicable. This block has the following schema:
+  - `identity_provider` - (Computed) The name of the identity provider
+  - `user_id` - (Computed) The ID of the user defined in the identity provider
+  - `user_name` - (Computed) The user name of the user defined in the identity provider
diff --git a/docs/resources/ldap_user_federation.md b/docs/resources/ldap_user_federation.md
index c09c40d..2016e9e 100644
--- a/docs/resources/ldap_user_federation.md
+++ b/docs/resources/ldap_user_federation.md
@@ -80,7 +80,13 @@ resource "keycloak_ldap_user_federation" "ldap_user_federation" {
 - `batch_size_for_sync` - (Optional) The number of users to sync within a single transaction. Defaults to `1000`.
 - `full_sync_period` - (Optional) How frequently Keycloak should sync all LDAP users, in seconds. Omit this property to disable periodic full sync.
 - `changed_sync_period` - (Optional) How frequently Keycloak should sync changed LDAP users, in seconds. Omit this property to disable periodic changed users sync.
-- `cache_policy` - (Optional) Can be one of `DEFAULT`, `EVICT_DAILY`, `EVICT_WEEKLY`, `MAX_LIFESPAN`, or `NO_CACHE`. Defaults to `DEFAULT`.
+- `cache_policy` - (Optional) **Deprecated** Can be one of `DEFAULT`, `EVICT_DAILY`, `EVICT_WEEKLY`, `MAX_LIFESPAN`, or `NO_CACHE`. Defaults to `DEFAULT`.
+- `cache` - (Optional) A block containing the cache settings.
+  - `policy` - (Optional) Can be one of `DEFAULT`, `EVICT_DAILY`, `EVICT_WEEKLY`, `MAX_LIFESPAN`, or `NO_CACHE`. Defaults to `DEFAULT`.
+  - `max_lifespan` - (Optional) Max lifespan of cache entry (duration string).
+  - `eviction_day` - (Optional) Day of the week the entry will become invalid on
+  - `eviction_hour` - (Optional) Hour of day the entry will become invalid on.
+  - `eviction_day` - (Optional) Minute of day the entry will become invalid on.
 - `kerberos` - (Optional) A block containing the kerberos settings.
   - `kerberos_realm` - (Required) The name of the kerberos realm, e.g. FOO.LOCAL.
   - `server_principal` - (Required) The kerberos server principal, e.g. 'HTTP/host.foo.com@FOO.LOCAL'.
diff --git a/docs/resources/realm.md b/docs/resources/realm.md
index cb3b432..a090f14 100644
--- a/docs/resources/realm.md
+++ b/docs/resources/realm.md
@@ -67,6 +67,12 @@ resource "keycloak_realm" "realm" {
       failure_reset_time_seconds        = 43200
     }
   }
+
+  web_authn_policy {
+    relying_party_entity_name = "Example"
+    relying_party_id          = "keycloak.example.com"
+    signature_algorithms      = ["ES256", "RS256"]
+  }
 }
 ```
 
@@ -190,6 +196,27 @@ The arguments below can be used to configure authentication flow bindings:
 - `client_authentication_flow` - (Optional) The desired flow for client authentication. Defaults to `clients`.
 - `docker_authentication_flow` - (Optional) The desired flow for Docker authentication. Defaults to `docker auth`.
 
+### WebAuthn
+
+The following settings can be used to modify the "WebAuthn Policy" and "WebAuthn Passwordless Policy" settings found within
+the "Authentication" section of the realm configuration UI. These top level attributes can be used:
+
+- `web_authn_policy` - (Optional) Configuration for WebAuthn Policy authentication.
+- `web_authn_passwordless_policy` - (Optional) Configuration for WebAuthn Passwordless Policy authentication.
+
+Each of these attributes are blocks with the following attributes:
+
+- `relying_party_entity_name` - (Optional) A human readable server name for the WebAuthn Relying Party. Defaults to `keycloak`.
+- `relying_party_id` - (Optional) The WebAuthn relying party ID.
+- `signature_algorithms` - (Optional) A set of signature algorithms that should be used for the authentication assertion. Valid options at the time these docs were written are `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, `RS512`, and `RS1`.
+- `attestation_conveyance_preference` - (Optional) The preference of how to generate a WebAuthn attestation statement. Valid options are `not specified`, `none`, `indirect`, `direct`, or `enterprise`. Defaults to `not specified`.
+- `authenticator_attachment` - (Optional) The acceptable attachment pattern for the WebAuthn authenticator. Valid options are `not specified`, `platform`, or `cross-platform`. Defaults to `not specified`.
+- `require_resident_key` - (Optional) Specifies whether or not a public key should be created to represent the resident key. Valid options are `not specified`, `Yes`, or `No`. Defaults to `not specified`.
+- `user_verification_requirement` - (Optional) Specifies the policy for verifying a user logging in via WebAuthn. Valid options are `not specified`, `required`, `preferred`, or `discouraged`. Defaults to `not specified`.
+- `create_timeout` - (Optional) The timeout value for creating a user's public key credential in seconds. When set to `0`, this timeout option is not adapted. Defaults to `0`.
+- `avoid_same_authenticator_register` - (Optional) When `true`, Keycloak will avoid registering the authenticator for WebAuthn if it has already been registered. Defaults to `false`.
+- `acceptable_aaguids` - (Optional) A set of AAGUIDs for which an authenticator can be registered.
+
 ## Attributes Reference
 
 - `internal_id` - (Computed) When importing realms created outside of this terraform provider, they could use generated arbitrary IDs for the internal realm id. Realms created by this provider always use the realm's name for its internal id.
diff --git a/example/main.tf b/example/main.tf
index e95fc22..b87c8d7 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -76,6 +76,18 @@ resource "keycloak_realm" "test" {
   attributes = {
     mycustomAttribute = "myCustomValue"
   }
+
+  web_authn_policy {
+    relying_party_entity_name = "Example"
+    relying_party_id = "keycloak.example.com"
+    signature_algorithms = ["ES256", "RS256"]
+  }
+
+  web_authn_passwordless_policy {
+    relying_party_entity_name = "Example"
+    relying_party_id = "keycloak.example.com"
+    signature_algorithms = ["ES256", "RS256"]
+  }
 }
 
 resource "keycloak_required_action" "custom-terms-and-conditions" {
diff --git a/keycloak/authentication_execution.go b/keycloak/authentication_execution.go
index b8b2d6b..840d955 100644
--- a/keycloak/authentication_execution.go
+++ b/keycloak/authentication_execution.go
@@ -2,6 +2,7 @@ package keycloak
 
 import (
 	"fmt"
+	"time"
 )
 
 // this is only used when creating an execution on a flow.
@@ -73,6 +74,49 @@ func (keycloakClient *KeycloakClient) ListAuthenticationExecutions(realmId, pare
 	return authenticationExecutions, err
 }
 
+func (keycloakClient *KeycloakClient) GetAuthenticationExecutionInfoFromProviderId(realmId, parentFlowAlias, providerId string) (*AuthenticationExecutionInfo, error) {
+	var authenticationExecutions []*AuthenticationExecutionInfo
+	var authenticationExecution AuthenticationExecutionInfo
+
+	err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions", realmId, parentFlowAlias), &authenticationExecutions, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	// Retry 3 more times if not found, sometimes it took split milliseconds the Authentication Executions to populate
+	if len(authenticationExecutions) == 0 {
+		for i := 0; i < 3; i++ {
+			err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions", realmId, parentFlowAlias), &authenticationExecutions, nil)
+
+			if len(authenticationExecutions) > 0 {
+				break
+			}
+
+			if err != nil {
+				return nil, err
+			}
+
+			time.Sleep(time.Millisecond * 50)
+		}
+
+		if len(authenticationExecutions) == 0 {
+			return nil, fmt.Errorf("no authentication executions found for parent flow alias %s", parentFlowAlias)
+		}
+	}
+
+	for _, aExecution := range authenticationExecutions {
+		if aExecution != nil && aExecution.ProviderId == providerId {
+			authenticationExecution = *aExecution
+			authenticationExecution.RealmId = realmId
+			authenticationExecution.ParentFlowAlias = parentFlowAlias
+
+			return &authenticationExecution, nil
+		}
+	}
+
+	return nil, fmt.Errorf("no authentication execution under parent flow alias %s with provider id %s found", parentFlowAlias, providerId)
+}
+
 func (keycloakClient *KeycloakClient) NewAuthenticationExecution(execution *AuthenticationExecution) error {
 	_, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions/execution", execution.RealmId, execution.ParentFlowAlias), &authenticationExecutionCreate{Provider: execution.Authenticator})
 	if err != nil {
diff --git a/keycloak/ldap_user_federation.go b/keycloak/ldap_user_federation.go
index b9d3aaf..2e78e76 100644
--- a/keycloak/ldap_user_federation.go
+++ b/keycloak/ldap_user_federation.go
@@ -46,7 +46,11 @@ type LdapUserFederation struct {
 	FullSyncPeriod    int // either a number, in milliseconds, or -1 if full sync is disabled
 	ChangedSyncPeriod int // either a number, in milliseconds, or -1 if changed sync is disabled
 
-	CachePolicy string
+	CachePolicy    string
+	MaxLifespan    string // duration string (ex: 1h30m)
+	EvictionDay    *int
+	EvictionHour   *int
+	EvictionMinute *int
 }
 
 func convertFromLdapUserFederationToComponent(ldap *LdapUserFederation) (*component, error) {
@@ -173,6 +177,31 @@ func convertFromLdapUserFederationToComponent(ldap *LdapUserFederation) (*compon
 		componentConfig["readTimeout"] = []string{} // the keycloak API will not unset this unless the config is present with an empty array
 	}
 
+	componentConfig["evictionHour"] = []string{}
+	componentConfig["evictionMinute"] = []string{}
+	componentConfig["evictionDay"] = []string{}
+	componentConfig["maxLifespan"] = []string{}
+
+	if ldap.CachePolicy != "" {
+		if ldap.EvictionHour != nil {
+			componentConfig["evictionHour"] = []string{strconv.Itoa(*ldap.EvictionHour)}
+		}
+		if ldap.EvictionMinute != nil {
+			componentConfig["evictionMinute"] = []string{strconv.Itoa(*ldap.EvictionMinute)}
+		}
+		if ldap.EvictionDay != nil {
+			componentConfig["evictionDay"] = []string{strconv.Itoa(*ldap.EvictionDay)}
+		}
+
+		if ldap.MaxLifespan != "" {
+			maxLifespanMs, err := getMillisecondsFromDurationString(ldap.MaxLifespan)
+			if err != nil {
+				return nil, err
+			}
+			componentConfig["maxLifespan"] = []string{maxLifespanMs}
+		}
+	}
+
 	return &component{
 		Id:           ldap.Id,
 		Name:         ldap.Name,
@@ -324,6 +353,49 @@ func convertFromComponentToLdapUserFederation(component *component) (*LdapUserFe
 		ldap.ReadTimeout = readTimeoutDurationString
 	}
 
+	if maxLifespan, ok := component.getConfigOk("maxLifespan"); ok {
+		maxLifespanString, err := GetDurationStringFromMilliseconds(maxLifespan)
+		if err != nil {
+			return nil, err
+		}
+
+		ldap.MaxLifespan = maxLifespanString
+	}
+
+	defaultEvictioValue := -1
+
+	if evictionDay, ok := component.getConfigOk("evictionDay"); ok {
+		evictionDayInt, err := strconv.Atoi(evictionDay)
+		if err != nil {
+			return nil, fmt.Errorf("unable to parse `evictionDay`: %w", err)
+		}
+
+		ldap.EvictionDay = &evictionDayInt
+	} else {
+		ldap.EvictionDay = &defaultEvictioValue
+	}
+
+	if evictionHour, ok := component.getConfigOk("evictionHour"); ok {
+		evictionHourInt, err := strconv.Atoi(evictionHour)
+		if err != nil {
+			return nil, fmt.Errorf("unable to parse `evictionHour`: %w", err)
+		}
+
+		ldap.EvictionHour = &evictionHourInt
+	} else {
+		ldap.EvictionHour = &defaultEvictioValue
+	}
+	if evictionMinute, ok := component.getConfigOk("evictionMinute"); ok {
+		evictionMinuteInt, err := strconv.Atoi(evictionMinute)
+		if err != nil {
+			return nil, fmt.Errorf("unable to parse `evictionMinute`: %w", err)
+		}
+
+		ldap.EvictionMinute = &evictionMinuteInt
+	} else {
+		ldap.EvictionMinute = &defaultEvictioValue
+	}
+
 	return ldap, nil
 }
 
diff --git a/keycloak/realm.go b/keycloak/realm.go
index d62dd40..27bd69b 100644
--- a/keycloak/realm.go
+++ b/keycloak/realm.go
@@ -94,6 +94,30 @@ type Realm struct {
 	ResetCredentialsFlow     string `json:"resetCredentialsFlow,omitempty"`
 	ClientAuthenticationFlow string `json:"clientAuthenticationFlow,omitempty"`
 	DockerAuthenticationFlow string `json:"dockerAuthenticationFlow,omitempty"`
+
+	// WebAuthn
+	WebAuthnPolicyAcceptableAaguids               []string `json:"webAuthnPolicyAcceptableAaguids"`
+	WebAuthnPolicyAttestationConveyancePreference string   `json:"webAuthnPolicyAttestationConveyancePreference"`
+	WebAuthnPolicyAuthenticatorAttachment         string   `json:"webAuthnPolicyAuthenticatorAttachment"`
+	WebAuthnPolicyAvoidSameAuthenticatorRegister  bool     `json:"webAuthnPolicyAvoidSameAuthenticatorRegister"`
+	WebAuthnPolicyCreateTimeout                   int      `json:"webAuthnPolicyCreateTimeout"`
+	WebAuthnPolicyRequireResidentKey              string   `json:"webAuthnPolicyRequireResidentKey"`
+	WebAuthnPolicyRpEntityName                    string   `json:"webAuthnPolicyRpEntityName"`
+	WebAuthnPolicyRpId                            string   `json:"webAuthnPolicyRpId"`
+	WebAuthnPolicySignatureAlgorithms             []string `json:"webAuthnPolicySignatureAlgorithms"`
+	WebAuthnPolicyUserVerificationRequirement     string   `json:"webAuthnPolicyUserVerificationRequirement"`
+
+	// WebAuthn Passwordless
+	WebAuthnPolicyPasswordlessAcceptableAaguids               []string `json:"webAuthnPolicyPasswordlessAcceptableAaguids"`
+	WebAuthnPolicyPasswordlessAttestationConveyancePreference string   `json:"webAuthnPolicyPasswordlessAttestationConveyancePreference"`
+	WebAuthnPolicyPasswordlessAuthenticatorAttachment         string   `json:"webAuthnPolicyPasswordlessAuthenticatorAttachment"`
+	WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister  bool     `json:"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister"`
+	WebAuthnPolicyPasswordlessCreateTimeout                   int      `json:"webAuthnPolicyPasswordlessCreateTimeout"`
+	WebAuthnPolicyPasswordlessRequireResidentKey              string   `json:"webAuthnPolicyPasswordlessRequireResidentKey"`
+	WebAuthnPolicyPasswordlessRpEntityName                    string   `json:"webAuthnPolicyPasswordlessRpEntityName"`
+	WebAuthnPolicyPasswordlessRpId                            string   `json:"webAuthnPolicyPasswordlessRpId"`
+	WebAuthnPolicyPasswordlessSignatureAlgorithms             []string `json:"webAuthnPolicyPasswordlessSignatureAlgorithms"`
+	WebAuthnPolicyPasswordlessUserVerificationRequirement     string   `json:"webAuthnPolicyPasswordlessUserVerificationRequirement"`
 }
 
 type BrowserSecurityHeaders struct {
diff --git a/provider/data_source_keycloak_authentication_execution.go b/provider/data_source_keycloak_authentication_execution.go
new file mode 100644
index 0000000..a4cc1c3
--- /dev/null
+++ b/provider/data_source_keycloak_authentication_execution.go
@@ -0,0 +1,43 @@
+package provider
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func dataSourceKeycloakAuthenticationExecution() *schema.Resource {
+	return &schema.Resource{
+		Read: dataSourceKeycloakAuthenticationExecutionRead,
+		Schema: map[string]*schema.Schema{
+			"realm_id": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"parent_flow_alias": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"provider_id": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+		},
+	}
+}
+
+func dataSourceKeycloakAuthenticationExecutionRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmID := data.Get("realm_id").(string)
+	parentFlowAlias := data.Get("parent_flow_alias").(string)
+	providerID := data.Get("provider_id").(string)
+
+	authenticationExecutionInfo, err := keycloakClient.GetAuthenticationExecutionInfoFromProviderId(realmID, parentFlowAlias, providerID)
+	if err != nil {
+		return err
+	}
+
+	mapFromAuthenticationExecutionInfoToData(data, authenticationExecutionInfo)
+
+	return nil
+}
diff --git a/provider/data_source_keycloak_realm.go b/provider/data_source_keycloak_realm.go
index 992ce46..0c4d9de 100644
--- a/provider/data_source_keycloak_realm.go
+++ b/provider/data_source_keycloak_realm.go
@@ -6,6 +6,59 @@ import (
 )
 
 func dataSourceKeycloakRealm() *schema.Resource {
+	webAuthnSchema := map[string]*schema.Schema{
+		"acceptable_aaguids": {
+			Type: schema.TypeSet,
+			Elem: &schema.Schema{
+				Type: schema.TypeString,
+			},
+			Computed: true,
+		},
+		"attestation_conveyance_preference": {
+			Type:        schema.TypeString,
+			Description: "Either none, indirect or direct",
+			Computed:    true,
+		},
+		"authenticator_attachment": {
+			Type:        schema.TypeString,
+			Description: "Either platform or cross-platform",
+			Computed:    true,
+		},
+		"avoid_same_authenticator_register": {
+			Type:     schema.TypeBool,
+			Computed: true,
+		},
+		"create_timeout": {
+			Type:     schema.TypeInt,
+			Computed: true,
+		},
+		"require_resident_key": {
+			Type:        schema.TypeString,
+			Description: "Either Yes or No",
+			Computed:    true,
+		},
+		"relying_party_entity_name": {
+			Type:     schema.TypeString,
+			Computed: true,
+		},
+		"relying_party_id": {
+			Type:     schema.TypeString,
+			Computed: true,
+		},
+		"signature_algorithms": {
+			Type: schema.TypeSet,
+			Elem: &schema.Schema{
+				Type: schema.TypeString,
+			},
+			Description: "Keycloak lists ES256, ES384, ES512, RS256, ES384, ES512 at the time of writing",
+			Computed:    true,
+		},
+		"user_verification_requirement": {
+			Type:        schema.TypeString,
+			Description: "Either required, preferred or discouraged",
+			Computed:    true,
+		},
+	}
 	return &schema.Resource{
 		Read: dataSourceKeycloakRealmRead,
 		Schema: map[string]*schema.Schema{
@@ -372,6 +425,28 @@ func dataSourceKeycloakRealm() *schema.Resource {
 				Optional: true,
 				Computed: true,
 			},
+
+			// WebAuthn
+			"web_authn_policy": {
+				Type:     schema.TypeList,
+				Optional: true,
+				Computed: true,
+				MaxItems: 1,
+				Elem: &schema.Resource{
+					Schema: webAuthnSchema,
+				},
+			},
+
+			// WebAuthn Passwordless
+			"web_authn_passwordless_policy": {
+				Type:     schema.TypeList,
+				Optional: true,
+				Computed: true,
+				MaxItems: 1,
+				Elem: &schema.Resource{
+					Schema: webAuthnSchema,
+				},
+			},
 		},
 	}
 }
diff --git a/provider/data_source_keycloak_user.go b/provider/data_source_keycloak_user.go
new file mode 100644
index 0000000..c7719e8
--- /dev/null
+++ b/provider/data_source_keycloak_user.go
@@ -0,0 +1,85 @@
+package provider
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func dataSourceKeycloakUser() *schema.Resource {
+	return &schema.Resource{
+		Read: dataSourceKeycloakUserRead,
+		Schema: map[string]*schema.Schema{
+			"realm_id": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"username": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"email": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"email_verified": {
+				Type:     schema.TypeBool,
+				Computed: true,
+			},
+			"first_name": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"last_name": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"attributes": {
+				Type:     schema.TypeMap,
+				Computed: true,
+			},
+			"federated_identity": {
+				Type:     schema.TypeSet,
+				Elem:     &schema.Schema{Type: schema.TypeString},
+				Computed: true,
+			},
+			"federated_identities": {
+				Type:       schema.TypeSet,
+				Elem:       &schema.Schema{Type: schema.TypeString},
+				Computed:   true,
+				Deprecated: "use federated_identity instead",
+			},
+			"enabled": {
+				Type:     schema.TypeBool,
+				Computed: true,
+			},
+		},
+	}
+}
+
+func dataSourceKeycloakUserRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmID := data.Get("realm_id").(string)
+	username := data.Get("username").(string)
+
+	user, err := keycloakClient.GetUserByUsername(realmID, username)
+	if err != nil {
+		return err
+	}
+
+	mapFromUserToData(data, user)
+
+	// handling deprecated "federated_identities" attribute
+	federatedIdentities := []interface{}{}
+	for _, federatedIdentity := range user.FederatedIdentities {
+		identity := map[string]interface{}{
+			"identity_provider": federatedIdentity.IdentityProvider,
+			"user_id":           federatedIdentity.UserId,
+			"user_name":         federatedIdentity.UserName,
+		}
+		federatedIdentities = append(federatedIdentities, identity)
+	}
+	data.Set("federated_identities", federatedIdentities)
+
+	return nil
+}
diff --git a/provider/provider.go b/provider/provider.go
index 016e5ef..d8ca5a7 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -3,6 +3,7 @@ package provider
 import (
 	"context"
 	"fmt"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
@@ -19,7 +20,9 @@ func KeycloakProvider() *schema.Provider {
 			"keycloak_realm":                              dataSourceKeycloakRealm(),
 			"keycloak_realm_keys":                         dataSourceKeycloakRealmKeys(),
 			"keycloak_role":                               dataSourceKeycloakRole(),
+			"keycloak_user":                               dataSourceKeycloakUser(),
 			"keycloak_saml_client_installation_provider":  dataSourceKeycloakSamlClientInstallationProvider(),
+			"keycloak_authentication_execution":           dataSourceKeycloakAuthenticationExecution(),
 		},
 		ResourcesMap: map[string]*schema.Resource{
 			"keycloak_realm":                                             resourceKeycloakRealm(),
diff --git a/provider/resource_keycloak_authentication_execution.go b/provider/resource_keycloak_authentication_execution.go
index 65ab492..e1f4857 100644
--- a/provider/resource_keycloak_authentication_execution.go
+++ b/provider/resource_keycloak_authentication_execution.go
@@ -2,10 +2,11 @@ package provider
 
 import (
 	"fmt"
+	"strings"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"strings"
 )
 
 func resourceKeycloakAuthenticationExecution() *schema.Resource {
@@ -64,6 +65,13 @@ func mapFromAuthenticationExecutionToData(data *schema.ResourceData, authenticat
 	data.Set("requirement", authenticationExecution.Requirement)
 }
 
+func mapFromAuthenticationExecutionInfoToData(data *schema.ResourceData, authenticationExecutionInfo *keycloak.AuthenticationExecutionInfo) {
+	data.SetId(authenticationExecutionInfo.Id)
+
+	data.Set("realm_id", authenticationExecutionInfo.RealmId)
+	data.Set("parent_flow_alias", authenticationExecutionInfo.ParentFlowAlias)
+}
+
 func resourceKeycloakAuthenticationExecutionCreate(data *schema.ResourceData, meta interface{}) error {
 	keycloakClient := meta.(*keycloak.KeycloakClient)
 
diff --git a/provider/resource_keycloak_group.go b/provider/resource_keycloak_group.go
index 038d5bf..21d7ecf 100644
--- a/provider/resource_keycloak_group.go
+++ b/provider/resource_keycloak_group.go
@@ -2,9 +2,10 @@ package provider
 
 import (
 	"fmt"
+	"strings"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"strings"
 )
 
 func resourceKeycloakGroup() *schema.Resource {
diff --git a/provider/resource_keycloak_ldap_user_federation.go b/provider/resource_keycloak_ldap_user_federation.go
index 2b23487..046070d 100644
--- a/provider/resource_keycloak_ldap_user_federation.go
+++ b/provider/resource_keycloak_ldap_user_federation.go
@@ -223,10 +223,56 @@ func resourceKeycloakLdapUserFederation() *schema.Resource {
 			},
 
 			"cache_policy": {
-				Type:         schema.TypeString,
-				Optional:     true,
-				Default:      "DEFAULT",
-				ValidateFunc: validation.StringInSlice(keycloakUserFederationCachePolicies, false),
+				Type:          schema.TypeString,
+				Optional:      true,
+				Default:       "DEFAULT",
+				Deprecated:    "use cache.policy instead",
+				ConflictsWith: []string{"cache"},
+				ValidateFunc:  validation.StringInSlice(keycloakUserFederationCachePolicies, false),
+			},
+			"cache": {
+				Type:          schema.TypeList,
+				Optional:      true,
+				MaxItems:      1,
+				Description:   "Settings regarding cache policy for this realm.",
+				ConflictsWith: []string{"cache_policy"},
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"policy": {
+							Type:         schema.TypeString,
+							Optional:     true,
+							Default:      "DEFAULT",
+							ValidateFunc: validation.StringInSlice(keycloakUserFederationCachePolicies, false),
+						},
+						"max_lifespan": {
+							Type:             schema.TypeString,
+							Optional:         true,
+							DiffSuppressFunc: suppressDurationStringDiff,
+							Description:      "Max lifespan of cache entry (duration string).",
+						},
+						"eviction_day": {
+							Type:         schema.TypeInt,
+							Optional:     true,
+							Default:      "-1",
+							ValidateFunc: validation.All(validation.IntAtLeast(0), validation.IntAtMost(6)),
+							Description:  "Day of the week the entry will become invalid on.",
+						},
+						"eviction_hour": {
+							Type:         schema.TypeInt,
+							Optional:     true,
+							Default:      "-1",
+							ValidateFunc: validation.All(validation.IntAtLeast(0), validation.IntAtMost(23)),
+							Description:  "Hour of day the entry will become invalid on.",
+						},
+						"eviction_minute": {
+							Type:         schema.TypeInt,
+							Optional:     true,
+							Default:      "-1",
+							ValidateFunc: validation.All(validation.IntAtLeast(0), validation.IntAtMost(59)),
+							Description:  "Minute of day the entry will become invalid on.",
+						},
+					},
+				},
 			},
 		},
 	}
@@ -289,6 +335,22 @@ func getLdapUserFederationFromData(data *schema.ResourceData) *keycloak.LdapUser
 		CachePolicy: data.Get("cache_policy").(string),
 	}
 
+	if cache, ok := data.GetOk("cache"); ok {
+		cache := cache.([]interface{})
+		cacheData := cache[0].(map[string]interface{})
+
+		evictionDay := cacheData["eviction_day"].(int)
+		evictionHour := cacheData["eviction_hour"].(int)
+		evictionMinute := cacheData["eviction_minute"].(int)
+
+		ldapUserFederation.MaxLifespan = cacheData["max_lifespan"].(string)
+
+		ldapUserFederation.EvictionDay = &evictionDay
+		ldapUserFederation.EvictionHour = &evictionHour
+		ldapUserFederation.EvictionMinute = &evictionMinute
+		ldapUserFederation.CachePolicy = cacheData["policy"].(string)
+	}
+
 	if kerberos, ok := data.GetOk("kerberos"); ok {
 		ldapUserFederation.AllowKerberosAuthentication = true
 		kerberosSettingsData := kerberos.(*schema.Set).List()[0]
@@ -353,7 +415,29 @@ func setLdapUserFederationData(data *schema.ResourceData, ldap *keycloak.LdapUse
 	data.Set("full_sync_period", ldap.FullSyncPeriod)
 	data.Set("changed_sync_period", ldap.ChangedSyncPeriod)
 
-	data.Set("cache_policy", ldap.CachePolicy)
+	if _, ok := data.GetOk("cache"); ok {
+		cachePolicySettings := make(map[string]interface{})
+
+		if ldap.MaxLifespan != "" {
+			cachePolicySettings["max_lifespan"] = ldap.MaxLifespan
+		}
+
+		if ldap.EvictionDay != nil {
+			cachePolicySettings["eviction_day"] = *ldap.EvictionDay
+		}
+		if ldap.EvictionHour != nil {
+			cachePolicySettings["eviction_hour"] = *ldap.EvictionHour
+		}
+		if ldap.EvictionMinute != nil {
+			cachePolicySettings["eviction_minute"] = *ldap.EvictionMinute
+		}
+
+		cachePolicySettings["policy"] = ldap.CachePolicy
+
+		data.Set("cache", []interface{}{cachePolicySettings})
+	} else {
+		data.Set("cache_policy", ldap.CachePolicy)
+	}
 }
 
 func resourceKeycloakLdapUserFederationCreate(data *schema.ResourceData, meta interface{}) error {
diff --git a/provider/resource_keycloak_realm.go b/provider/resource_keycloak_realm.go
index 9fe2785..67512c5 100644
--- a/provider/resource_keycloak_realm.go
+++ b/provider/resource_keycloak_realm.go
@@ -2,10 +2,87 @@ package provider
 
 import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
 )
 
 func resourceKeycloakRealm() *schema.Resource {
+	webAuthnSchema := map[string]*schema.Schema{
+		"acceptable_aaguids": {
+			Type: schema.TypeSet,
+			Elem: &schema.Schema{
+				Type: schema.TypeString,
+			},
+			Optional: true,
+		},
+		"attestation_conveyance_preference": {
+			Type:         schema.TypeString,
+			Description:  "Either none, indirect or direct",
+			Optional:     true,
+			Default:      "not specified",
+			ValidateFunc: validation.StringInSlice([]string{"not specified", "none", "indirect", "direct", "enterprise"}, false),
+		},
+		"authenticator_attachment": {
+			Type:         schema.TypeString,
+			Description:  "Either platform or cross-platform",
+			Optional:     true,
+			Default:      "not specified",
+			ValidateFunc: validation.StringInSlice([]string{"not specified", "platform", "cross-platform"}, false),
+		},
+		"avoid_same_authenticator_register": {
+			Type:     schema.TypeBool,
+			Optional: true,
+			Default:  false,
+		},
+		"create_timeout": {
+			Type:     schema.TypeInt,
+			Optional: true,
+			Default:  0,
+			ValidateFunc: func(i interface{}, k string) ([]string, []error) {
+				v := i.(int)
+
+				// https://w3c.github.io/webauthn/#sctn-createCredential
+				if v != 0 && (v < 30 || v > 600) {
+					return []string{"the recommended timeout value is between 30<->180 seconds (inclusive, userVerification=discouraged) or 30<->600 seconds (inclusive, userVerification=(required || preferred))"}, nil
+				}
+
+				return nil, nil
+			},
+		},
+		"require_resident_key": {
+			Type:         schema.TypeString,
+			Description:  "Either Yes or No",
+			Optional:     true,
+			Default:      "not specified",
+			ValidateFunc: validation.StringInSlice([]string{"not specified", "Yes", "No"}, false),
+		},
+		"relying_party_entity_name": {
+			Type:     schema.TypeString,
+			Optional: true,
+			Default:  "keycloak",
+		},
+		"relying_party_id": {
+			Type:     schema.TypeString,
+			Optional: true,
+			Default:  "",
+		},
+		"signature_algorithms": {
+			Type: schema.TypeSet,
+			Elem: &schema.Schema{
+				Type: schema.TypeString,
+			},
+			Description: "Keycloak lists ES256, ES384, ES512, RS256, RS384, RS512, RS1 at the time of writing",
+			Optional:    true,
+			Computed:    true,
+		},
+		"user_verification_requirement": {
+			Type:         schema.TypeString,
+			Description:  "Either required, preferred or discouraged",
+			Optional:     true,
+			Default:      "not specified",
+			ValidateFunc: validation.StringInSlice([]string{"not specified", "required", "preferred", "discouraged"}, false),
+		},
+	}
 	return &schema.Resource{
 		Create: resourceKeycloakRealmCreate,
 		Read:   resourceKeycloakRealmRead,
@@ -441,6 +518,28 @@ func resourceKeycloakRealm() *schema.Resource {
 				Type:     schema.TypeMap,
 				Optional: true,
 			},
+
+			// WebAuthn
+			"web_authn_policy": {
+				Type:     schema.TypeList,
+				Optional: true,
+				Computed: true,
+				MaxItems: 1,
+				Elem: &schema.Resource{
+					Schema: webAuthnSchema,
+				},
+			},
+
+			// WebAuthn Passwordless
+			"web_authn_passwordless_policy": {
+				Type:     schema.TypeList,
+				Optional: true,
+				Computed: true,
+				MaxItems: 1,
+				Elem: &schema.Resource{
+					Schema: webAuthnSchema,
+				},
+			},
 		},
 	}
 }
@@ -749,6 +848,88 @@ func getRealmFromData(data *schema.ResourceData) (*keycloak.Realm, error) {
 	}
 	realm.Attributes = attributes
 
+	//WebAuthn
+	if v, ok := data.GetOk("web_authn_policy"); ok {
+		webAuthnPolicy := v.([]interface{})[0].(map[string]interface{})
+
+		realm.WebAuthnPolicyAcceptableAaguids = interfaceSliceToStringSlice(webAuthnPolicy["acceptable_aaguids"].(*schema.Set).List())
+
+		if webAuthnPolicyAttestationConveyancePreference, ok := webAuthnPolicy["attestation_conveyance_preference"]; ok {
+			realm.WebAuthnPolicyAttestationConveyancePreference = webAuthnPolicyAttestationConveyancePreference.(string)
+		}
+
+		if webAuthnPolicyAuthenticatorAttachment, ok := webAuthnPolicy["authenticator_attachment"]; ok {
+			realm.WebAuthnPolicyAuthenticatorAttachment = webAuthnPolicyAuthenticatorAttachment.(string)
+		}
+
+		if webAuthnPolicyAvoidSameAuthenticatorRegister, ok := webAuthnPolicy["avoid_same_authenticator_register"]; ok {
+			realm.WebAuthnPolicyAvoidSameAuthenticatorRegister = webAuthnPolicyAvoidSameAuthenticatorRegister.(bool)
+		}
+
+		if webAuthnPolicyCreateTimeout, ok := webAuthnPolicy["create_timeout"]; ok {
+			realm.WebAuthnPolicyCreateTimeout = webAuthnPolicyCreateTimeout.(int)
+		}
+
+		if webAuthnPolicyRequireResidentKey, ok := webAuthnPolicy["require_resident_key"]; ok {
+			realm.WebAuthnPolicyRequireResidentKey = webAuthnPolicyRequireResidentKey.(string)
+		}
+
+		if webAuthnPolicyRpEntityName, ok := webAuthnPolicy["relying_party_entity_name"]; ok {
+			realm.WebAuthnPolicyRpEntityName = webAuthnPolicyRpEntityName.(string)
+		}
+
+		if webAuthnPolicyRpId, ok := webAuthnPolicy["relying_party_id"]; ok {
+			realm.WebAuthnPolicyRpId = webAuthnPolicyRpId.(string)
+		}
+
+		realm.WebAuthnPolicySignatureAlgorithms = interfaceSliceToStringSlice(webAuthnPolicy["signature_algorithms"].(*schema.Set).List())
+
+		if webAuthnPolicyUserVerificationRequirement, ok := webAuthnPolicy["user_verification_requirement"]; ok {
+			realm.WebAuthnPolicyUserVerificationRequirement = webAuthnPolicyUserVerificationRequirement.(string)
+		}
+	}
+
+	//WebAuthn Passwordless
+	if v, ok := data.GetOk("web_authn_passwordless_policy"); ok {
+		webAuthnPasswordlessPolicy := v.([]interface{})[0].(map[string]interface{})
+
+		realm.WebAuthnPolicyPasswordlessAcceptableAaguids = interfaceSliceToStringSlice(webAuthnPasswordlessPolicy["acceptable_aaguids"].(*schema.Set).List())
+
+		if webAuthnPolicyPasswordlessAttestationConveyancePreference, ok := webAuthnPasswordlessPolicy["attestation_conveyance_preference"]; ok {
+			realm.WebAuthnPolicyPasswordlessAttestationConveyancePreference = webAuthnPolicyPasswordlessAttestationConveyancePreference.(string)
+		}
+
+		if webAuthnPolicyPasswordlessAuthenticatorAttachment, ok := webAuthnPasswordlessPolicy["authenticator_attachment"]; ok {
+			realm.WebAuthnPolicyPasswordlessAuthenticatorAttachment = webAuthnPolicyPasswordlessAuthenticatorAttachment.(string)
+		}
+
+		if webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister, ok := webAuthnPasswordlessPolicy["avoid_same_authenticator_register"]; ok {
+			realm.WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister = webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister.(bool)
+		}
+
+		if webAuthnPolicyPasswordlessCreateTimeout, ok := webAuthnPasswordlessPolicy["create_timeout"]; ok {
+			realm.WebAuthnPolicyPasswordlessCreateTimeout = webAuthnPolicyPasswordlessCreateTimeout.(int)
+		}
+
+		if webAuthnPolicyPasswordlessRequireResidentKey, ok := webAuthnPasswordlessPolicy["require_resident_key"]; ok {
+			realm.WebAuthnPolicyPasswordlessRequireResidentKey = webAuthnPolicyPasswordlessRequireResidentKey.(string)
+		}
+
+		if webAuthnPolicyPasswordlessRpEntityName, ok := webAuthnPasswordlessPolicy["relying_party_entity_name"]; ok {
+			realm.WebAuthnPolicyPasswordlessRpEntityName = webAuthnPolicyPasswordlessRpEntityName.(string)
+		}
+
+		if webAuthnPolicyPasswordlessRpId, ok := webAuthnPasswordlessPolicy["relying_party_id"]; ok {
+			realm.WebAuthnPolicyPasswordlessRpId = webAuthnPolicyPasswordlessRpId.(string)
+		}
+
+		realm.WebAuthnPolicyPasswordlessSignatureAlgorithms = interfaceSliceToStringSlice(webAuthnPasswordlessPolicy["signature_algorithms"].(*schema.Set).List())
+
+		if webAuthnPolicyPasswordlessUserVerificationRequirement, ok := webAuthnPasswordlessPolicy["user_verification_requirement"]; ok {
+			realm.WebAuthnPolicyPasswordlessUserVerificationRequirement = webAuthnPolicyPasswordlessUserVerificationRequirement.(string)
+		}
+	}
+
 	return realm, nil
 }
 
@@ -889,6 +1070,34 @@ func setRealmData(data *schema.ResourceData, realm *keycloak.Realm) {
 	data.Set("client_authentication_flow", realm.ClientAuthenticationFlow)
 	data.Set("docker_authentication_flow", realm.DockerAuthenticationFlow)
 
+	//WebAuthn
+	webAuthnPolicy := make(map[string]interface{})
+	webAuthnPolicy["acceptable_aaguids"] = realm.WebAuthnPolicyAcceptableAaguids
+	webAuthnPolicy["attestation_conveyance_preference"] = realm.WebAuthnPolicyAttestationConveyancePreference
+	webAuthnPolicy["authenticator_attachment"] = realm.WebAuthnPolicyAuthenticatorAttachment
+	webAuthnPolicy["avoid_same_authenticator_register"] = realm.WebAuthnPolicyAvoidSameAuthenticatorRegister
+	webAuthnPolicy["create_timeout"] = realm.WebAuthnPolicyCreateTimeout
+	webAuthnPolicy["require_resident_key"] = realm.WebAuthnPolicyRequireResidentKey
+	webAuthnPolicy["relying_party_entity_name"] = realm.WebAuthnPolicyRpEntityName
+	webAuthnPolicy["relying_party_id"] = realm.WebAuthnPolicyRpId
+	webAuthnPolicy["signature_algorithms"] = realm.WebAuthnPolicySignatureAlgorithms
+	webAuthnPolicy["user_verification_requirement"] = realm.WebAuthnPolicyUserVerificationRequirement
+	data.Set("web_authn_policy", []interface{}{webAuthnPolicy})
+
+	//WebAuthn Passwordless
+	webAuthnPasswordlessPolicy := make(map[string]interface{})
+	webAuthnPasswordlessPolicy["acceptable_aaguids"] = realm.WebAuthnPolicyPasswordlessAcceptableAaguids
+	webAuthnPasswordlessPolicy["attestation_conveyance_preference"] = realm.WebAuthnPolicyPasswordlessAttestationConveyancePreference
+	webAuthnPasswordlessPolicy["authenticator_attachment"] = realm.WebAuthnPolicyPasswordlessAuthenticatorAttachment
+	webAuthnPasswordlessPolicy["avoid_same_authenticator_register"] = realm.WebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister
+	webAuthnPasswordlessPolicy["create_timeout"] = realm.WebAuthnPolicyPasswordlessCreateTimeout
+	webAuthnPasswordlessPolicy["require_resident_key"] = realm.WebAuthnPolicyPasswordlessRequireResidentKey
+	webAuthnPasswordlessPolicy["relying_party_entity_name"] = realm.WebAuthnPolicyPasswordlessRpEntityName
+	webAuthnPasswordlessPolicy["relying_party_id"] = realm.WebAuthnPolicyPasswordlessRpId
+	webAuthnPasswordlessPolicy["signature_algorithms"] = realm.WebAuthnPolicyPasswordlessSignatureAlgorithms
+	webAuthnPasswordlessPolicy["user_verification_requirement"] = realm.WebAuthnPolicyPasswordlessUserVerificationRequirement
+	data.Set("web_authn_passwordless_policy", []interface{}{webAuthnPasswordlessPolicy})
+
 	attributes := map[string]interface{}{}
 	if v, ok := data.GetOk("attributes"); ok {
 		for key := range v.(map[string]interface{}) {


Test patch:
diff --git a/provider/data_source_keycloak_authentication_execution_test.go b/provider/data_source_keycloak_authentication_execution_test.go
new file mode 100644
index 0000000..4059814
--- /dev/null
+++ b/provider/data_source_keycloak_authentication_execution_test.go
@@ -0,0 +1,171 @@
+package provider
+
+import (
+	"fmt"
+	"regexp"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func TestAccKeycloakDataSourceAuthenticationExecution_basic(t *testing.T) {
+	realm := "terraform-" + acctest.RandString(10)
+	parentFlowAlias := acctest.RandString(20)
+
+	resource.Test(t, resource.TestCase{
+		ProviderFactories: testAccProviderFactories,
+		PreCheck:          func() { testAccPreCheck(t) },
+		CheckDestroy:      testAccCheckKeycloakAuthenticationExecutionConfigDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testDataSourceKeycloakAuthenticationExecution_basic(realm, parentFlowAlias),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakAuthenticationExecutionExists("keycloak_authentication_execution.execution"),
+					resource.TestCheckResourceAttrPair("keycloak_authentication_execution.execution", "id", "data.keycloak_authentication_execution.execution", "id"),
+					resource.TestCheckResourceAttrPair("keycloak_authentication_execution.execution", "realm_id", "data.keycloak_authentication_execution.execution", "realm_id"),
+					resource.TestCheckResourceAttrPair("keycloak_authentication_execution.execution", "parent_flow_alias", "data.keycloak_authentication_execution.execution", "parent_flow_alias"),
+					resource.TestCheckResourceAttrPair("keycloak_authentication_execution.execution", "authenticator", "data.keycloak_authentication_execution.execution", "provider_id"),
+					testAccCheckDataKeycloakAuthenticationExecution("data.keycloak_authentication_execution.execution"),
+				),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakDataSourceAuthenticationExecution_errorNoExecutions(t *testing.T) {
+	realm := "terraform-" + acctest.RandString(10)
+	parentFlowAlias := acctest.RandString(20)
+
+	resource.Test(t, resource.TestCase{
+		ProviderFactories: testAccProviderFactories,
+		PreCheck:          func() { testAccPreCheck(t) },
+		CheckDestroy:      testAccCheckKeycloakAuthenticationExecutionConfigDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config:      testDataSourceKeycloakAuthenticationExecution_errorNoExecutions(realm, parentFlowAlias),
+				ExpectError: regexp.MustCompile("no authentication executions found for parent flow alias .*"),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakDataSourceAuthenticationExecution_errorWrongProviderId(t *testing.T) {
+	realm := "terraform-" + acctest.RandString(10)
+	parentFlowAlias := acctest.RandString(20)
+
+	resource.Test(t, resource.TestCase{
+		ProviderFactories: testAccProviderFactories,
+		PreCheck:          func() { testAccPreCheck(t) },
+		CheckDestroy:      testAccCheckKeycloakAuthenticationExecutionConfigDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config:      testDataSourceKeycloakAuthenticationExecution_errorWrongProviderId(realm, parentFlowAlias, acctest.RandString(10)),
+				ExpectError: regexp.MustCompile("no authentication execution under parent flow alias .* with provider id .* found"),
+			},
+		},
+	})
+}
+
+func testAccCheckDataKeycloakAuthenticationExecution(resourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[resourceName]
+		if !ok {
+			return fmt.Errorf("resource not found: %s", resourceName)
+		}
+
+		keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+		id := rs.Primary.ID
+		realmID := rs.Primary.Attributes["realm_id"]
+		parentFlowAlias := rs.Primary.Attributes["parent_flow_alias"]
+		providerID := rs.Primary.Attributes["provider_id"]
+
+		authenticationExecutionInfo, err := keycloakClient.GetAuthenticationExecutionInfoFromProviderId(realmID, parentFlowAlias, providerID)
+		if err != nil {
+			return err
+		}
+
+		if authenticationExecutionInfo.Id != id {
+			return fmt.Errorf("expected authenticationExecutionInfo with ID %s but got %s", id, authenticationExecutionInfo.Id)
+		}
+
+		return nil
+	}
+}
+
+func testDataSourceKeycloakAuthenticationExecution_basic(realm, parentFlowAlias string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm   = "%s"
+	enabled = true
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = keycloak_realm.realm.id
+	alias    = "%s"
+}
+
+resource "keycloak_authentication_execution" "execution" {
+	realm_id          = keycloak_realm.realm.id
+	parent_flow_alias = keycloak_authentication_flow.flow.alias
+	authenticator     = "identity-provider-redirector"
+	requirement       = "REQUIRED"
+}
+
+data "keycloak_authentication_execution" "execution" {
+	realm_id 			= keycloak_realm.realm.id
+	parent_flow_alias   = keycloak_authentication_flow.flow.alias
+	provider_id     	= "identity-provider-redirector"
+}
+	`, realm, parentFlowAlias)
+}
+
+func testDataSourceKeycloakAuthenticationExecution_errorNoExecutions(realm, parentFlowAlias string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm   = "%s"
+	enabled = true
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = keycloak_realm.realm.id
+	alias    = "%s"
+}
+
+data "keycloak_authentication_execution" "execution" {
+	realm_id 			= keycloak_realm.realm.id
+	parent_flow_alias   = keycloak_authentication_flow.flow.alias
+	provider_id     	= "foo"
+}
+	`, realm, parentFlowAlias)
+}
+
+func testDataSourceKeycloakAuthenticationExecution_errorWrongProviderId(realm, parentFlowAlias, providerId string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm   = "%s"
+	enabled = true
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = keycloak_realm.realm.id
+	alias    = "%s"
+}
+
+resource "keycloak_authentication_execution" "execution" {
+	realm_id          = keycloak_realm.realm.id
+	parent_flow_alias = keycloak_authentication_flow.flow.alias
+	authenticator     = "identity-provider-redirector"
+	requirement       = "REQUIRED"
+}
+
+data "keycloak_authentication_execution" "execution" {
+	realm_id 			= keycloak_realm.realm.id
+	parent_flow_alias   = keycloak_authentication_flow.flow.alias
+	provider_id     	= "%s"
+}
+	`, realm, parentFlowAlias, providerId)
+}
diff --git a/provider/data_source_keycloak_user_test.go b/provider/data_source_keycloak_user_test.go
new file mode 100644
index 0000000..180da23
--- /dev/null
+++ b/provider/data_source_keycloak_user_test.go
@@ -0,0 +1,83 @@
+package provider
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func TestAccKeycloakDataSourceUser(t *testing.T) {
+	realm := "terraform-" + acctest.RandString(10)
+	username := acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		ProviderFactories: testAccProviderFactories,
+		PreCheck:          func() { testAccPreCheck(t) },
+		CheckDestroy:      testAccCheckKeycloakUserDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testDataSourceKeycloakUser(realm, username),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakUserExists("keycloak_user.user"),
+					resource.TestCheckResourceAttrPair("keycloak_user.user", "id", "data.keycloak_user.user", "id"),
+					resource.TestCheckResourceAttrPair("keycloak_user.user", "realm_id", "data.keycloak_user.user", "realm_id"),
+					resource.TestCheckResourceAttrPair("keycloak_user.user", "username", "data.keycloak_user.user", "username"),
+					testAccCheckDataKeycloakUser("data.keycloak_user.user"),
+				),
+			},
+		},
+	})
+}
+
+func testAccCheckDataKeycloakUser(resourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[resourceName]
+		if !ok {
+			return fmt.Errorf("resource not found: %s", resourceName)
+		}
+
+		keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+		id := rs.Primary.ID
+		realmID := rs.Primary.Attributes["realm_id"]
+		username := rs.Primary.Attributes["username"]
+
+		user, err := keycloakClient.GetUser(realmID, id)
+		if err != nil {
+			return err
+		}
+
+		if user.Username != username {
+			return fmt.Errorf("expected user with ID %s to have username %s, but got %s", id, username, user.Username)
+		}
+
+		return nil
+	}
+}
+
+func testDataSourceKeycloakUser(realm, username string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm 		= "%s"
+}
+
+resource "keycloak_user" "user" {
+	username    = "%s"
+	realm_id 	= "${keycloak_realm.realm.id}"
+	enabled    	= true
+
+    email      	= "bob@domain.com"
+    first_name 	= "Bob"
+    last_name  	= "Bobson"
+}
+
+data "keycloak_user" "user" {
+	realm_id 	= "${keycloak_realm.realm.id}"
+	username    = "${keycloak_user.user.username}"
+}
+	`, realm, username)
+}
diff --git a/provider/resource_keycloak_authentication_execution_test.go b/provider/resource_keycloak_authentication_execution_test.go
index a98bdcd..3842f97 100644
--- a/provider/resource_keycloak_authentication_execution_test.go
+++ b/provider/resource_keycloak_authentication_execution_test.go
@@ -2,11 +2,12 @@ package provider
 
 import (
 	"fmt"
+	"testing"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"testing"
 )
 
 func TestAccKeycloakAuthenticationExecution_basic(t *testing.T) {
diff --git a/provider/resource_keycloak_ldap_user_federation_test.go b/provider/resource_keycloak_ldap_user_federation_test.go
index 08af993..1dbdd17 100644
--- a/provider/resource_keycloak_ldap_user_federation_test.go
+++ b/provider/resource_keycloak_ldap_user_federation_test.go
@@ -127,6 +127,10 @@ func generateRandomLdapKerberos(enabled bool) *keycloak.LdapUserFederation {
 	connectionTimeout, _ := keycloak.GetDurationStringFromMilliseconds(strconv.Itoa(acctest.RandIntRange(1, 3600) * 1000))
 	readTimeout, _ := keycloak.GetDurationStringFromMilliseconds(strconv.Itoa(acctest.RandIntRange(1, 3600) * 1000))
 
+	evictionDay := acctest.RandIntRange(0, 6)
+	evictionHour := acctest.RandIntRange(0, 23)
+	evictionMinute := acctest.RandIntRange(0, 59)
+
 	return &keycloak.LdapUserFederation{
 		RealmId:                              acctest.RandString(10),
 		Name:                                 "terraform-" + acctest.RandString(10),
@@ -153,6 +157,10 @@ func generateRandomLdapKerberos(enabled bool) *keycloak.LdapUserFederation {
 		AllowKerberosAuthentication:          true,
 		KeyTab:                               acctest.RandString(10),
 		KerberosRealm:                        acctest.RandString(10),
+		MaxLifespan:                          randomStringInSlice([]string{"1h", "2h", "3h"}),
+		EvictionDay:                          &evictionDay,
+		EvictionHour:                         &evictionHour,
+		EvictionMinute:                       &evictionMinute,
 	}
 }
 
@@ -225,6 +233,10 @@ func TestAccKeycloakLdapUserFederation_basicUpdateAll(t *testing.T) {
 	firstReadTimeout, _ := keycloak.GetDurationStringFromMilliseconds(strconv.Itoa(acctest.RandIntRange(1, 3600) * 1000))
 	secondReadTimeout, _ := keycloak.GetDurationStringFromMilliseconds(strconv.Itoa(acctest.RandIntRange(1, 3600) * 1000))
 
+	evictionDay := acctest.RandIntRange(0, 6)
+	evictionHour := acctest.RandIntRange(0, 23)
+	evictionMinute := acctest.RandIntRange(0, 59)
+
 	firstLdap := &keycloak.LdapUserFederation{
 		RealmId:                              realmName,
 		Name:                                 "terraform-" + acctest.RandString(10),
@@ -251,8 +263,16 @@ func TestAccKeycloakLdapUserFederation_basicUpdateAll(t *testing.T) {
 		AllowKerberosAuthentication:          randomBool(),
 		KeyTab:                               acctest.RandString(10),
 		KerberosRealm:                        acctest.RandString(10),
+		MaxLifespan:                          randomStringInSlice([]string{"1h", "2h", "3h"}),
+		EvictionDay:                          &evictionDay,
+		EvictionHour:                         &evictionHour,
+		EvictionMinute:                       &evictionMinute,
 	}
 
+	evictionDay = acctest.RandIntRange(0, 6)
+	evictionHour = acctest.RandIntRange(0, 23)
+	evictionMinute = acctest.RandIntRange(0, 59)
+
 	secondLdap := &keycloak.LdapUserFederation{
 		RealmId:                              realmName,
 		Name:                                 "terraform-" + acctest.RandString(10),
@@ -279,6 +299,10 @@ func TestAccKeycloakLdapUserFederation_basicUpdateAll(t *testing.T) {
 		AllowKerberosAuthentication:          randomBool(),
 		KeyTab:                               acctest.RandString(10),
 		KerberosRealm:                        acctest.RandString(10),
+		MaxLifespan:                          randomStringInSlice([]string{"1h", "2h", "3h"}),
+		EvictionDay:                          &evictionDay,
+		EvictionHour:                         &evictionHour,
+		EvictionMinute:                       &evictionMinute,
 	}
 
 	resource.Test(t, resource.TestCase{
@@ -659,9 +683,15 @@ resource "keycloak_ldap_user_federation" "openldap" {
 		kerberos_realm                           = "%s"
 	}
 
-	cache_policy             = "%s"
+	cache {
+		policy               = "%s"
+		max_lifespan         = "%s"
+		eviction_day         = %d
+		eviction_hour        = %d
+		eviction_minute      = %d
+	}
 }
-	`, ldap.RealmId, ldap.Name, ldap.Enabled, ldap.UsernameLDAPAttribute, ldap.RdnLDAPAttribute, ldap.UuidLDAPAttribute, arrayOfStringsForTerraformResource(ldap.UserObjectClasses), ldap.ConnectionUrl, ldap.UsersDn, ldap.BindDn, ldap.BindCredential, ldap.SearchScope, ldap.ValidatePasswordPolicy, ldap.UseTruststoreSpi, ldap.ConnectionTimeout, ldap.ReadTimeout, ldap.Pagination, ldap.BatchSizeForSync, ldap.FullSyncPeriod, ldap.ChangedSyncPeriod, ldap.ServerPrincipal, ldap.UseKerberosForPasswordAuthentication, ldap.KeyTab, ldap.KerberosRealm, ldap.CachePolicy)
+	`, ldap.RealmId, ldap.Name, ldap.Enabled, ldap.UsernameLDAPAttribute, ldap.RdnLDAPAttribute, ldap.UuidLDAPAttribute, arrayOfStringsForTerraformResource(ldap.UserObjectClasses), ldap.ConnectionUrl, ldap.UsersDn, ldap.BindDn, ldap.BindCredential, ldap.SearchScope, ldap.ValidatePasswordPolicy, ldap.UseTruststoreSpi, ldap.ConnectionTimeout, ldap.ReadTimeout, ldap.Pagination, ldap.BatchSizeForSync, ldap.FullSyncPeriod, ldap.ChangedSyncPeriod, ldap.ServerPrincipal, ldap.UseKerberosForPasswordAuthentication, ldap.KeyTab, ldap.KerberosRealm, ldap.CachePolicy, ldap.MaxLifespan, *ldap.EvictionDay, *ldap.EvictionHour, *ldap.EvictionMinute)
 }
 
 func testKeycloakLdapUserFederation_basicWithAttrValidation(attr, realm, ldap, val string) string {
diff --git a/provider/resource_keycloak_realm_test.go b/provider/resource_keycloak_realm_test.go
index 2fb1b22..15b582c 100644
--- a/provider/resource_keycloak_realm_test.go
+++ b/provider/resource_keycloak_realm_test.go
@@ -708,6 +708,40 @@ func TestAccKeycloakRealm_internalId(t *testing.T) {
 	})
 }
 
+func TestAccKeycloakRealm_webauthn(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	realmDisplayName := "terraform-" + acctest.RandString(10)
+	realmDisplayNameHtml := "<b>terraform-" + acctest.RandString(10) + "</b>"
+	rpName := "terraform-" + acctest.RandString(10)
+	rpId := "terraform-" + acctest.RandString(10)
+	attestationConveyancePreference := randomStringInSlice([]string{"none", "indirect", "not specified"})
+	authenticatorAttachment := randomStringInSlice([]string{"platform", "cross-platform", "not specified"})
+	requireResidentKey := randomStringInSlice([]string{"Yes", "No", "not specified"})
+	userVerificationRequirement := randomStringInSlice([]string{"not specified", "required", "preferred", "discouraged"})
+	signatureAlgorithms := randomStringSliceSubset([]string{"ES256", "ES384", "ES512", "RS256", "ES384", "ES512"})
+	avoidSameAuthenticatorRegister := randomBool()
+
+	resource.Test(t, resource.TestCase{
+		ProviderFactories: testAccProviderFactories,
+		PreCheck:          func() { testAccPreCheck(t) },
+		CheckDestroy:      testAccCheckKeycloakRealmDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakRealm_webauthn_policy(realmName, realmDisplayName, realmDisplayNameHtml, rpName, rpId, attestationConveyancePreference, authenticatorAttachment, requireResidentKey, userVerificationRequirement, signatureAlgorithms, avoidSameAuthenticatorRegister),
+				Check:  testAccCheckKeycloakRealmExists("keycloak_realm.realm"),
+			},
+			{
+				Config: testKeycloakRealm_webauthn_passwordless_policy(realmName, realmDisplayName, realmDisplayNameHtml, rpName, rpId, attestationConveyancePreference, authenticatorAttachment, requireResidentKey, userVerificationRequirement, signatureAlgorithms, avoidSameAuthenticatorRegister),
+				Check:  testAccCheckKeycloakRealmExists("keycloak_realm.realm"),
+			},
+			{
+				Config: testKeycloakRealm_basic(realmName, realmDisplayName, realmDisplayNameHtml),
+				Check:  testAccCheckKeycloakRealmExists("keycloak_realm.realm"),
+			},
+		},
+	})
+}
+
 func testKeycloakRealmLoginInfo(resourceName string, realm *keycloak.Realm) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		realmFromState, err := getRealmFromState(s, resourceName)
@@ -1381,3 +1415,49 @@ resource "keycloak_realm" "realm" {
 }
 	`, realm, key, value)
 }
+
+func testKeycloakRealm_webauthn_policy(realm, realmDisplayName, realmDisplayNameHtml, rpName, rpId, attestationConveyancePreference, authenticatorAttachment, requireResidentKey, userVerificationRequirement string, signatureAlgorithms []string, avoidSameAuthenticatorRegister bool) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm        		= "%s"
+	enabled     	 	= true
+	display_name 		= "%s"
+	display_name_html 	= "%s"
+
+	web_authn_policy {
+		relying_party_entity_name         = "%s"
+		relying_party_id                  = "%s"
+		signature_algorithms              = %s
+
+		attestation_conveyance_preference = "%s"
+		authenticator_attachment          = "%s"
+		avoid_same_authenticator_register = %t
+		require_resident_key              = "%s"
+		user_verification_requirement     = "%s"
+	}
+}
+	`, realm, realmDisplayName, realmDisplayNameHtml, rpName, rpId, arrayOfStringsForTerraformResource(signatureAlgorithms), attestationConveyancePreference, authenticatorAttachment, avoidSameAuthenticatorRegister, requireResidentKey, userVerificationRequirement)
+}
+
+func testKeycloakRealm_webauthn_passwordless_policy(realm, realmDisplayName, realmDisplayNameHtml, rpName, rpId, attestationConveyancePreference, authenticatorAttachment, requireResidentKey, userVerificationRequirement string, signatureAlgorithms []string, avoidSameAuthenticatorRegister bool) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm        		= "%s"
+	enabled     	 	= true
+	display_name 		= "%s"
+	display_name_html 	= "%s"
+
+	web_authn_passwordless_policy {
+		relying_party_entity_name         = "%s"
+		relying_party_id                  = "%s"
+		signature_algorithms              = %s
+
+		attestation_conveyance_preference = "%s"
+		authenticator_attachment          = "%s"
+		avoid_same_authenticator_register = %t
+		require_resident_key              = "%s"
+		user_verification_requirement     = "%s"
+	}
+}
+	`, realm, realmDisplayName, realmDisplayNameHtml, rpName, rpId, arrayOfStringsForTerraformResource(signatureAlgorithms), attestationConveyancePreference, authenticatorAttachment, avoidSameAuthenticatorRegister, requireResidentKey, userVerificationRequirement)
+}