6.6 KiB
6.6 KiB
Laravel/Inertia OTP-авторизация и claim промокодов (MLH / Hacktoberfest)
Реверс Laravel+Inertia+Vite сайтов (www.mlh.com) и Next.js API-фронтов (hacktoberfest.com → hacktoberfest-api.mlh.com). Проверено E2E: OTP sign-in, OAuth, offers, claim 2 из 4 кодов.
1. Распознавание стека и чтение props
- Страница Inertia: весь стейт в
<script id="..." data-page="app" type="application/json">{...}</script>. Парсить regextype="application/json">(.*?)</script>+html.unescape()ПЕРЕДjson.loads— атрибут HTML-экранирован. - Ключевые props:
component,version,isSignedIn,currentUser,errors,flash,apiBaseUrl,authBaseUrl. - Vite-манифест:
https://<host>/vite/.vite/manifest.json→ маппингpages/...tsx -> assets/...js. Искать по имени компонента из props (напр.Events/Partners/PromoCodes/Redeem.tsx→Redeem-*.js) — в чанке видны точные URL, data-testid кнопок и payload форм.
2. Inertia POST — правила
- Заголовки:
X-Inertia: true,X-Inertia-Version: <version из data-page JSON>(НЕ из data-version атрибута — его может не быть),X-CSRF-Token: <urldecode(XSRF-TOKEN cookie)>,Referer: <URL страницы>,Content-Type: application/json. - Ответ на POST = ЧИСТЫЙ JSON (component+props), не HTML — парсить
json.loads(resp), а не искать<script>. - HTTP 409 = version conflict: GET страницу заново, взять свежий version. ПЕРЕД ретраем проверить, не konsumировано ли действие (POST мог пройти до 409 на повторе) — читать signed/claimed state с главной.
- PITFALL «silent no-op»: корректный на вид POST может вернуть 200 с тем же компонентом,
promoCode: null,errors: [],flash: null— действие НЕ зарегистрировано, сервер молча игнорирует. Если после POST state не изменился — НЕ долбить HTTP, а идти в браузер (п.4).
3. MLH OTP sign-in (флоу октября 2026, компонент Auth/Session/New)
- GET
https://www.mlh.com/signin→ version + XSRF из data-page. - POST
/signin{email, return_to, cf-turnstile-response}→ в propsotpId— ЭТО ULID-СТРОКА, regex"otpId":\s*"?([A-Za-z0-9_\-]+); числовой(\d+)схватит только «01». - GET
/signin/<otpId>/otp— ОБЯЗАТЕЛЬНО перед POST, чтобы взять свежий version (иначе 409). - Код из письма (Gmail IMAP): subject «One-Time Login Code», в теле
Login Code is NNNNNN(regexLogin Code is\s*(\d{6})). Magic-link «Sign in (» — legacy-путь, теперь ведёт на OTP-страницу. - POST
https://www.mlh.com/signin/<otpId>/otp{otp_id, otp, email, return_to, cf-turnstile-response}→ signed. Проверять GET/→ propsisSignedIn:true; куки сохранить (cookie string в JSON-стейт).
Turnstile MLH: sitekey 0x4AAAAAAARi-8xgLgfKOXIb (публичный, в HTML). siteverify ОТБИВАЕТ plain-токены sidecar'а (invalid-input-response) — только real_page (camoufox) solve; action для signin = authentication, для телефона = phone_verification.
4. OAuth и browser-claim (работает там, где HTTP no-op)
- Raw approve формы OAuth (
hacktoberfest-api.mlh.com/oauth/mlh) через urllib = 422 (не угадать скрытые поля Inertia-формы). РАБОЧИЙ ПУТЬ: Camoufox headless →ctx.add_cookies([{name,value,domain:'.mlh.com',path:'/'}])из сохранённой сессии (в т.ч.mlh_token) → goto OAuth start →wait_for_load_state('networkidle')→ кликbutton[type=submit]на authorize-странице → landinghacktoberfest.com/my/. - Токен API: localStorage ключ
hacktoberfest.session→ JSON → refreshToken → POSThttps://hacktoberfest-api.mlh.com/auth/refresh→ accessToken;Authorization: Bearer <accessToken>на/api/me/offers,/api/me,/api/me/fests. - Claim промокода: только КЛИКОМ в браузере по
[data-testid="claim-promo-code-button"](href=redeemUrl, method post, data {token}). После клика код читать изpage.inner_text('body')— секция «Just claimed! ... Promo Code»; в props промокод может остаться null даже после успеха.
5. Offers/redeem структура (MLH events)
- Redeem-страница:
https://www.mlh.com<redeemUrl>/redeem?token=<token>; props: token, redeemUrl, validLink, userMismatch, redemptionLimitExceeded, unmetClaimRequirements, claimRequirements, promoCode, previouslyClaimedPromoCodes, sponsor{name,slug}.
unmetClaimRequirements=[] → можно клеймить. Непустой → сначала закрыть гейты:
verified_phone: POST /api/phone_numbers {phone_number, channel:'sms', 'cf-turnstile-response'} (turnstile action phone_verification) → POST /api/phone_numbers/<id>/confirm_verification {code} (6 цифр из SMS). Нужен реальный приём SMS.
github_oauth: привязка GitHub-аккаунта к MLH (SocialProfiles); .tech дополнительно требует github_account_age (акк GH ≥30 дней — см. mlh_account_age_30_days в Redeem-чанке).
- Claimed-коды видны на
https://www.mlh.com/account/promo_codes (component Account/PromoCodes, props sponsors, totalCount).
6. Сессии и переиспользование
- Стейт:
hf_mlh_sessions.json (cookies string на акк) — переиспользуется и в urllib (http.cookiejar, domain .mlh.com) и в Camoufox (add_cookies). Сессия живёт днями; при signed:False — полный OTP-прогон заново (hf_reauth2.py паттерн).
- Долгие прогоны (captcha real_page 30-90с + IMAP wait) — только background с логом; foreground упрётся в timeout инструмента.
- Артефакты сессии-первоисточника:
C:/Users/User/tmp/hf_reauth2.py (OTP), hf_oauth_browser.py (Camoufox OAuth), hf_redeem_props.json, hf_codes_final.json, hf_mlh_sessions.json.