Awesome Testing

Text file

Proof of concept pełnego przepływu 2FA

Lekcja 23: Automatyzacja two-factor authentication

Historical artifacts may name disposable training credentials and environments. Do not reuse credentials, target course systems, or execute archived prompts without authorization.

#!/usr/bin/env bash

set -euo pipefail

BASE_URL="${API_BASE_URL:-https://awesome.byst.re}"
RUN_ID="$(date -u +%Y%m%d%H%M%S)${RANDOM}"
USERNAME="mfapoc${RUN_ID}"
PASSWORD="MfaPoc-${RUN_ID}-A1!"
WORK_DIR="$(mktemp -d /private/tmp/l18-2fa-poc.XXXXXX)"
ACCESS_TOKEN=""
ACCOUNT_CREATED=false
ACCOUNT_DELETED=false

chmod 700 "${WORK_DIR}"

cleanup() {
  local cleanup_status

  if [[ "${ACCOUNT_CREATED}" == true && "${ACCOUNT_DELETED}" == false && -n "${ACCESS_TOKEN}" ]]; then
    cleanup_status="$(curl -sS --connect-timeout 10 --max-time 30 \
      -o "${WORK_DIR}/cleanup-body.json" \
      -X DELETE \
      -H 'Accept: application/json' \
      -H "Authorization: Bearer ${ACCESS_TOKEN}" \
      -w '%{http_code}' \
      "${BASE_URL}/api/v1/users/${USERNAME}/right-to-be-forgotten" || true)"
    if [[ "${cleanup_status}" == 204 || "${cleanup_status}" == 404 ]]; then
      printf 'cleanup: account removed (HTTP %s)\n' "${cleanup_status}"
    else
      printf 'cleanup: FAILED for generated account %s (HTTP %s)\n' "${USERNAME}" "${cleanup_status:-curl-error}" >&2
    fi
  fi

  rm -rf "${WORK_DIR}"
}

trap cleanup EXIT

request() {
  local label="$1"
  local method="$2"
  local path="$3"
  local body="${4:-}"
  local token="${5:-}"
  local -a curl_args

  curl_args=(
    -sS
    --connect-timeout 10
    --max-time 30
    -D "${WORK_DIR}/headers.txt"
    -o "${WORK_DIR}/body.json"
    -X "${method}"
    -H 'Accept: application/json'
    -w '%{http_code}'
  )

  if [[ -n "${body}" ]]; then
    curl_args+=(-H 'Content-Type: application/json' --data "${body}")
  fi
  if [[ -n "${token}" ]]; then
    curl_args+=(-H "Authorization: Bearer ${token}")
  fi

  HTTP_STATUS="$(curl "${curl_args[@]}" "${BASE_URL}${path}")"
  cp "${WORK_DIR}/body.json" "${WORK_DIR}/${label}.json"
  printf '%-38s HTTP %s\n' "${label}" "${HTTP_STATUS}"
}

expect_status() {
  local expected="$1"

  if [[ "${HTTP_STATUS}" != "${expected}" ]]; then
    printf 'Expected HTTP %s, received HTTP %s. Redacted body shape: ' "${expected}" "${HTTP_STATUS}" >&2
    jq -c 'if type == "object" then keys else type end' "${WORK_DIR}/body.json" >&2 2>/dev/null || printf '<non-JSON>\n' >&2
    return 1
  fi
}

expect_json() {
  jq -e "$1" "${WORK_DIR}/body.json" >/dev/null
}

totp_for_step_offset() {
  local secret="$1"
  local offset="$2"
  local epoch="$3"

  node --input-type=module -e '
    import { createHmac } from "node:crypto";

    const [secret, offsetText, epochText] = process.argv.slice(1);
    const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    let bits = "";
    for (const character of secret.replace(/=+$/, "").toUpperCase()) {
      const value = alphabet.indexOf(character);
      if (value < 0) throw new Error("Invalid Base32 secret");
      bits += value.toString(2).padStart(5, "0");
    }
    const bytes = [];
    for (let index = 0; index + 8 <= bits.length; index += 8) {
      bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
    }

    const counter = Math.floor(Number(epochText) / 30) + Number(offsetText);
    const message = Buffer.alloc(8);
    message.writeBigUInt64BE(BigInt(counter));
    const digest = createHmac("sha1", Buffer.from(bytes)).update(message).digest();
    const dynamicOffset = digest[digest.length - 1] & 0x0f;
    const binary = (digest.readUInt32BE(dynamicOffset) & 0x7fffffff) % 1_000_000;
    process.stdout.write(String(binary).padStart(6, "0"));
  ' "${secret}" "${offset}" "${epoch}"
}

printf 'Target: %s\n' "${BASE_URL}"
printf '%s\n' 'No secrets, credentials, tokens, or recovery codes are printed.'

# Public/protected negative checks before creating state.
request 'status_without_auth' GET '/api/v1/users/2fa/status'
expect_status 401
request 'setup_without_auth' POST '/api/v1/users/2fa/setup'
expect_status 401
request 'confirm_without_auth' POST '/api/v1/users/2fa/confirm' '{"code":"123456"}'
expect_status 401
request 'recovery_codes_without_auth' POST '/api/v1/users/2fa/recovery-codes' '{"password":"not-a-real-password","code":"123456"}'
expect_status 401
request 'disable_without_auth' POST '/api/v1/users/2fa/disable' '{"password":"not-a-real-password","code":"123456"}'
expect_status 401
request 'complete_signin_invalid_body' POST '/api/v1/users/signin/2fa' '{}'
expect_status 400
request 'complete_signin_bad_challenge' POST '/api/v1/users/signin/2fa' '{"challengeToken":"not-a-real-challenge","code":"123456"}'
expect_status 401

# Disposable local user and initial authenticated state.
SIGNUP_BODY="$(jq -cn \
  --arg username "${USERNAME}" \
  --arg email "${USERNAME}@example.com" \
  --arg password "${PASSWORD}" \
  '{username:$username,email:$email,password:$password,firstName:"MfaPoC",lastName:"Tester"}')"
request 'signup' POST '/api/v1/users/signup' "${SIGNUP_BODY}"
expect_status 201
ACCOUNT_CREATED=true

SIGNIN_BODY="$(jq -cn --arg username "${USERNAME}" --arg password "${PASSWORD}" '{username:$username,password:$password}')"
request 'signin_before_enrollment' POST '/api/v1/users/signin' "${SIGNIN_BODY}"
expect_status 200
expect_json '.token | type == "string"'
ACCESS_TOKEN="$(jq -r '.token' "${WORK_DIR}/body.json")"

request 'status_before_enrollment' GET '/api/v1/users/2fa/status' '' "${ACCESS_TOKEN}"
expect_status 200
expect_json '.enabled == false and .unusedRecoveryCodes == 0'

request 'confirm_invalid_body' POST '/api/v1/users/2fa/confirm' '{}' "${ACCESS_TOKEN}"
expect_status 400
expect_json '.code == "must not be blank"'

request 'recovery_codes_invalid_body' POST '/api/v1/users/2fa/recovery-codes' '{}' "${ACCESS_TOKEN}"
expect_status 400
expect_json '.password == "must not be blank" and .code == "must not be blank"'

request 'disable_invalid_body' POST '/api/v1/users/2fa/disable' '{}' "${ACCESS_TOKEN}"
expect_status 400
expect_json '.password == "must not be blank" and .code == "must not be blank"'

request 'setup' POST '/api/v1/users/2fa/setup' '' "${ACCESS_TOKEN}"
expect_status 200
expect_json '(.secret | test("^[A-Z2-7]+=*$")) and (.otpAuthUri | startswith("otpauth://totp/")) and (.qrCodeDataUri | startswith("data:image/png;base64,")) and (.expiresAt | type == "string")'
TOTP_SECRET="$(jq -r '.secret' "${WORK_DIR}/body.json")"

# Keep all three accepted codes in distinct, increasing steps while avoiding a boundary crossing.
while :; do
  WINDOW_POSITION=$(( $(date +%s) % 30 ))
  if (( WINDOW_POSITION >= 5 && WINDOW_POSITION <= 15 )); then
    break
  fi
  sleep 1
done
REFERENCE_EPOCH="$(date +%s)"
CONFIRM_CODE="$(totp_for_step_offset "${TOTP_SECRET}" -1 "${REFERENCE_EPOCH}")"
SIGNIN_CODE="$(totp_for_step_offset "${TOTP_SECRET}" 0 "${REFERENCE_EPOCH}")"
ROTATE_CODE="$(totp_for_step_offset "${TOTP_SECRET}" 1 "${REFERENCE_EPOCH}")"
WRONG_CONFIRM_CODE="${CONFIRM_CODE:0:5}$(( (10#${CONFIRM_CODE:5:1} + 1) % 10 ))"

request 'confirm_wrong_totp' POST '/api/v1/users/2fa/confirm' "$(jq -cn --arg code "${WRONG_CONFIRM_CODE}" '{code:$code}')" "${ACCESS_TOKEN}"
expect_status 401

request 'confirm' POST '/api/v1/users/2fa/confirm' "$(jq -cn --arg code "${CONFIRM_CODE}" '{code:$code}')" "${ACCESS_TOKEN}"
expect_status 200
expect_json '.recoveryCodes | type == "array" and length == 8 and all(test("^[2-9A-HJ-NP-Z]{4}(-[2-9A-HJ-NP-Z]{4}){4}$"))'
OLD_RECOVERY_CODE="$(jq -r '.recoveryCodes[0]' "${WORK_DIR}/body.json")"

request 'status_after_enrollment' GET '/api/v1/users/2fa/status' '' "${ACCESS_TOKEN}"
expect_status 200
expect_json '.enabled == true and .unusedRecoveryCodes == 8'

request 'setup_when_enabled' POST '/api/v1/users/2fa/setup' '' "${ACCESS_TOKEN}"
expect_status 409

request 'password_signin_requires_mfa' POST '/api/v1/users/signin' "${SIGNIN_BODY}"
expect_status 200
expect_json '.mfaRequired == true and (.challengeToken | type == "string" and length > 20) and (.challengeExpiresAt | type == "string") and .token == null and .refreshToken == null'
CHALLENGE_TOKEN="$(jq -r '.challengeToken' "${WORK_DIR}/body.json")"

request 'complete_signin_with_totp' POST '/api/v1/users/signin/2fa' "$(jq -cn --arg challengeToken "${CHALLENGE_TOKEN}" --arg code "${SIGNIN_CODE}" '{challengeToken:$challengeToken,code:$code}')"
expect_status 200
expect_json '(.token | type == "string") and (.refreshToken | type == "string") and .mfaRequired == false'
ACCESS_TOKEN="$(jq -r '.token' "${WORK_DIR}/body.json")"

request 'replay_consumed_challenge' POST '/api/v1/users/signin/2fa' "$(jq -cn --arg challengeToken "${CHALLENGE_TOKEN}" --arg code "${SIGNIN_CODE}" '{challengeToken:$challengeToken,code:$code}')"
expect_status 401

request 'replace_recovery_codes' POST '/api/v1/users/2fa/recovery-codes' "$(jq -cn --arg password "${PASSWORD}" --arg code "${ROTATE_CODE}" '{password:$password,code:$code}')" "${ACCESS_TOKEN}"
expect_status 200
expect_json '.recoveryCodes | type == "array" and length == 8 and all(test("^[2-9A-HJ-NP-Z]{4}(-[2-9A-HJ-NP-Z]{4}){4}$"))'
NEW_RECOVERY_CODE="$(jq -r '.recoveryCodes[0]' "${WORK_DIR}/body.json")"

request 'status_after_rotation' GET '/api/v1/users/2fa/status' '' "${ACCESS_TOKEN}"
expect_status 200
expect_json '.enabled == true and .unusedRecoveryCodes == 8'

request 'disable_with_replaced_code' POST '/api/v1/users/2fa/disable' "$(jq -cn --arg password "${PASSWORD}" --arg code "${OLD_RECOVERY_CODE}" '{password:$password,code:$code}')" "${ACCESS_TOKEN}"
expect_status 401

request 'disable_with_current_code' POST '/api/v1/users/2fa/disable' "$(jq -cn --arg password "${PASSWORD}" --arg code "${NEW_RECOVERY_CODE}" '{password:$password,code:$code}')" "${ACCESS_TOKEN}"
expect_status 200

request 'status_after_disable' GET '/api/v1/users/2fa/status' '' "${ACCESS_TOKEN}"
expect_status 200
expect_json '.enabled == false and .unusedRecoveryCodes == 0'

request 'signin_after_disable' POST '/api/v1/users/signin' "${SIGNIN_BODY}"
expect_status 200
expect_json '(.token | type == "string") and (.refreshToken | type == "string") and .mfaRequired == false'
ACCESS_TOKEN="$(jq -r '.token' "${WORK_DIR}/body.json")"

request 'delete_disposable_account' DELETE "/api/v1/users/${USERNAME}/right-to-be-forgotten" '' "${ACCESS_TOKEN}"
expect_status 204
ACCOUNT_DELETED=true

printf '%s\n' 'PoC complete: TOTP enrollment, TOTP sign-in, recovery-code rotation, disable, and account cleanup all succeeded.'