#!/usr/bin/env bash set -euo pipefail BASE_URL="${BASE_URL:-http://127.0.0.1:3000}" DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-auth-security}" ADMIN_IDENTIFIER="${ADMIN_IDENTIFIER:-admin@hyinda.com}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-AdminPassword1234}" LOGIN_FAIL_MAX_ATTEMPTS="${LOGIN_FAIL_MAX_ATTEMPTS:-${AUTH_LOGIN_FAIL_MAX_ATTEMPTS:-5}}" RUN_AUTH_POSTGRES_CONCURRENCY="${RUN_AUTH_POSTGRES_CONCURRENCY:-0}" case "$RUN_AUTH_POSTGRES_CONCURRENCY" in 0|1) ;; *) echo "[AUTH-REGRESSION][FAIL] invalid_concurrency_mode" >&2 exit 1 ;; esac if ! command -v curl >/dev/null 2>&1; then echo "[ERROR] curl not found" >&2 exit 1 fi if ! command -v jq >/dev/null 2>&1; then echo "[ERROR] jq not found" >&2 exit 1 fi if ! command -v node >/dev/null 2>&1; then echo "[ERROR] node not found" >&2 exit 1 fi SERVER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" if ! node -e ' try { require(process.argv[1]).validateAuthRegressionEnvironment(process.env); } catch { process.exit(1); } ' "$SERVER_DIR/src/scripts/check-auth-postgres-concurrency.js"; then echo "[AUTH-REGRESSION][FAIL] environment_guard_rejected" >&2 exit 1 fi TMP_DIR="$(mktemp -d)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT extract_json_field() { local file_path="$1" local jq_expr="$2" jq -er "$jq_expr" "$file_path" } assert_status() { local actual="$1" local expected="$2" local name="$3" if [[ "$actual" != "$expected" ]]; then echo "[ERROR] $name expected HTTP $expected but got $actual" >&2 exit 1 fi } assert_error_code() { local file_path="$1" local expected_code="$2" local name="$3" local actual_code actual_code="$(jq -r '.error.code // empty' "$file_path")" if [[ "$actual_code" != "$expected_code" ]]; then echo "[ERROR] $name expected error code $expected_code but got $actual_code" >&2 exit 1 fi } login() { local identifier="$1" local password="$2" local fingerprint="$3" local out_body="$4" local status status="$(curl -sS -o "$out_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/auth/login" \ -H "content-type: application/json" \ -H "x-device-fingerprint: $fingerprint" \ -d "{\"identifier\":\"$identifier\",\"password\":\"$password\"}")" echo "$status" } echo "[1/8] Login admin" admin_login_body="$TMP_DIR/admin-login.json" admin_login_status="$(login "$ADMIN_IDENTIFIER" "$ADMIN_PASSWORD" "${DEVICE_FINGERPRINT}-admin" "$admin_login_body")" assert_status "$admin_login_status" "200" "admin login" "$admin_login_body" ADMIN_TOKEN="$(extract_json_field "$admin_login_body" '.data.accessToken')" echo "[2/8] Weak password should be rejected" weak_employee_no="W$(date +%s)$RANDOM" weak_employee_email="weak_${weak_employee_no,,}@hyinda.com" weak_employee_phone="139${RANDOM}${RANDOM}" weak_create_body="$TMP_DIR/weak-create.json" weak_create_status="$(curl -sS -o "$weak_create_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/users" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d "{\"employeeNo\":\"$weak_employee_no\",\"name\":\"Weak User\",\"phone\":\"$weak_employee_phone\",\"email\":\"$weak_employee_email\",\"title\":\"测试\",\"directManagerId\":null,\"hireDate\":\"2020-01-01\",\"roles\":[\"employee\"],\"password\":\"Password123456\"}")" assert_status "$weak_create_status" "400" "weak password rejected" "$weak_create_body" assert_error_code "$weak_create_body" "VALID_001" "weak password rejected" if ! jq -e '.error.details.password | type == "array" and (map(tostring) | join(" ") | contains("常见"))' "$weak_create_body" >/dev/null; then echo "[ERROR] weak password validation category missing" >&2 exit 1 fi echo "[3/8] Create employee (initial password => mustChangePassword)" employee_no="A$(date +%s)$RANDOM" employee_email="auth_${employee_no,,}@hyinda.com" employee_phone="138${RANDOM}${RANDOM}" INITIAL_PASSWORD="InitA1$(date +%s%N)Xy" create_body="$TMP_DIR/create.json" create_status="$(curl -sS -o "$create_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/users" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d "{\"employeeNo\":\"$employee_no\",\"name\":\"Auth User\",\"phone\":\"$employee_phone\",\"email\":\"$employee_email\",\"title\":\"测试\",\"directManagerId\":null,\"hireDate\":\"2020-01-01\",\"roles\":[\"employee\"],\"password\":\"$INITIAL_PASSWORD\"}")" assert_status "$create_status" "200" "create employee" "$create_body" EMPLOYEE_ID="$(extract_json_field "$create_body" '.data.id')" login_body_1="$TMP_DIR/login1.json" login_status_1="$(login "$employee_email" "$INITIAL_PASSWORD" "${DEVICE_FINGERPRINT}-employee" "$login_body_1")" assert_status "$login_status_1" "200" "initial login" "$login_body_1" if [[ "$(jq -r '.data.mustChangePassword' "$login_body_1")" != "true" ]]; then echo "[ERROR] expected mustChangePassword=true on initial login" >&2 exit 1 fi ACCESS_TOKEN_1="$(extract_json_field "$login_body_1" '.data.accessToken')" echo "[4/8] Change password should revoke old token" NEW_PASSWORD="NewA1$(date +%s%N)Zz" change_body="$TMP_DIR/change.json" change_status="$(curl -sS -o "$change_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/auth/change-password" \ -H "authorization: Bearer $ACCESS_TOKEN_1" \ -H "content-type: application/json" \ -H "x-device-fingerprint: ${DEVICE_FINGERPRINT}-employee" \ -d "{\"oldPassword\":\"$INITIAL_PASSWORD\",\"newPassword\":\"$NEW_PASSWORD\"}")" assert_status "$change_status" "200" "change password" "$change_body" ACCESS_TOKEN_2="$(extract_json_field "$change_body" '.data.accessToken')" if [[ "$(jq -r '.data.mustChangePassword' "$change_body")" != "false" ]]; then echo "[ERROR] expected mustChangePassword=false after change" >&2 exit 1 fi old_me_body="$TMP_DIR/old-me.json" old_me_status="$(curl -sS -o "$old_me_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/me" \ -H "authorization: Bearer $ACCESS_TOKEN_1")" assert_status "$old_me_status" "401" "old token revoked" "$old_me_body" assert_error_code "$old_me_body" "AUTH_004" "old token revoked" new_me_body="$TMP_DIR/new-me.json" new_me_status="$(curl -sS -o "$new_me_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/me" \ -H "authorization: Bearer $ACCESS_TOKEN_2")" assert_status "$new_me_status" "200" "new token works" "$new_me_body" echo "[5/8] Admin reset password => mustChangePassword=true" reset_body="$TMP_DIR/reset.json" reset_status="$(curl -sS -o "$reset_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/users/$EMPLOYEE_ID/reset-password" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d '{}')" assert_status "$reset_status" "200" "admin reset password" "$reset_body" ONE_TIME_PASSWORD="$(extract_json_field "$reset_body" '.data.oneTimePassword')" login_body_2="$TMP_DIR/login2.json" login_status_2="$(login "$employee_email" "$ONE_TIME_PASSWORD" "${DEVICE_FINGERPRINT}-employee-reset" "$login_body_2")" assert_status "$login_status_2" "200" "login with reset password" "$login_body_2" if [[ "$(jq -r '.data.mustChangePassword' "$login_body_2")" != "true" ]]; then echo "[ERROR] expected mustChangePassword=true after admin reset" >&2 exit 1 fi echo "[6/8] Login failure lockout (AUTH_002)" wrong_password="WrongPassword1234" locked=0 for i in $(seq 1 "$LOGIN_FAIL_MAX_ATTEMPTS"); do attempt_body="$TMP_DIR/lock-attempt-$i.json" attempt_status="$(login "$employee_email" "$wrong_password" "${DEVICE_FINGERPRINT}-lock-$i" "$attempt_body")" if [[ "$i" -lt "$LOGIN_FAIL_MAX_ATTEMPTS" ]]; then assert_status "$attempt_status" "401" "lock attempt $i" "$attempt_body" assert_error_code "$attempt_body" "AUTH_001" "lock attempt $i" else assert_status "$attempt_status" "423" "lock attempt $i triggers lock" "$attempt_body" assert_error_code "$attempt_body" "AUTH_002" "lock attempt $i triggers lock" if [[ "$(jq -r '.error.details.lockedUntil // empty' "$attempt_body")" == "" ]]; then echo "[ERROR] expected lockedUntil in AUTH_002 details" >&2 exit 1 fi locked=1 fi done if [[ "$locked" != "1" ]]; then echo "[ERROR] expected account to be locked" >&2 exit 1 fi echo "[7/8] Locked account cannot login, unlock restores" locked_login_body="$TMP_DIR/locked-login.json" locked_login_status="$(login "$employee_email" "$ONE_TIME_PASSWORD" "${DEVICE_FINGERPRINT}-locked-correct" "$locked_login_body")" assert_status "$locked_login_status" "423" "locked account login blocked" "$locked_login_body" assert_error_code "$locked_login_body" "AUTH_002" "locked account login blocked" unlock_body="$TMP_DIR/unlock.json" unlock_status="$(curl -sS -o "$unlock_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/users/$EMPLOYEE_ID/unlock" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$unlock_status" "200" "unlock user" "$unlock_body" post_unlock_login_body="$TMP_DIR/post-unlock-login.json" post_unlock_login_status="$(login "$employee_email" "$ONE_TIME_PASSWORD" "${DEVICE_FINGERPRINT}-post-unlock" "$post_unlock_login_body")" assert_status "$post_unlock_login_status" "200" "login after unlock" "$post_unlock_login_body" if [[ "$RUN_AUTH_POSTGRES_CONCURRENCY" == "1" ]]; then echo "[8/9] PostgreSQL auth challenge concurrency" AUTH_SECURITY_EMPLOYEE_ID="$EMPLOYEE_ID" \ AUTH_SECURITY_EMPLOYEE_EMAIL="$employee_email" \ AUTH_SECURITY_CURRENT_PASSWORD="$ONE_TIME_PASSWORD" \ node src/scripts/check-auth-postgres-concurrency.js else echo "[8/9] PostgreSQL auth challenge concurrency [SKIP: explicit opt-in required]" fi echo "[9/9] Done" if [[ "$RUN_AUTH_POSTGRES_CONCURRENCY" == "1" ]]; then echo "[AUTH-REGRESSION][PASS] FULL" else echo "[AUTH-REGRESSION][PARTIAL] BASE_ONLY" fi