Files
Portainer/authentik/create-auth-app.sh
T
Eduardo David Paredes Vara 61134d7834 feat(authentik): añadir script de creación de aplicaciones OAuth
- create-auth-app.sh: script interactivo para crear providers y
  aplicaciones en Authentik vía API, automatizando la integración SSO
- docker-compose.yml y stack.env: ajustes del stack
- README.md: documentación actualizada

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 05:47:33 +00:00

328 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# ============================================================
# create-auth-app.sh — Crea una aplicación protegida por
# Authentik (Proxy Provider + Application + Outpost) vía API
# ============================================================
#
# Uso:
# ./create-auth-app.sh <dominio> [nombre] [slug]
#
# Ejemplos:
# ./create-auth-app.sh crowdsec.sherlockhomeless.net "CrowdSec" crowdsec
# ./create-auth-app.sh mi-app.midominio.com
#
# Requisitos:
# - Contenedor ths-authentik-server corriendo y healthy
# - jq instalado (sudo apt install jq)
# ============================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${SCRIPT_DIR}/.env"
# -----------------------------------------------------------
# Cargar configuración
# -----------------------------------------------------------
AK_HOST_DEFAULT="https://auth.sherlockhomeless.net"
AK_CONTAINER="ths-authentik-server"
if [ -f "${ENV_FILE}" ]; then
set -a; source "${ENV_FILE}"; set +a
fi
AK_HOST="${AUTHENTIK_DOMAIN:-$AK_HOST_DEFAULT}"
AK_HOST="https://${AK_HOST#https://}" # normalizar
# -----------------------------------------------------------
# Parámetros
# -----------------------------------------------------------
DOMAIN="${1:-}"
APP_NAME="${2:-}"
APP_SLUG="${3:-}"
if [ -z "$DOMAIN" ]; then
echo "Uso: $0 <dominio> [nombre] [slug]"
echo ""
echo "Ejemplos:"
echo " $0 crowdsec.sherlockhomeless.net CrowdSec crowdsec"
echo " $0 mi-app.midominio.com"
echo ""
echo "Si no se especifica nombre/slug, se derivan del dominio."
exit 1
fi
# Derivar nombre y slug del dominio si no se especificaron
if [ -z "$APP_NAME" ]; then
# Extraer subdominio: mi-app.midominio.com -> Mi App
SUB=$(echo "$DOMAIN" | cut -d. -f1 | sed 's/-/ /g' | sed 's/\b\(.\)/\u\1/g')
APP_NAME="$SUB"
fi
if [ -z "$APP_SLUG" ]; then
APP_SLUG=$(echo "$DOMAIN" | cut -d. -f1 | tr '[:upper:]' '[:lower:]')
fi
EXTERNAL_HOST="https://${DOMAIN}"
echo "========================================="
echo " Authentik — Crear Application"
echo "========================================="
echo " Dominio: ${DOMAIN}"
echo " External host: ${EXTERNAL_HOST}"
echo " Nombre app: ${APP_NAME}"
echo " Slug: ${APP_SLUG}"
echo "========================================="
echo ""
# -----------------------------------------------------------
# Verificar dependencias
# -----------------------------------------------------------
if ! command -v jq &>/dev/null; then
echo "ERROR: jq no está instalado. Instálalo con: sudo apt install jq"
exit 1
fi
if ! docker ps --format '{{.Names}}' | grep -q "^${AK_CONTAINER}$"; then
echo "ERROR: El contenedor '${AK_CONTAINER}' no está corriendo."
exit 1
fi
# -----------------------------------------------------------
# 1. Generar token temporal de API
# -----------------------------------------------------------
echo "[1/6] Generando token temporal de API..."
TOKEN_SCRIPT="
from authentik.core.models import Token, User
user = User.objects.filter(username='akadmin').first()
if not user:
print('ERROR:NO_USER')
else:
token, _ = Token.objects.update_or_create(
user=user,
identifier='setup-script',
defaults={'intent': 'api', 'expiring': True, 'expires': None, 'description': 'Token temporal para create-auth-app.sh'}
)
print(token.key)
"
AK_TOKEN=$(docker exec "$AK_CONTAINER" bash -c "echo \"$TOKEN_SCRIPT\" | ak shell 2>/dev/null" | grep -v '^###' | grep -v '^$' | grep -v 'objects imported' | tail -1)
if [ -z "$AK_TOKEN" ] || [[ "$AK_TOKEN" == ERROR:* ]]; then
echo "ERROR: No se pudo generar el token de API. ¿Existe el usuario akadmin?"
echo "Respuesta: $AK_TOKEN"
exit 1
fi
echo " Token obtenido correctamente."
# -----------------------------------------------------------
# 2. Obtener el authorization flow
# -----------------------------------------------------------
echo "[2/6] Buscando authorization flow (implicit-consent)..."
FLOW_JSON=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \
"${AK_HOST}/api/v3/flows/instances/?slug=default-provider-authorization-implicit-consent" 2>/dev/null)
FLOW_UUID=$(echo "$FLOW_JSON" | jq -r '.results[0].pk // empty')
if [ -z "$FLOW_UUID" ]; then
echo "ERROR: No se encontró el flow 'default-provider-authorization-implicit-consent'."
exit 1
fi
echo " Flow UUID: ${FLOW_UUID}"
# Buscar un provider existente para copiar sus property_mappings e invalidation_flow
EXISTING_PROVIDER=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \
"${AK_HOST}/api/v3/providers/proxy/?page_size=1" 2>/dev/null)
INVAL_FLOW=$(echo "$EXISTING_PROVIDER" | jq -r '.results[0].invalidation_flow // empty')
PROPERTY_MAPPINGS=$(echo "$EXISTING_PROVIDER" | jq -c '.results[0].property_mappings // []')
# -----------------------------------------------------------
# 3. Verificar si ya existe un provider para este dominio
# -----------------------------------------------------------
echo "[3/6] Verificando si ya existe un provider para ${EXTERNAL_HOST}..."
# Listar todos los providers y filtrar por external_host exacto (el API no filtra por este campo)
EXISTING_PK=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \
"${AK_HOST}/api/v3/providers/proxy/?page_size=100" 2>/dev/null | jq -r --arg HOST "${EXTERNAL_HOST}" '.results[] | select(.external_host == $HOST) | .pk' 2>/dev/null)
if [ -n "$EXISTING_PK" ]; then
echo " Ya existe un provider (pk=${EXISTING_PK}). Saltando creación."
PROVIDER_PK="$EXISTING_PK"
else
# Crear provider
PROVIDER_JSON=$(curl -sk -X POST "${AK_HOST}/api/v3/providers/proxy/" \
-H "Authorization: Bearer $AK_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Provider for ${APP_NAME}\",
\"authentication_flow\": null,
\"authorization_flow\": \"${FLOW_UUID}\",
\"invalidation_flow\": \"${INVAL_FLOW}\",
\"property_mappings\": ${PROPERTY_MAPPINGS},
\"external_host\": \"${EXTERNAL_HOST}\",
\"internal_host\": \"\",
\"internal_host_ssl_validation\": true,
\"mode\": \"forward_single\",
\"intercept_header_auth\": true,
\"cookie_domain\": \"\",
\"access_token_validity\": \"hours=1\",
\"refresh_token_validity\": \"days=30\"
}" 2>/dev/null)
PROVIDER_PK=$(echo "$PROVIDER_JSON" | jq -r '.pk // empty')
if [ -z "$PROVIDER_PK" ]; then
echo "ERROR: No se pudo crear el provider."
echo "Respuesta: $PROVIDER_JSON"
exit 1
fi
echo " Provider creado: pk=${PROVIDER_PK}"
fi
# -----------------------------------------------------------
# 4. Verificar si ya existe una aplicación con este slug
# -----------------------------------------------------------
echo "[4/6] Verificando si ya existe la aplicación '${APP_SLUG}'..."
EXISTING_APP=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \
"${AK_HOST}/api/v3/core/applications/?slug=${APP_SLUG}" 2>/dev/null | jq -r '.results[0].pk // empty')
if [ -n "$EXISTING_APP" ]; then
echo " Ya existe la aplicación (pk=${EXISTING_APP}). Saltando creación."
APP_PK="$EXISTING_APP"
else
# Crear aplicación
APP_JSON=$(curl -sk -X POST "${AK_HOST}/api/v3/core/applications/" \
-H "Authorization: Bearer $AK_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"${APP_NAME}\",
\"slug\": \"${APP_SLUG}\",
\"provider\": ${PROVIDER_PK},
\"policy_engine_mode\": \"any\",
\"meta_launch_url\": \"${EXTERNAL_HOST}\"
}" 2>/dev/null)
APP_PK=$(echo "$APP_JSON" | jq -r '.pk // empty')
if [ -z "$APP_PK" ]; then
echo "ERROR: No se pudo crear la aplicación."
echo "Respuesta: $APP_JSON"
exit 1
fi
echo " Aplicación creada: pk=${APP_PK}"
fi
# -----------------------------------------------------------
# 5. Copiar policy binding de una app existente
# -----------------------------------------------------------
echo "[5/6] Aplicando policy binding..."
REFERENCE_APP_POLICY=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \
"${AK_HOST}/api/v3/policies/bindings/?target=${APP_PK}" 2>/dev/null | jq -r '.results[0].policy // empty')
if [ -n "$REFERENCE_APP_POLICY" ]; then
echo " La app ya tiene un binding. Saltando."
else
# Tomar la policy del primer binding de cualquier app existente
SAMPLE_APP=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \
"${AK_HOST}/api/v3/core/applications/?page_size=5" 2>/dev/null | jq -r '.results[] | select(.slug != "'"${APP_SLUG}"'") | .pk' | head -1)
POLICY_PK=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \
"${AK_HOST}/api/v3/policies/bindings/?target=${SAMPLE_APP}" 2>/dev/null | jq -r '.results[0].policy // empty')
if [ -n "$POLICY_PK" ]; then
curl -sk -X POST "${AK_HOST}/api/v3/policies/bindings/" \
-H "Authorization: Bearer $AK_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"policy\": \"${POLICY_PK}\", \"target\": \"${APP_PK}\", \"enabled\": true, \"order\": 0}" 2>/dev/null > /dev/null
echo " Policy binding creado (policy=${POLICY_PK})."
else
echo " WARN: No se encontró una policy de referencia. Añade el binding manualmente en Authentik."
fi
fi
# -----------------------------------------------------------
# 6. Obtener el Embedded Outpost
# -----------------------------------------------------------
echo "[6/7] Buscando el Embedded Outpost..."
OUTPOST_JSON=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \
"${AK_HOST}/api/v3/outposts/instances/?name=authentik%20Embedded%20Outpost" 2>/dev/null)
OUTPOST_UUID=$(echo "$OUTPOST_JSON" | jq -r '.results[0].pk // empty')
if [ -z "$OUTPOST_UUID" ]; then
echo "ERROR: No se encontró el Embedded Outpost."
exit 1
fi
echo " Outpost UUID: ${OUTPOST_UUID}"
# -----------------------------------------------------------
# 6. Añadir provider al outpost
# -----------------------------------------------------------
echo "[7/7] Añadiendo provider al outpost..."
CURRENT_PROVIDERS=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \
"${AK_HOST}/api/v3/outposts/instances/${OUTPOST_UUID}/" 2>/dev/null | jq -r '.providers | join(",")')
# Verificar si el provider ya está en la lista
if echo "$CURRENT_PROVIDERS" | grep -qw "${PROVIDER_PK}"; then
echo " El provider ya está asociado al outpost."
else
NEW_PROVIDERS="${CURRENT_PROVIDERS},${PROVIDER_PK}"
# Convertir a array JSON
PROVIDER_ARRAY=$(echo "$NEW_PROVIDERS" | tr ',' '\n' | jq -R 'tonumber' | jq -s '.')
curl -sk -X PATCH "${AK_HOST}/api/v3/outposts/instances/${OUTPOST_UUID}/" \
-H "Authorization: Bearer $AK_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"providers\": ${PROVIDER_ARRAY}}" 2>/dev/null > /dev/null
echo " Provider añadido al outpost."
fi
# -----------------------------------------------------------
# Verificación
# -----------------------------------------------------------
echo ""
echo "========================================="
echo " Verificación"
echo "========================================="
HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "${EXTERNAL_HOST}/" 2>/dev/null)
REDIRECT=$(curl -sk -o /dev/null -w "%{redirect_url}" "${EXTERNAL_HOST}/" 2>/dev/null)
if [ "$HTTP_CODE" = "302" ] && echo "$REDIRECT" | grep -q "/application/o/authorize/"; then
echo " OK: ${EXTERNAL_HOST} redirige a Authentik (302)"
elif [ "$HTTP_CODE" = "502" ] || [ "$HTTP_CODE" = "503" ]; then
echo " WARN: El servicio backend no responde (HTTP ${HTTP_CODE})."
echo " La protección Authentik funciona, pero el servicio no está UP."
elif [ "$HTTP_CODE" = "200" ] && echo "$REDIRECT" | grep -q "authentik"; then
echo " WARN: Redirige a Authentik pero con código 200."
echo " Puede que necesites loguearte primero."
else
echo " INFO: HTTP ${HTTP_CODE}"
echo " Verifica manualmente: curl -kI ${EXTERNAL_HOST}"
fi
echo ""
echo "========================================="
echo " Resumen"
echo "========================================="
echo " Provider: Provider for ${APP_NAME} (pk=${PROVIDER_PK})"
echo " Application: ${APP_NAME} (slug=${APP_SLUG}, pk=${APP_PK})"
echo " Outpost: authentik Embedded Outpost (${OUTPOST_UUID})"
echo " URL: ${EXTERNAL_HOST}"
echo "========================================="
echo ""
echo "Listo. El middleware 'ths-authentik@docker' ya debería proteger ${EXTERNAL_HOST}."
echo "Si usas CrowdSec también, añade: crowdsec-bouncer@file,ths-authentik@docker"