1
0
forked from baron/baron-sso
Files
baron-sso/scripts/auth_config.sh

431 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUTPUT_DIR="$ROOT_DIR/.generated"
OUTPUT_FILE="$OUTPUT_DIR/auth-config.env"
MODE="${1:-build}"
if [[ -f "$ROOT_DIR/.env" ]]; then
# shellcheck disable=SC1091
set -a
source "$ROOT_DIR/.env"
set +a
fi
USERFRONT_URL="${USERFRONT_URL:-http://localhost:5000}"
OATHKEEPER_PUBLIC_URL="${OATHKEEPER_PUBLIC_URL:-$USERFRONT_URL}"
HYDRA_PUBLIC_URL="${HYDRA_PUBLIC_URL:-${OATHKEEPER_PUBLIC_URL%/}/oidc}"
KRATOS_UI_URL="${KRATOS_UI_URL:-http://localhost:5000}"
ADMINFRONT_CALLBACK_URLS="${ADMINFRONT_CALLBACK_URLS:-http://localhost:5173/auth/callback}"
DEVFRONT_CALLBACK_URLS="${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/callback}"
KRATOS_ALLOWED_RETURN_URLS_EXTRA="${KRATOS_ALLOWED_RETURN_URLS_EXTRA:-}"
declare -a WARNINGS=()
fail() {
echo "[auth-config] ERROR: $1" >&2
exit 1
}
warn() {
WARNINGS+=("$1")
}
validate_dotenv_line_safety() {
local key="$1"
local env_file="$ROOT_DIR/.env"
[[ -f "$env_file" ]] || return 0
local raw_line
raw_line="$(grep -E "^${key}=" "$env_file" | tail -n 1 || true)"
[[ -n "$raw_line" ]] || return 0
if [[ "$raw_line" == *" #"* ]]; then
fail ".env line for $key contains inline comment. Use comment-only line above the key."
fi
if [[ "$raw_line" =~ [[:space:]]+$ ]]; then
fail ".env line for $key has trailing whitespace."
fi
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
csv_to_lines() {
local csv="$1"
printf '%s\n' "$csv" | tr ',' '\n' | while IFS= read -r raw; do
local item
item="$(trim "$raw")"
if [[ -n "$item" ]]; then
printf '%s\n' "$item"
fi
done
}
list_to_lines() {
local raw="$1"
raw="$(trim "$raw")"
if [[ -z "$raw" || "$raw" == "[]" ]]; then
return 0
fi
if [[ "$raw" =~ ^\[(.*)\]$ ]]; then
local inner="${BASH_REMATCH[1]}"
inner="$(trim "$inner")"
if [[ -z "$inner" ]]; then
return 0
fi
printf '%s\n' "$inner" | tr ',' '\n' | while IFS= read -r token; do
local item
item="$(trim "$token")"
item="${item#\"}"
item="${item%\"}"
item="${item#\'}"
item="${item%\'}"
item="$(trim "$item")"
if [[ -n "$item" ]]; then
printf '%s\n' "$item"
fi
done
return 0
fi
csv_to_lines "$raw"
}
is_http_url() {
local url="$1"
[[ "$url" =~ ^https?://[^[:space:]]+$ ]]
}
canonicalize_url() {
local url="$1"
if [[ "$url" =~ ^(https?://[^/]+)(/.*)?$ ]]; then
local origin="${BASH_REMATCH[1]}"
local path="${BASH_REMATCH[2]:-}"
if [[ -z "$path" || "$path" == "/" ]]; then
printf '%s' "$origin"
return
fi
path="${path%/}"
if [[ -z "$path" ]]; then
path="/"
fi
printf '%s%s' "$origin" "$path"
return
fi
printf '%s' "$url"
}
is_origin_like_url() {
local url="$1"
[[ "$url" =~ ^https?://[^/]+/?$ ]]
}
url_path() {
local url="$1"
if [[ "$url" =~ ^https?://[^/]+(/.*)?$ ]]; then
local path="${BASH_REMATCH[1]:-/}"
printf '%s' "$path"
return
fi
printf ''
}
json_escape() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
printf '%s' "$value"
}
join_csv() {
local -n arr_ref=$1
local out=""
local first=1
for item in "${arr_ref[@]}"; do
if [[ $first -eq 1 ]]; then
out="$item"
first=0
else
out="$out,$item"
fi
done
printf '%s' "$out"
}
to_json_array() {
local -n arr_ref=$1
local json="["
local first=1
for item in "${arr_ref[@]}"; do
local escaped
escaped="$(json_escape "$item")"
if [[ $first -eq 1 ]]; then
json="$json\"$escaped\""
first=0
else
json="$json,\"$escaped\""
fi
done
json="$json]"
printf '%s' "$json"
}
collect_values() {
declare -ga ADMIN_CALLBACKS=()
declare -ga DEV_CALLBACKS=()
declare -ga EXTRA_ALLOWED_RETURNS=()
while IFS= read -r item; do
ADMIN_CALLBACKS+=("$item")
done < <(csv_to_lines "$ADMINFRONT_CALLBACK_URLS")
while IFS= read -r item; do
DEV_CALLBACKS+=("$item")
done < <(csv_to_lines "$DEVFRONT_CALLBACK_URLS")
while IFS= read -r item; do
EXTRA_ALLOWED_RETURNS+=("$item")
done < <(list_to_lines "$KRATOS_ALLOWED_RETURN_URLS_EXTRA")
}
validate_urls() {
local key="$1"
local value="$2"
if ! is_http_url "$value"; then
fail "$key must be a valid http/https URL: $value"
fi
}
validate_callback_group() {
local group_name="$1"
local expected_path="$2"
shift 2
local urls=("$@")
if [[ ${#urls[@]} -eq 0 ]]; then
fail "$group_name is empty"
fi
local matched_expected=0
local has_path=0
for url in "${urls[@]}"; do
validate_urls "$group_name entry" "$url"
local canonical
canonical="$(canonicalize_url "$url")"
if [[ "$url" != "$canonical" ]]; then
fail "$group_name entry must not end with trailing slash: $url"
fi
local path
path="$(url_path "$canonical")"
if [[ -n "$path" && "$path" != "/" ]]; then
has_path=1
fi
if [[ "$path" == "$expected_path" ]]; then
matched_expected=1
fi
done
if [[ $has_path -eq 0 ]]; then
fail "$group_name must include path segment (callback path required)"
fi
if [[ $matched_expected -eq 0 ]]; then
warn "$group_name does not include recommended path: $expected_path"
fi
}
validate_gateway_mapping() {
validate_urls "HYDRA_PUBLIC_URL" "$HYDRA_PUBLIC_URL"
validate_urls "USERFRONT_URL" "$USERFRONT_URL"
validate_urls "KRATOS_UI_URL" "$KRATOS_UI_URL"
local mode=""
if [[ "$HYDRA_PUBLIC_URL" =~ ^https?://hydra(:[0-9]+)?(/|$) ]]; then
mode="direct_match"
else
mode="mapped_match"
if ! grep -Eq 'location /oidc' "$ROOT_DIR/gateway/nginx.conf"; then
mode="unmapped_fail"
fi
if ! grep -Eq 'rewrite \^/oidc/\(\.\*\)\$ /\$1 break;' "$ROOT_DIR/gateway/nginx.conf"; then
mode="unmapped_fail"
fi
if ! grep -Eq '"url": "<\.\*>://<\.\*>/oidc/oauth2/<\.\*>"' "$ROOT_DIR/docker/ory/oathkeeper/rules.json"; then
mode="unmapped_fail"
fi
if ! grep -Eq '"strip_path_prefix": "/oidc"' "$ROOT_DIR/docker/ory/oathkeeper/rules.json"; then
mode="unmapped_fail"
fi
fi
if [[ "$mode" == "unmapped_fail" ]]; then
fail "Public/Internal Hydra URL mismatch exists but gateway mapping rules are incomplete"
fi
OIDC_HYDRA_URL_MATCH_MODE="$mode"
}
build_allowed_return_urls() {
declare -ga KRATOS_ALLOWED_RETURN_URLS=()
declare -gA _seen_allowed=()
add_allowed_url() {
local candidate="$1"
candidate="$(trim "$candidate")"
[[ -z "$candidate" ]] && return
validate_urls "allowed_return_url" "$candidate"
candidate="$(canonicalize_url "$candidate")"
if [[ -z "${_seen_allowed[$candidate]:-}" ]]; then
KRATOS_ALLOWED_RETURN_URLS+=("$candidate")
_seen_allowed["$candidate"]=1
fi
}
add_allowed_with_slash_variant() {
local candidate="$1"
add_allowed_url "$candidate"
local normalized
normalized="$(canonicalize_url "$candidate")"
if is_origin_like_url "$candidate"; then
add_allowed_url "${normalized}/"
fi
}
add_allowed_with_slash_variant "$KRATOS_UI_URL"
add_allowed_with_slash_variant "$USERFRONT_URL"
for url in "${ADMIN_CALLBACKS[@]}"; do
add_allowed_url "$url"
done
for url in "${DEV_CALLBACKS[@]}"; do
add_allowed_url "$url"
done
for url in "${EXTRA_ALLOWED_RETURNS[@]}"; do
add_allowed_url "$url"
done
if [[ ${#KRATOS_ALLOWED_RETURN_URLS[@]} -eq 0 ]]; then
fail "KRATOS allowed return URL list is empty"
fi
}
write_output() {
mkdir -p "$OUTPUT_DIR"
local admin_csv dev_csv returns_json
admin_csv="$(join_csv ADMIN_CALLBACKS)"
dev_csv="$(join_csv DEV_CALLBACKS)"
returns_json="$(to_json_array KRATOS_ALLOWED_RETURN_URLS)"
cat >"$OUTPUT_FILE" <<EOF
# Generated by scripts/auth_config.sh
# Do not edit manually.
ADMINFRONT_CALLBACK_URLS=$admin_csv
DEVFRONT_CALLBACK_URLS=$dev_csv
KRATOS_ALLOWED_RETURN_URLS_JSON=$returns_json
OIDC_HYDRA_URL_MATCH_MODE=$OIDC_HYDRA_URL_MATCH_MODE
EOF
}
validate_compose_wiring() {
grep -Eq 'KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=\$\{KRATOS_ALLOWED_RETURN_URLS_JSON' "$ROOT_DIR/compose.ory.yaml" \
|| fail "compose.ory.yaml is not wired to KRATOS_ALLOWED_RETURN_URLS_JSON"
grep -Eq 'ADMINFRONT_CALLBACK_URLS' "$ROOT_DIR/compose.ory.yaml" \
|| fail "compose.ory.yaml is not wired to ADMINFRONT_CALLBACK_URLS"
grep -Eq 'DEVFRONT_CALLBACK_URLS' "$ROOT_DIR/compose.ory.yaml" \
|| fail "compose.ory.yaml is not wired to DEVFRONT_CALLBACK_URLS"
}
verify_runtime_hydra_clients() {
if ! command -v docker >/dev/null 2>&1; then
warn "docker command is unavailable; runtime hydra verification skipped"
return
fi
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^ory_hydra$'; then
warn "ory_hydra is not running; runtime hydra verification skipped"
return
fi
local admin_info dev_info
if ! admin_info="$(docker exec ory_hydra hydra get oauth2-client --endpoint http://hydra:4445 adminfront 2>/dev/null)"; then
fail "failed to read hydra client 'adminfront' from running container"
fi
if ! dev_info="$(docker exec ory_hydra hydra get oauth2-client --endpoint http://hydra:4445 devfront 2>/dev/null)"; then
fail "failed to read hydra client 'devfront' from running container"
fi
for callback in "${ADMIN_CALLBACKS[@]}"; do
if ! grep -Fq "$callback" <<<"$admin_info"; then
fail "adminfront hydra client does not include callback: $callback"
fi
done
for callback in "${DEV_CALLBACKS[@]}"; do
if ! grep -Fq "$callback" <<<"$dev_info"; then
fail "devfront hydra client does not include callback: $callback"
fi
done
}
run_validation() {
validate_dotenv_line_safety "USERFRONT_URL"
validate_dotenv_line_safety "BACKEND_URL"
validate_dotenv_line_safety "OATHKEEPER_PUBLIC_URL"
validate_dotenv_line_safety "HYDRA_PUBLIC_URL"
validate_dotenv_line_safety "KRATOS_BROWSER_URL"
validate_dotenv_line_safety "KRATOS_UI_URL"
validate_dotenv_line_safety "ADMINFRONT_CALLBACK_URLS"
validate_dotenv_line_safety "DEVFRONT_CALLBACK_URLS"
collect_values
validate_callback_group "ADMINFRONT_CALLBACK_URLS" "/auth/callback" "${ADMIN_CALLBACKS[@]}"
validate_callback_group "DEVFRONT_CALLBACK_URLS" "/callback" "${DEV_CALLBACKS[@]}"
validate_gateway_mapping
build_allowed_return_urls
}
print_summary() {
echo "[auth-config] mode: $MODE"
echo "[auth-config] hydra_url_match_mode: $OIDC_HYDRA_URL_MATCH_MODE"
echo "[auth-config] admin_callbacks: $(join_csv ADMIN_CALLBACKS)"
echo "[auth-config] dev_callbacks: $(join_csv DEV_CALLBACKS)"
echo "[auth-config] kratos_allowed_return_urls_count: ${#KRATOS_ALLOWED_RETURN_URLS[@]}"
if [[ ${#WARNINGS[@]} -gt 0 ]]; then
for message in "${WARNINGS[@]}"; do
echo "[auth-config] WARN: $message"
done
fi
}
case "$MODE" in
build)
run_validation
write_output
print_summary
echo "[auth-config] wrote: $OUTPUT_FILE"
;;
validate)
run_validation
print_summary
;;
verify)
run_validation
validate_compose_wiring
verify_runtime_hydra_clients
print_summary
echo "[auth-config] compose wiring verified"
;;
*)
fail "Unsupported mode: $MODE (supported: build|validate|verify)"
;;
esac