#!/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.'
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.
