# ============================================================================= # Realm # ============================================================================= resource "keycloak_realm" "hexor" { realm = "hexor" enabled = true display_name = "Hexor" login_theme = "keycloak.v2" account_theme = "keycloak.v3" registration_allowed = false reset_password_allowed = true remember_me = true verify_email = false login_with_email_allowed = true duplicate_emails_allowed = false ssl_required = "external" web_authn_passwordless_policy { relying_party_entity_name = "Hexor" relying_party_id = "hexor.cy" signature_algorithms = ["ES256", "RS256"] user_verification_requirement = "required" require_resident_key = "Yes" attestation_conveyance_preference = "none" } } # ============================================================================= # Passkey (WebAuthn Passwordless) — required action # ============================================================================= resource "keycloak_required_action" "webauthn_register_passwordless" { realm_id = keycloak_realm.hexor.id alias = "webauthn-register-passwordless" name = "Webauthn Register Passwordless" enabled = true default_action = false } # ============================================================================= # Browser flow with Passkey support # ============================================================================= resource "keycloak_authentication_flow" "browser_with_passkey" { realm_id = keycloak_realm.hexor.id alias = "browser-with-passkey" description = "Browser flow with passkey/password alternatives" provider_id = "basic-flow" } # --- Cookie (re-use existing session) --- resource "keycloak_authentication_execution" "cookie" { realm_id = keycloak_realm.hexor.id parent_flow_alias = keycloak_authentication_flow.browser_with_passkey.alias authenticator = "auth-cookie" requirement = "ALTERNATIVE" priority = 10 } # --- Identity Provider Redirector --- resource "keycloak_authentication_execution" "idp_redirector" { realm_id = keycloak_realm.hexor.id parent_flow_alias = keycloak_authentication_flow.browser_with_passkey.alias authenticator = "identity-provider-redirector" requirement = "ALTERNATIVE" priority = 20 } # --- Passkey (WebAuthn Passwordless) --- resource "keycloak_authentication_execution" "passkey" { realm_id = keycloak_realm.hexor.id parent_flow_alias = keycloak_authentication_flow.browser_with_passkey.alias authenticator = "webauthn-authenticator-passwordless" requirement = "ALTERNATIVE" priority = 30 } # --- Username/Password + optional 2FA subflow --- resource "keycloak_authentication_subflow" "forms" { realm_id = keycloak_realm.hexor.id parent_flow_alias = keycloak_authentication_flow.browser_with_passkey.alias alias = "browser-with-passkey-forms" provider_id = "basic-flow" requirement = "ALTERNATIVE" priority = 40 } resource "keycloak_authentication_execution" "username_password" { realm_id = keycloak_realm.hexor.id parent_flow_alias = keycloak_authentication_subflow.forms.alias authenticator = "auth-username-password-form" requirement = "REQUIRED" priority = 10 } resource "keycloak_authentication_subflow" "conditional_2fa" { realm_id = keycloak_realm.hexor.id parent_flow_alias = keycloak_authentication_subflow.forms.alias alias = "browser-with-passkey-conditional-2fa" provider_id = "basic-flow" requirement = "CONDITIONAL" priority = 20 } resource "keycloak_authentication_execution" "condition_user_configured" { realm_id = keycloak_realm.hexor.id parent_flow_alias = keycloak_authentication_subflow.conditional_2fa.alias authenticator = "conditional-user-configured" requirement = "REQUIRED" priority = 10 } resource "keycloak_authentication_execution" "otp_form" { realm_id = keycloak_realm.hexor.id parent_flow_alias = keycloak_authentication_subflow.conditional_2fa.alias authenticator = "auth-otp-form" requirement = "ALTERNATIVE" priority = 20 } # --- Bind the new flow as the browser flow --- resource "keycloak_authentication_bindings" "browser" { realm_id = keycloak_realm.hexor.id browser_flow = keycloak_authentication_flow.browser_with_passkey.alias } # ============================================================================= # Google Identity Provider # ============================================================================= resource "keycloak_oidc_google_identity_provider" "google" { realm = keycloak_realm.hexor.id client_id = var.google_client_id client_secret = var.google_client_secret trust_email = true sync_mode = "IMPORT" } # ============================================================================= # Standalone groups # ============================================================================= resource "keycloak_group" "standalone" { for_each = toset(var.groups) realm_id = keycloak_realm.hexor.id name = each.value } resource "keycloak_default_groups" "default" { realm_id = keycloak_realm.hexor.id group_ids = [for g in keycloak_group.standalone : g.id if g.name == "users"] } # ============================================================================= # rsauth2-proxy client (production) # ============================================================================= resource "keycloak_openid_client" "rsauth2_proxy" { realm_id = keycloak_realm.hexor.id client_id = "rsauth2-proxy" name = "rsauth2-proxy" enabled = true access_type = "CONFIDENTIAL" standard_flow_enabled = true direct_access_grants_enabled = false valid_redirect_uris = [ "https://oauth.hexor.cy/callback", ] web_origins = [ "https://oauth.hexor.cy", ] } resource "keycloak_openid_group_membership_protocol_mapper" "rsauth2_proxy_groups" { realm_id = keycloak_realm.hexor.id client_id = keycloak_openid_client.rsauth2_proxy.id name = "groups" claim_name = "groups" full_path = false } resource "keycloak_openid_client_default_scopes" "rsauth2_proxy" { realm_id = keycloak_realm.hexor.id client_id = keycloak_openid_client.rsauth2_proxy.id default_scopes = [ "profile", "email", ] } # ============================================================================= # Proxy applications — auto-created groups + routes ConfigMap # ============================================================================= resource "keycloak_group" "app" { for_each = var.proxy_applications realm_id = keycloak_realm.hexor.id name = "app-${each.key}" } locals { app_allowed_groups = { for k, v in var.proxy_applications : k => concat( ["app-${k}"], v.allowed_groups ) } } # ============================================================================= # OAuth2 applications — full OIDC clients for apps that handle auth themselves # ============================================================================= resource "keycloak_openid_client" "oauth2_app" { for_each = var.oauth2_applications realm_id = keycloak_realm.hexor.id client_id = each.key name = each.key enabled = true access_type = "CONFIDENTIAL" standard_flow_enabled = true direct_access_grants_enabled = false valid_redirect_uris = each.value.redirect_uris valid_post_logout_redirect_uris = each.value.post_logout_redirect_uris web_origins = each.value.web_origins } resource "keycloak_openid_group_membership_protocol_mapper" "oauth2_app_groups" { for_each = var.oauth2_applications realm_id = keycloak_realm.hexor.id client_id = keycloak_openid_client.oauth2_app[each.key].id name = "groups" claim_name = "groups" full_path = false } resource "keycloak_openid_client_default_scopes" "oauth2_app" { for_each = var.oauth2_applications realm_id = keycloak_realm.hexor.id client_id = keycloak_openid_client.oauth2_app[each.key].id default_scopes = concat( ["profile", "email"], each.value.extra_default_scopes ) } resource "keycloak_openid_client_optional_scopes" "oauth2_app" { for_each = { for k, v in var.oauth2_applications : k => v if length(v.extra_optional_scopes) > 0 } realm_id = keycloak_realm.hexor.id client_id = keycloak_openid_client.oauth2_app[each.key].id optional_scopes = each.value.extra_optional_scopes } resource "keycloak_group" "oauth2_app" { for_each = var.oauth2_applications realm_id = keycloak_realm.hexor.id name = "app-${each.key}" } # ============================================================================= # Proxy applications — routes ConfigMap # ============================================================================= resource "kubernetes_config_map_v1" "auth_proxy_routes" { metadata { name = "auth-proxy-routes" namespace = "auth-proxy" } data = { "routes.yaml" = yamlencode({ routes = { for k, v in var.proxy_applications : v.domain => { allowed_groups = local.app_allowed_groups[k] } } }) } }