From dcdc69ebfe60a8f539317ab9d77f6667dd1c5e48 Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Thu, 19 Feb 2026 14:28:48 +0900 Subject: [PATCH] =?UTF-8?q?feature/i18n=20=EC=BD=94=EB=93=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20&=20=EC=9D=B8=ED=94=84=EB=9D=BC=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.sample | 8 + .gitignore | 1 + Makefile | 37 +- README.md | 31 +- compose.ory.yaml | 10 +- ...oidc_redirect_mapping_validation_policy.md | 67 ++++ docs/ory-usage.md | 10 + scripts/auth_config.sh | 366 ++++++++++++++++++ 8 files changed, 515 insertions(+), 15 deletions(-) create mode 100644 docs/oidc_redirect_mapping_validation_policy.md create mode 100755 scripts/auth_config.sh diff --git a/.env.sample b/.env.sample index bd8f46b3..33a261d8 100644 --- a/.env.sample +++ b/.env.sample @@ -110,6 +110,14 @@ HYDRA_ADMIN_URL=http://hydra:4445 # Oathkeeper가 /oidc 경로를 Hydra Public API로 라우팅합니다. HYDRA_PUBLIC_URL=${OATHKEEPER_PUBLIC_URL}/oidc +# OIDC 클라이언트 callback (콤마 구분) +ADMINFRONT_CALLBACK_URLS=http://localhost:5173/auth/callback +DEVFRONT_CALLBACK_URLS=http://localhost:5174/callback + +# Kratos allowed_return_urls 확장 목록 (콤마 구분, 선택) +# 기본값은 KRATOS_UI_URL, USERFRONT_URL, 각 callback URL을 자동 포함합니다. +KRATOS_ALLOWED_RETURN_URLS_EXTRA= + # Oathkeeper JWKS (내부 통신용) JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json diff --git a/.gitignore b/.gitignore index 31b94f32..b1d8d029 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .vscode/ .codex .codex/ +.generated/ *.swp *.log *.out diff --git a/Makefile b/Makefile index de3e624d..b3d390b5 100644 --- a/Makefile +++ b/Makefile @@ -10,29 +10,50 @@ endif COMPOSE_INFRA := compose.infra.yaml COMPOSE_ORY := compose.ory.yaml COMPOSE_APP := docker-compose.yaml +AUTH_CONFIG_ENV := .generated/auth-config.env + +COMPOSE_ENV_FILES := +ifneq (,$(wildcard ./.env)) +COMPOSE_ENV_FILES += --env-file .env +endif +COMPOSE_ENV_FILES += --env-file $(AUTH_CONFIG_ENV) + +# --- 인증 설정 빌드/검증 --- +build-auth-config: + @echo "Building auth config..." + @mkdir -p .generated + @bash scripts/auth_config.sh build + +validate-auth-config: build-auth-config + @echo "Validating auth config..." + @bash scripts/auth_config.sh validate + +verify-auth-config: validate-auth-config + @echo "Verifying auth config wiring..." + @bash scripts/auth_config.sh verify # --- 기본 실행 --- # 주의: --remove-orphan 사용 금지 (다른 스택이 orphan으로 판단되어 종료될 수 있음) -up-all: +up-all: validate-auth-config @echo "Starting ALL stacks (infra + ory + app)..." - docker compose -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up -d + docker compose $(COMPOSE_ENV_FILES) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up -d # --- 개별 스택 실행 --- up-infra: @echo "Starting Infra stack (postgres/clickhouse/redis)..." docker compose -f $(COMPOSE_INFRA) up -d -up-ory: +up-ory: validate-auth-config @echo "Starting Ory stack (kratos/hydra/keto/oathkeeper)..." - docker compose -f $(COMPOSE_ORY) up -d + docker compose $(COMPOSE_ENV_FILES) -f $(COMPOSE_ORY) up -d -up-app: +up-app: validate-auth-config @echo "Starting App stack (backend/userfront/adminfront/devfront)..." - docker compose -f $(COMPOSE_APP) up -d + docker compose $(COMPOSE_ENV_FILES) -f $(COMPOSE_APP) up -d -up-backend: +up-backend: validate-auth-config @echo "Starting Backend only..." - docker compose -f $(COMPOSE_APP) up -d backend + docker compose $(COMPOSE_ENV_FILES) -f $(COMPOSE_APP) up -d backend up-dev: up-infra up-ory @echo "Dev stack is up (infra + ory)." diff --git a/README.md b/README.md index 5268d366..c65edbc1 100644 --- a/README.md +++ b/README.md @@ -178,15 +178,40 @@ docker network create public_net #서비스용 #### 2. 인프라 및 Ory Stack 실행 데이터베이스와 Ory 서비스(Kratos, Hydra, Keto 등)를 실행합니다. ```bash -docker compose -f compose.infra.yaml -f compose.ory.yaml up -d +# 권장: Make 실행 (인증 설정 검증 포함) +make up-dev ``` #### 3. 애플리케이션 실행 userfront와 backend 서비스를 실행합니다. ```bash -docker compose -f docker-compose.yaml up -d +make up-app +``` +(또는 전체 스택 한번에 실행: `make up-all`) + +### Make 기반 인증 설정 검증 (권장) +`up-*` 타깃은 실행 전 인증 리다이렉트 설정을 자동 검증합니다. + +```bash +# 1) 인증 설정 생성 +make build-auth-config + +# 2) 정적 검증 (callback / allowed_return_urls / 게이트웨이 매핑) +make validate-auth-config + +# 3) 배선 + (가능 시) 런타임 Hydra client 검증 +make verify-auth-config +``` + +- 생성 파일: `.generated/auth-config.env` (compose 실행 시 자동 주입) +- 게이트웨이 경유 환경은 URL 문자열 완전일치 대신 매핑 유효성(`direct_match` / `mapped_match`) 기준으로 검증합니다. +- 관련 정책 문서: `docs/oidc_redirect_mapping_validation_policy.md` + +직접 Compose를 사용하려면 다음처럼 env 파일을 함께 주입하세요. +```bash +docker compose --env-file .env --env-file .generated/auth-config.env -f compose.infra.yaml -f compose.ory.yaml up -d +docker compose --env-file .env --env-file .generated/auth-config.env -f docker-compose.yaml up -d ``` -(또는 한번에 실행: `docker compose -f compose.infra.yaml -f compose.ory.yaml -f docker-compose.yaml up -d`) - **gateway (UserFront 프록시)**: http://localhost:5000 접속 - **backend**: http://localhost:3000 (API) diff --git a/compose.ory.yaml b/compose.ory.yaml index 3fa4d71c..991842fa 100644 --- a/compose.ory.yaml +++ b/compose.ory.yaml @@ -29,7 +29,7 @@ services: - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL:-http://localhost:4433} - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000} - - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=["${KRATOS_UI_URL:-http://localhost:5000}","${USERFRONT_URL:-http://localhost:5000}"] + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["http://localhost:5000","http://localhost:5000/"]} - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error?error=settings_disabled - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/recovery @@ -55,7 +55,7 @@ services: - KRATOS_SERVE_PUBLIC_BASE_URL=${KRATOS_BROWSER_URL:-http://localhost:4433} - KRATOS_SERVE_ADMIN_BASE_URL=${KRATOS_ADMIN_URL:-http://kratos:4434} - KRATOS_SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=${KRATOS_UI_URL:-http://localhost:5000} - - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=["${KRATOS_UI_URL:-http://localhost:5000}","${USERFRONT_URL:-http://localhost:5000}"] + - KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON:-["http://localhost:5000","http://localhost:5000/"]} - KRATOS_SELFSERVICE_FLOWS_ERROR_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error - KRATOS_SELFSERVICE_FLOWS_SETTINGS_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/error?error=settings_disabled - KRATOS_SELFSERVICE_FLOWS_RECOVERY_UI_URL=${KRATOS_UI_URL:-http://localhost:5000}/recovery @@ -203,6 +203,8 @@ services: - HYDRA_ADMIN_URL=http://hydra:4445 - OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} - OATHKEEPER_INTROSPECT_CLIENT_SECRET=${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oathkeeper-secret} + - ADMINFRONT_CALLBACK_URLS=${ADMINFRONT_CALLBACK_URLS:-http://localhost:5173/auth/callback} + - DEVFRONT_CALLBACK_URLS=${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/callback} command: | hydra clients create \ --endpoint http://hydra:4445 \ @@ -211,7 +213,7 @@ services: --grant-types authorization_code,refresh_token \ --response-types code \ --scope openid,offline_access,profile,email \ - --callbacks http://localhost:5000/callback; + --callbacks "$ADMINFRONT_CALLBACK_URLS"; hydra clients create \ --endpoint http://hydra:4445 \ @@ -221,7 +223,7 @@ services: --scope openid,offline_access,profile,email \ --token-endpoint-auth-method none \ --response-types code \ - --callbacks http://localhost:5174/callback; + --callbacks "$DEVFRONT_CALLBACK_URLS"; hydra clients create \ --endpoint http://hydra:4445 \ diff --git a/docs/oidc_redirect_mapping_validation_policy.md b/docs/oidc_redirect_mapping_validation_policy.md new file mode 100644 index 00000000..f71a1680 --- /dev/null +++ b/docs/oidc_redirect_mapping_validation_policy.md @@ -0,0 +1,67 @@ +# OIDC Redirect 매핑 검증 정책 + +## 목적 +- OIDC 로그인/리다이렉트 검증 시, URL 문자열의 기계적 동일성만으로 정상/비정상을 판단하지 않습니다. +- Gateway(Oathkeeper/Nginx) 경유 구조에서 발생하는 Public URL과 Internal URL의 의도된 차이를 정책적으로 허용하되, 매핑의 유효성은 엄격히 검증합니다. + +## 적용 범위 +- UserFront, AdminFront, DevFront의 로그인/콜백 경로 +- Ory Stack(Hydra/Kratos/Oathkeeper) 설정 +- `compose.ory.yaml`, `gateway/nginx.conf`, `docker/ory/oathkeeper/rules*.json` +- `Makefile` 기반 사전 검증/스모크 검증 단계 + +## 핵심 원칙 +1. URL 직접 일치(`direct_match`)와 게이트웨이 매핑 일치(`mapped_match`)를 구분합니다. +2. Public URL과 Internal URL이 다르더라도, `Public -> Gateway/Oathkeeper -> Internal` 경로가 검증되면 정상으로 간주합니다. +3. 매핑 체인이 없거나 규칙이 누락된 경우는 실패(`unmapped_fail`)로 간주합니다. + +## 용어 정의 +- Public URL: 브라우저에서 접근하는 URL. 예: `https://sso-test.hmac.kr/oidc/oauth2/auth` +- Internal URL: 컨테이너 내부 통신 URL. 예: `http://hydra:4444/oauth2/auth` +- Mapping Chain: Public 요청이 Gateway/Oathkeeper 규칙을 통해 Internal URL로 전달되는 경로 + +## 판정 규칙 +1. `direct_match` +- 프론트가 생성한 `redirect_uri`/`authority`와 실제 등록/설정 URL이 동일 레이어에서 직접 일치 +- 예: 로컬 개발에서 모두 `http://localhost:*` 기준 + +2. `mapped_match` +- Public URL과 Internal URL이 다르지만, 아래가 모두 성립 +- Gateway 라우팅 규칙 존재 (예: `/oidc` rewrite) +- Oathkeeper `match`와 `upstream` 규칙 존재 (예: `strip_path_prefix=/oidc`) +- 최종 업스트림이 기대 서비스(Hydra/Kratos)로 연결 + +3. `unmapped_fail` +- Public/Internal 불일치가 있는데 매핑 규칙이 없거나 누락 +- callback/return URL이 등록되지 않았거나 경로 규약 불일치 +- 환경 변수는 존재하나 실제 compose/rules 반영이 누락 + +## 검증 항목 +1. 정적 검증 (`make validate-auth-config`) +- `USERFRONT_URL`, `OATHKEEPER_PUBLIC_URL`, `HYDRA_PUBLIC_URL`, `KRATOS_BROWSER_URL` 정합성 +- `ADMINFRONT_CALLBACK_URLS`, `DEVFRONT_CALLBACK_URLS` URL 유효성/중복/경로 규약 +- Gateway `/oidc`, `/auth` 라우팅 규칙 존재 여부 +- Oathkeeper `rules*.json`의 Hydra/Kratos 매핑 규칙 존재 여부 + +2. 런타임 검증 (`make verify-oidc-config`) +- OIDC Discovery endpoint 조회 가능 여부 +- Hydra 등록 client(`adminfront`, `devfront`)의 `redirect_uris` 확인 +- 필요 시 Gateway 경유 endpoint probe로 매핑 체인 확인 + +## 경로 규약 +- DevFront callback: `/callback` +- AdminFront callback: `/auth/callback` +- UserFront OIDC 진입점: `/oidc/*` (Gateway 경유) + +## 운영 지침 +1. 환경별 URL은 동일할 필요가 없고, 매핑 체인이 검증 가능해야 합니다. +2. `localhost` 하드코딩은 로컬 전용 예외로만 허용하며, 스테이징/운영은 env 기반으로 주입합니다. +3. 신규 도메인/경로 추가 시, 프론트 설정과 Ory/Gateway 규칙을 반드시 동시에 변경하고 검증 결과를 이슈/PR에 첨부합니다. + +## 관련 이슈 +- #262 +- #269 +- #271 +- #272 +- #274 +- #276 diff --git a/docs/ory-usage.md b/docs/ory-usage.md index 7f381071..e82c702d 100644 --- a/docs/ory-usage.md +++ b/docs/ory-usage.md @@ -10,6 +10,9 @@ ## 2) 실행 방법 ```bash +# 인증 리다이렉트 설정 생성/검증 +make validate-auth-config + # 인프라 + Ory Stack docker compose -f compose.infra.yaml -f compose.ory.yaml up -d @@ -17,6 +20,13 @@ docker compose -f compose.infra.yaml -f compose.ory.yaml up -d docker compose -f docker-compose.yaml up -d ``` +Make 기반 실행을 사용할 경우: +```bash +make up-ory +make up-app +``` +`up-*` 타깃은 내부적으로 `validate-auth-config`를 선행 수행하여 callback/allowed_return_urls 정합성을 먼저 검증합니다. + ## 3) 내부 통신 vs 브라우저 접근용 URL 분리 Ory 구성은 **컨테이너 내부 통신 URL**과 **브라우저 접근 URL**을 분리해야 합니다. diff --git a/scripts/auth_config.sh b/scripts/auth_config.sh new file mode 100755 index 00000000..f4c3e4e5 --- /dev/null +++ b/scripts/auth_config.sh @@ -0,0 +1,366 @@ +#!/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") +} + +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 +} + +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 < <(csv_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 path + path="$(url_path "$url")" + 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" </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 clients get --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 clients get --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() { + 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