#!/usr/bin/env bash set -euo pipefail authentik_url="${AUTHENTIK_URL:-https://auth.burrow.net}" bootstrap_token="${AUTHENTIK_BOOTSTRAP_TOKEN:-}" provider_slug="${AUTHENTIK_TAILNET_PROVIDER_SLUG:-ts}" provider_slugs_json="${AUTHENTIK_TAILNET_PROVIDER_SLUGS_JSON:-}" authentication_flow_name="${AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_NAME:-Burrow Tailnet Authentication}" authentication_flow_slug="${AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_SLUG:-burrow-tailnet-authentication}" identification_stage_name="${AUTHENTIK_TAILNET_IDENTIFICATION_STAGE_NAME:-burrow-tailnet-identification-stage}" password_stage_name="${AUTHENTIK_TAILNET_PASSWORD_STAGE_NAME:-burrow-tailnet-password-stage}" user_login_stage_name="${AUTHENTIK_TAILNET_USER_LOGIN_STAGE_NAME:-burrow-tailnet-user-login-stage}" google_source_slug="${AUTHENTIK_TAILNET_GOOGLE_SOURCE_SLUG:-google}" usage() { cat <<'EOF' Usage: Scripts/authentik-sync-tailnet-auth-flow.sh Required environment: AUTHENTIK_BOOTSTRAP_TOKEN Optional environment: AUTHENTIK_URL AUTHENTIK_TAILNET_PROVIDER_SLUG AUTHENTIK_TAILNET_PROVIDER_SLUGS_JSON AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_NAME AUTHENTIK_TAILNET_AUTHENTICATION_FLOW_SLUG AUTHENTIK_TAILNET_IDENTIFICATION_STAGE_NAME AUTHENTIK_TAILNET_PASSWORD_STAGE_NAME AUTHENTIK_TAILNET_USER_LOGIN_STAGE_NAME AUTHENTIK_TAILNET_GOOGLE_SOURCE_SLUG EOF } if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then usage exit 0 fi if [[ -z "$bootstrap_token" ]]; then echo "error: AUTHENTIK_BOOTSTRAP_TOKEN is required" >&2 exit 1 fi if [[ -n "$provider_slugs_json" ]]; then if ! printf '%s' "$provider_slugs_json" | jq -e 'type == "array" and length > 0 and all(.[]; type == "string" and length > 0)' >/dev/null; then echo "error: AUTHENTIK_TAILNET_PROVIDER_SLUGS_JSON must be a non-empty JSON array of strings" >&2 exit 1 fi else provider_slugs_json="$(jq -cn --arg slug "$provider_slug" '[$slug]')" fi api() { local method="$1" local path="$2" local data="${3:-}" if [[ -n "$data" ]]; then curl -fsS \ -X "$method" \ -H "Authorization: Bearer ${bootstrap_token}" \ -H "Content-Type: application/json" \ -d "$data" \ "${authentik_url}${path}" else curl -fsS \ -X "$method" \ -H "Authorization: Bearer ${bootstrap_token}" \ "${authentik_url}${path}" fi } wait_for_authentik() { for _ in $(seq 1 90); do if curl -fsS "${authentik_url}/-/health/ready/" >/dev/null 2>&1; then return 0 fi sleep 2 done echo "error: Authentik did not become ready at ${authentik_url}" >&2 exit 1 } lookup_stage_by_name() { local path="$1" local name="$2" api GET "${path}?page_size=200" \ | jq -c --arg name "$name" '.results[]? | select(.name == $name)' \ | head -n1 } lookup_flow_pk() { local slug="$1" api GET "/api/v3/flows/instances/?slug=${slug}" \ | jq -r '.results[]? | select(.slug != null) | .pk // empty' \ | head -n1 } lookup_source_pk() { local slug="$1" api GET "/api/v3/sources/oauth/?page_size=200&slug=${slug}" \ | jq -r --arg slug "$slug" '.results[]? | select(.slug == $slug) | .pk // empty' \ | head -n1 } ensure_password_stage() { local existing payload stage_pk existing="$(lookup_stage_by_name "/api/v3/stages/password/" "$password_stage_name")" payload="$( jq -cn \ --arg name "$password_stage_name" \ '{ name: $name, backends: [ "authentik.core.auth.InbuiltBackend", "authentik.core.auth.TokenBackend" ], allow_show_password: false, failed_attempts_before_cancel: 5 }' )" if [[ -n "$existing" ]]; then stage_pk="$(printf '%s\n' "$existing" | jq -r '.pk')" api PATCH "/api/v3/stages/password/${stage_pk}/" "$payload" >/dev/null else stage_pk="$( api POST "/api/v3/stages/password/" "$payload" \ | jq -r '.pk // empty' )" fi printf '%s\n' "$stage_pk" } ensure_identification_stage() { local password_stage_pk="$1" local google_source_pk="$2" local existing payload stage_pk sources_json existing="$(lookup_stage_by_name "/api/v3/stages/identification/" "$identification_stage_name")" if [[ -n "$google_source_pk" ]]; then sources_json="$(jq -cn --arg source "$google_source_pk" '[$source]')" else sources_json='[]' fi payload="$( jq -cn \ --arg name "$identification_stage_name" \ --arg password_stage "$password_stage_pk" \ --argjson sources "$sources_json" \ '{ name: $name, user_fields: ["username", "email"], password_stage: $password_stage, case_insensitive_matching: true, show_matched_user: true, sources: $sources, show_source_labels: true, pretend_user_exists: false, enable_remember_me: false }' )" if [[ -n "$existing" ]]; then stage_pk="$(printf '%s\n' "$existing" | jq -r '.pk')" api PATCH "/api/v3/stages/identification/${stage_pk}/" "$payload" >/dev/null else stage_pk="$( api POST "/api/v3/stages/identification/" "$payload" \ | jq -r '.pk // empty' )" fi printf '%s\n' "$stage_pk" } ensure_user_login_stage() { local existing payload stage_pk existing="$(lookup_stage_by_name "/api/v3/stages/user_login/" "$user_login_stage_name")" payload="$( jq -cn \ --arg name "$user_login_stage_name" \ '{ name: $name, session_duration: "hours=12", terminate_other_sessions: false, remember_me_offset: "seconds=0", network_binding: "no_binding", geoip_binding: "no_binding" }' )" if [[ -n "$existing" ]]; then stage_pk="$(printf '%s\n' "$existing" | jq -r '.pk')" api PATCH "/api/v3/stages/user_login/${stage_pk}/" "$payload" >/dev/null else stage_pk="$( api POST "/api/v3/stages/user_login/" "$payload" \ | jq -r '.pk // empty' )" fi printf '%s\n' "$stage_pk" } ensure_authentication_flow() { local existing_pk payload existing_pk="$(lookup_flow_pk "$authentication_flow_slug")" payload="$( jq -cn \ --arg name "$authentication_flow_name" \ --arg slug "$authentication_flow_slug" \ '{ name: $name, title: $name, slug: $slug, designation: "authentication", policy_engine_mode: "any", layout: "stacked" }' )" if [[ -n "$existing_pk" ]]; then api PATCH "/api/v3/flows/instances/${authentication_flow_slug}/" "$payload" >/dev/null printf '%s\n' "$existing_pk" else api POST "/api/v3/flows/instances/" "$payload" \ | jq -r '.pk // empty' fi } ensure_flow_binding() { local flow_pk="$1" local stage_pk="$2" local order="$3" local existing payload binding_pk existing="$( api GET "/api/v3/flows/bindings/?target=${flow_pk}&stage=${stage_pk}&page_size=200" \ | jq -c '.results[]?' \ | head -n1 )" payload="$( jq -cn \ --arg target "$flow_pk" \ --arg stage "$stage_pk" \ --argjson order "$order" \ '{ target: $target, stage: $stage, order: $order, policy_engine_mode: "any" }' )" if [[ -n "$existing" ]]; then binding_pk="$(printf '%s\n' "$existing" | jq -r '.pk')" api PATCH "/api/v3/flows/bindings/${binding_pk}/" "$payload" >/dev/null else api POST "/api/v3/flows/bindings/" "$payload" >/dev/null fi } wait_for_authentik mapfile -t provider_pks < <( api GET "/api/v3/providers/oauth2/?page_size=200" \ | jq -r --argjson provider_slugs "$provider_slugs_json" ' .results[]? | select( ((.assigned_application_slug // empty) as $assigned | ($provider_slugs | index($assigned)) != null) or ((.slug // empty) as $slug | ($provider_slugs | index($slug)) != null) ) | .pk // empty ' ) if [[ "${#provider_pks[@]}" -eq 0 ]]; then echo "error: could not resolve any Authentik Tailnet OAuth providers from ${provider_slugs_json}" >&2 exit 1 fi google_source_pk="$(lookup_source_pk "$google_source_slug" || true)" password_stage_pk="$(ensure_password_stage)" identification_stage_pk="$(ensure_identification_stage "$password_stage_pk" "$google_source_pk")" user_login_stage_pk="$(ensure_user_login_stage)" authentication_flow_pk="$(ensure_authentication_flow)" ensure_flow_binding "$authentication_flow_pk" "$identification_stage_pk" 10 ensure_flow_binding "$authentication_flow_pk" "$user_login_stage_pk" 30 for provider_pk in "${provider_pks[@]}"; do api PATCH "/api/v3/providers/oauth2/${provider_pk}/" "$( jq -cn --arg flow "$authentication_flow_pk" '{authentication_flow: $flow}' )" >/dev/null done echo "Synced Burrow Tailnet authentication flow for providers ${provider_slugs_json}."