Awcms Auth Online Hardening
Konteks desain lintas-fitur untuk pengerasan auth online awcms (Turnstile, MFA/TOTP, OIDC/SSO, admin policy UI). PENTING — nomor issue #587-#593, nama file, nama tabel, nomor migrasi, dan path endpoint di badan skill ini milik repo LAIN (awcms-micro); di awcms semuanya bernama BEDA. Baca §Peta ke artefak nyata awcms lebih dulu, lalu perlakukan sisanya sebagai catatan alasan-desain, bukan rujukan path. Kapabilitasnya SENDIRI sudah ada di awcms per #184/#185/#186/#274 — jangan bangun ulang.
Score breakdown
Estimated from the available content and source signals.
Model compatibility
Inferred fit is not the same as a recorded hands-on test.
Overview
AWCMS — Full-Online Auth Security Hardening
BACA INI DULU — dokumen ini punya dua lapis, dan hanya satu yang bisa dipercaya sebagai path. Badan skill di bawah disalin dari epic
awcms-micro"full-online auth security hardening". Setiap nomor issue (#587–#593, #598, #605), nama file (auth-security-status.ts), nomor migrasi (036), dan path endpoint (/api/v1/identity/sso/*,/api/v1/auth/providers/google/*) di bawah adalah milik repo ITU, bukan repo ini. Audit 2026-07-18 benar saat menyimpulkan tak satu pun ditemukan disrc/awcms — tapi kesimpulan yang ditarik saat itu ("epic ini fiktif, bangun dari nol") kini keliru arah sebaliknya: kapabilitasnya sudah dibangun di awcms sejak itu, dengan nama sendiri.Yang harus diambil dari dokumen ini adalah ALASAN DESAINNYA (gate gabungan, fail-closed, anti-enumerasi, blast radius circuit breaker, batas MFA-vs-reset) — itu tetap berlaku dan sudah terbukti mahal untuk di-derive ulang. Yang TIDAK boleh diambil adalah path/nama/nomornya. Lihat §Peta ke artefak nyata awcms tepat di bawah, lalu verifikasi ke kode.
Enam fitur hardening auth online-only (Cloudflare Turnstile, MFA/TOTP, Google OIDC login, generic tenant OIDC SSO, admin policy UI, plus penutup docs/kontrak) di atas login lokal/password + session opaque, tanpa mengubah perilaku default offline/LAN/local. Model gate-nya di awcms:
AUTH_ONLINE_SECURITY_ENABLED + AUTH_ONLINE_SECURITY_PROFILE=full_online
-> isFullOnlineSecurityActive(env) === true
-> Turnstile boleh aktif
Divergensi penting dari model micro di atas. Di awcms hanya Turnstile
yang tetap digerbangi profil deployment. MFA dan OIDC/SSO melepas gerbang
itu saat di-port (#184/#185): keduanya digerakkan state DB per tenant
(enrollment MFA, baris awcms_tenant_auth_policies), bukan env global —
AUTH_MFA_ENABLED hanya menggerbangi enrollment baru, sementara challenge
dan step-up jalan dari state. Membaca dokumen ini seolah "semua fitur
digerbangi isFullOnlineSecurityActive" akan salah.
Peta ke artefak nyata awcms
Kolom kiri = nama yang dipakai di badan skill ini (milik awcms-micro). Kolom kanan = yang benar-benar ada di repo ini. Selalu pakai kolom kanan.
| Disebut di bawah (micro) | Nyata di awcms |
|---|---|
auth-security-status.ts | tidak ada padanan; postur dirakit langsung di src/pages/admin/security.astro |
migration 036 | sql/024 (MFA), sql/025+sql/026 (OIDC/SSO + seed permission) |
/api/v1/identity/sso/providers | /api/v1/auth/sso-providers (+ /[id]) |
/api/v1/identity/sso/policy | /api/v1/auth/sso-policy (PATCH) |
/api/v1/auth/providers/google/* | tidak ada — awcms hanya punya OIDC generik /api/v1/auth/sso/[providerKey]/* |
| "admin policy UI #592" | src/pages/admin/security.astro (#274) — lihat identity-access/README.md |
| Issue #587–#593, PR #598, Issue #605 | nomor micro; padanan awcms: #184 (MFA), #185 (OIDC/SSO), #186 (Turnstile), #274 (UI) |
Yang memang ada dengan nama sama: src/lib/auth/online-security-config.ts,
src/lib/security/turnstile.ts, AUTH_ONLINE_SECURITY_ENABLED/_PROFILE,
isFullOnlineSecurityActive, checkOnlineAuthSecurityReady,
awcms_auth_providers, awcms_tenant_auth_policies.
Fitur auth yang ada di awcms tapi tidak dibahas dokumen ini sama sekali
(jangan simpulkan "belum ada" dari kebisuannya): password reset lewat email
(sql/073), self-registration ber-persetujuan admin (sql/074–075),
business-scope, SoD, dan ABAC DSL. Rujuk
src/modules/identity-access/README.md.
Kapan pakai skill ini vs skill generik
Skill ini melengkapi (bukan menggantikan) awcms-new-endpoint,
awcms-new-migration, awcms-idempotency,
awcms-abac-guard, awcms-audit-log, dan
awcms-sensitive-data (kredensial provider, TOTP seed, recovery
code semua data sensitif). Skill ini menyediakan konteks cross-cutting
epic ini spesifik: gate bersama yang wajib dicek setiap fitur, dan
keputusan desain yang mengikat semua issue di epic ini sekaligus.
Status di awcms (jangan bangun ulang yang sudah ada)
Nomor issue di kolom kiri adalah penomoran micro yang dipakai badan skill ini; PR di kolom kanan adalah pekerjaan awcms yang sesungguhnya.
| Scope (penomoran micro) | Status di awcms |
|---|---|
Gate bersama AUTH_ONLINE_SECURITY_ENABLED/_PROFILE (#587) | ✅ ada — src/lib/auth/online-security-config.ts, checkOnlineAuthSecurityReady |
| Cloudflare Turnstile untuk form auth publik (#588) | ✅ ada (#186) — src/lib/security/turnstile.ts; satu-satunya fitur yang masih digerbangi profil |
| MFA/TOTP login challenge (#589) | ✅ ada (#184, sql/024) — enforcement dari state DB, BUKAN dari gate profil |
| Google OIDC login (#590) | ❌ tidak ada, dan sengaja — awcms punya OIDC generik; port Google-spesifik hanya bila diminta (roadmap) |
| Generic tenant OIDC SSO provider (#591) | ✅ ada (#185, sql/025/026) — path & migrasi beda, lihat §Peta |
| Admin UI kebijakan auth security online (#592) | ✅ ada (#274) — src/pages/admin/security.astro; CRUD provider masih API-only |
| Docs/kontrak/readiness penutup epic (#593) | 🟡 sebagian — readiness check & threat-model awcms punya jalur sendiri; jangan pakai daftar audit micro |
Yang sudah ada — pakai ulang, jangan re-derive
Gate bersama (Issue #587, src/lib/auth/online-security-config.ts)
Dua env var, keduanya opsional/backward-compatible — tidak di-set
sama sekali (default setiap deployment offline/LAN/local), config:validate
tetap PASS dan tidak ada perubahan perilaku login sama sekali:
AUTH_ONLINE_SECURITY_ENABLED—"true"mengaktifkan gate, nilai lain (termasuk unset) berarti nonaktif.AUTH_ONLINE_SECURITY_PROFILE—"disabled"(default) atau"full_online". Wajib"full_online"kalauAUTH_ONLINE_SECURITY_ENABLED=true— kombinasi lain gagalbun run config:validate(checkOnlineAuthSecurityConfig,scripts/validate-env.ts).
Tiga fungsi diekspor:
isOnlineSecurityEnabled(env)— cek flag saja.resolveOnlineSecurityProfile(env)— selalu jatuh ke"disabled"untuk nilai kosong/tidak dikenal, tidak pernah throw.isFullOnlineSecurityActive(env)— satu-satunya fungsi yang WAJIB dipanggil setiap fitur #588-#592 sebelum melakukan apa pun yang online/provider-terkait. Jangan re-derive aturan "keduanya harus setuju" di modul lain — impor fungsi ini langsung.
scripts/security-readiness.ts's checkOnlineAuthSecurityReady
melaporkan status gate ini (severity critical supaya misconfiguration
sungguhan tetap blokir go-live, tapi status: pass untuk kondisi
disabled — informational, bukan kegagalan, sesuai acceptance criteria
#587). Detail env var lengkap: docs/awcms/18_configuration_env_reference.md
§Full-online auth security hardening,
docs/awcms/deployment-profiles.md §Full-online auth security
hardening, src/modules/identity-access/README.md §Full-online-only
auth security feature gate.
Cloudflare Turnstile (Issue #588, src/lib/security/turnstile.ts)
Fitur konkret pertama yang dibangun di atas gate #587 — pola referensi untuk #589-#592 berikutnya. Gate gabungan:
isTurnstileRequired(env)
= isFullOnlineSecurityActive(env) AND isTurnstileEnabled(env)
(isTurnstileEnabled = TURNSTILE_ENABLED === "true")
- Satu fungsi enforcement dipanggil dari 4 endpoint:
enforceTurnstileIfRequired(turnstileToken, remoteIp, env)— dipanggil diPOST /api/v1/auth/login,/auth/password/forgot,/auth/password/reset, dan/setup/initialize, tepat setelah body divalidasi tapi sebelum DB/password hashing (issue's security note: verifikasi Turnstile lebih murah, jangan buang kerja mahal untuk request yang bahkan tidak lolos bot-check). Mengembalikan{ok:true}atau{ok:false, code: "TURNSTILE_REQUIRED" | "TURNSTILE_INVALID"}— fail closed: misconfiguration (resolveTurnstileConfig→null) diperlakukan sama seperti token invalid, bukan dilewati. - Verifikasi env var independen dari gate #587:
TURNSTILE_ENABLED=truesendiri sudah mewajibkanTURNSTILE_SITE_KEY+TURNSTILE_SECRET_KEYdiconfig:validate/security-readiness(checkTurnstileConfig,scripts/validate-env.ts) — operator boleh isi kredensial ini lebih dulu tanpa menyalakanAUTH_ONLINE_SECURITY_ENABLED; aktivasi runtime tetap butuh KEDUA gate setuju. verifyTurnstileTokenmemanggil Cloudflare siteverify server-side (issue's security note: "client widget alone is not security"), timeout-bounded (withTimeout) + circuit breaker (getProviderCircuitBreaker("turnstile")), pola sama seperticloudflare-dns-adapter.ts/mailketing-provider.ts— dengan satu perbedaan penting yang wajib dipertahankan:breaker.recordFailure()HANYA dipanggil untuk kegagalan transport genuine ke Cloudflare (HTTP non-2xx, body tak terparse, network error/timeout), TIDAK PERNAH untuk respons 2xx yang sah dengansuccess:false(itu Cloudflare menjawab dengan benar bahwa token client-nya salah — hasil normal yang bisa dipicu siapa pun tanpa autentikasi). PR #596 security review menemukan versi awal menyamakan keduanya: breaker ini shared/cross-tenant, danenforceTurnstileIfRequiredfail-closed saat breaker terbuka, jadi penyerang bisa mengunci login/password-reset/setup SEMUA tenant hanya dengan mengirim segelintir token sampah setiap ~30 detik. Jangan regresi pola ini di fitur online lain (#589-#592) yang menambah circuit breaker provider baru — bedakan selalu "provider tidak sehat" dari "input client ditolak provider dengan benar". Logturnstile.circuit_breaker_open/turnstile.provider_call_failed/turnstile.provider_call_errored(severitywarning,src/lib/logging/logger.ts) memberi visibilitas operasional untuk keduanya.- CSP (
astro.config.mjs):script-src/frame-srcmengizinkanhttps://challenges.cloudflare.comtanpa syarat (tidak digerbangiTURNSTILE_ENABLEDdi build time) — alasannya didokumentasikan langsung di file itu: CSP Astro cuma bisa di-bake saat build, sedangkanTURNSTILE_ENABLEDdidesain runtime-toggleable seperti flag lain; widget sendiri tetap runtime-gated lewatisTurnstileRequired()dilogin.astro. - Widget UI hanya di-render di
login.astro(form publik lain — forgot/reset/setup — belum punya halaman UI di repo ini, baru endpoint API-nya) saatisTurnstileRequired()true; token dikirim sebagai field opsionalturnstileTokendi body JSON, dibaca dari hidden fieldcf-turnstile-responseyang otomatis diisi widget. - Error code i18n:
error.turnstile_required/error.turnstile_invalid(src/lib/i18n/error-messages.ts,i18n/en.po+id.po).
MFA/TOTP (Issue #589, src/modules/identity-access/application/mfa.ts)
Gate gabungan sama persis polanya dengan Turnstile:
isMfaRequired(env)
= isFullOnlineSecurityActive(env) AND isMfaEnabled(env)
(isMfaEnabled = AUTH_MFA_ENABLED === "true")
- MFA opt-in per identity, bukan mandatory tenant-wide — bahkan
dengan gate aktif, identity yang belum pernah enroll tetap login
normal (
login.tsmengecekfindActiveMfaFactorper identity SETELAH password valid, bukan hanya gate env). Jangan asumsikan mengaktifkanAUTH_MFA_ENABLED=trueotomatis mewajibkan MFA untuk semua user. - Login yang dijeda, bukan ditolak: password valid + factor
active→login.tsTIDAK membuat session, malah insert rowawcms_mfa_challengesdan balas401 MFA_REQUIREDberisierror.details.mfaChallengeToken(bentukdetailsdi sini SENGAJA bukanErrorDetail[]seperti endpoint lain — lihat OpenAPI schemaLoginMfaRequiredResponse— karena payload asli (token) harus dikembalikan, bukan sekadar array pesan validasi).POST /auth/mfa/totp/verifyadalah satu-satunya endpoint MFA yang TIDAK butuh session — diautentikasi lewat possession token challenge, pola sama sepertipassword/resetdiautentikasi lewat possession token reset. Kode/recovery code valid → session dibuat identik denganlogin.ts(token, cookie, response shape sama), supaya client tidak perlu logic berbeda untuk step kedua. - Enkripsi-at-rest, bukan hash, untuk TOTP secret —
src/lib/auth/mfa-secret-crypto.ts(AES-256-GCM,AUTH_MFA_SECRET_ENCRYPTION_KEY, base64 32-byte, divalidasicheckMfaConfig) — satu-satunya secret di aplikasi ini yang reversibel, karena verifikasi TOTP butuh menghitung ulang kode dari secret asli setiap request, tidak seperti password/token yang cukup dibandingkan hash-nya. Recovery code (mfa-recovery-code.ts) dan challenge token (mfa-challenge-token.ts) tetap hash-only (sha256, pola samasession-token.ts/password-reset-token.ts) — TIDAK reversibel, karena keduanya tidak pernah perlu ditampilkan ulang setelah reveal sekali di awal. - Replay prevention, dan WAJIB atomik, bukan read-then-write —
awcms_identity_mfa_factors.last_used_stepmenyimpan step time-counter TOTP tertinggi yang pernah diterima; verifikasi hanya diterima kalau step yang cocok STRICTLY LEBIH BESAR dari nilai ini (src/lib/auth/totp.ts'sverifyTotpCode, default ±1 step window). PR #597 security review menemukanverifyMfaChallengeawalnya melakukan SELECT lalu UPDATE terpisah (untuklast_used_step,awcms_identity_mfa_recovery_codes.used_at, DANawcms_mfa_challenges.failed_attempts) — di bawah READ COMMITTED (default Postgres,withTenanttidak mengubah isolation level), request verifikasi konkuren semuanya membaca state lama sebelum salah satu commit, sehingga replay guard maupun batasfailed_attemptsbisa dilewati sepenuhnya oleh penyerang yang mengirim tebakan paralel. Diperbaiki dengan: (a)SELECT ... FOR UPDATEpada baris challenge di awalverifyMfaChallenge(mengunci baris itu untuk sisa transaksi, men-serialize semua request verifikasi terhadap challenge yang sama), (b) compare-and-swap untuklast_used_step(UPDATE ... WHERE last_used_step < $step RETURNING id, 0 baris = gagal — melindungi replay lintas-challenge yang FOR UPDATE saja tidak jangkau, mis. dua login attempt berbeda membuat dua challenge terpisah untuk identity yang sama), (c) compare-and-swap yang sama untuk recovery code (UPDATE ... WHERE used_at IS NULL RETURNING id). Fitur online lain yang menambah state single-use/counter yang bisa diverifikasi berkali-kali (kode OTP lain, dsb.) WAJIB pola atomik yang sama — jangan pernah SELECT untuk mengevaluasi kondisi lalu UPDATE terpisah untuk menandainya terpakai/gagal; regression test-nya:mfa-flow.integration.test.ts§"concurrent verification attempts..." dan §"concurrent wrong-code attempts...". - Reset password BUKAN bypass MFA —
completePasswordResettidak menyentuh tabelawcms_identity_mfa_factorssama sekali; diverifikasi test integrasi eksplisit (mfa-flow.integration.test.ts§"password reset does not disable MFA"). - Re-enroll ditolak selagi factor aktif (
409 MFA_ALREADY_ACTIVE,POST /auth/mfa/totp/enroll/start) — sesi yang di-hijack tidak bisa diam-diam mengganti secret TOTP tanpa lebih duludisable. - Disable & regenerate recovery code = high-risk, diaudit
(
mfa_disabled/mfa_recovery_codes_regenerated, severitywarning) — pola samaawcms-audit-log. Catatan desain yang belum ditutup (PR #597 review, tidak blocking): kedua endpoint ini hanya mensyaratkan sesi valid, tanpa re-autentikasi tambahan (password saat ini/kode TOTP saat ini) — sesi yang dibajak (bukan hanya dicuri sebelum MFA aktif) cukup untuk mematikan MFA korban atau membuang recovery code lama. Diterima sebagai trade-off untuk scope issue #589 saat ini; fitur online lanjutan (mis. #592 admin policy UI) yang menyentuh area ini sebaiknya mempertimbangkan step-up re-auth di titik ini. - Error code i18n:
error.mfa_required/_disabled/_already_active/_not_active/_enrollment_not_found/_invalid_code/_challenge_invalid/_misconfigured(error-messages.ts,i18n/en.po+id.po).
Google OIDC login (Issue #590, src/modules/identity-access/application/google-oidc.ts)
Gate gabungan sama persis polanya dengan Turnstile/MFA:
isGoogleLoginRequired(env)
= isFullOnlineSecurityActive(env) AND isGoogleLoginEnabled(env)
(isGoogleLoginEnabled = AUTH_GOOGLE_LOGIN_ENABLED === "true")
- Tenant id lewat
state, bukan header —GET .../callbackadalah redirect target Google (navigasi browser murni), yang TIDAK BISA membawa headerX-AWCMS-Tenant-IDseperti endpoint lain.stateyang dikirim ke Google berbentuk${tenantId}.${rawToken}(src/lib/auth/oauth-state-token.ts'sbuildOAuthStateParam/parseOAuthStateParam) — tenant id BUKAN secret, jadi aman muncul di URL; bagian token (pertahanan CSRF/replay sesungguhnya, ≥32 byte random) tetap di-hash at rest sepertistate/session/reset/challenge token lain di aplikasi ini. Fitur online lain yang butuh redirect ke provider eksternal (mis. #591 generic SSO) WAJIB pola yang sama untuk membawa tenant id — jangan asumsikan header selalu tersedia di endpoint redirect-target. - Dua flow berbeda dari satu orkestrator:
GET .../start(unauthenticated, dari tombol "Continue with Google" di/login) selalupurpose='login'.POST .../link(BUTUH session — identity diambil server-side dari session yang sedang login, TIDAK PERNAH dipercaya dari request callback) mengembalikanauthorizationUrlsebagai JSON (bukan redirect 302), karena dipanggil lewatfetch()dari konteks yang sudah authenticated — client men-window.locationsendiri.GET .../callback(satu-satunya redirect target Google) menangani KEDUA purpose lewat satu orkestratorcompleteGoogleOAuthCallback(application layer) berdasarkan kolompurpose/identity_iddi barisawcms_oidc_auth_requestsyang tersimpan saat start/link — BUKAN dua implementasi terpisah yang bisa divergen soal keamanan. - Verifikasi ID token kriptografis PENUH, bukan sekadar decode JSON
(issue's security note: "Do not trust query parameters alone; validate
ID token cryptographically") — signature RS256 lewat WebCrypto
crypto.subtle(src/lib/auth/jwt-verify.ts, TIDAK ada library JWT eksternal), lalu issuer/audience/expiry/nonce (google-oidc-policy.ts'svalidateIdTokenClaims, pure/testable). Setiap kegagalan collapse keGOOGLE_ID_TOKEN_INVALIDgenerik (anti-enumeration, pola samaMFA_CHALLENGE_INVALID) — JANGAN bocorkan alasan spesifik (issuer salah vs audience salah vs signature invalid) ke response. - Provider account ditautkan via
sub, TIDAK PERNAH via email (issue's security note: "Usesubas the stable provider key") —awcms_identity_provider_accounts, unique per (tenant, provider, subject) DAN per (tenant, identity, provider). Auto-link by email HANYA aktif bilaemail_verified=trueDAN domain email ada diAUTH_GOOGLE_ALLOWED_DOMAINS(isEmailDomainAllowed— fail-closed: list kosong/tidak di-set = auto-link SELALU ditolak, bukan "izinkan semua domain"). Kalau tidak ada provider account yang cocok dan auto-link tidak berlaku →401 GOOGLE_ACCOUNT_NOT_LINKED, TIDAK PERNAH provisioning identity baru (self-service registration via Google eksplisit out-of-scope issue ini). - Google login TIDAK PERNAH bypass MFA (issue's acceptance
criterion: "If #589 is implemented and MFA is required, Google login
still proceeds through MFA challenge before session creation") —
completeGoogleOAuthCallbackmemanggilfindActiveMfaFactor/createMfaChallengeyang SAMA persis denganlogin.ts(bukan jalur MFA terpisah yang bisa lupa di-wire). Endpointcallback.tsmengembalikan401 MFA_REQUIREDdenganmfaChallengeTokenyang sama bentuknya seperti darilogin.ts— client menyelesaikan lewatPOST /auth/mfa/totp/verifyyang sudah ada, tidak perlu endpoint MFA baru untuk provider OIDC lain. - Circuit breaker HANYA trip pada kegagalan transport genuine —
pelajaran langsung dari bug Turnstile (PR #596 security review, lihat
§Cloudflare Turnstile di atas): token exchange yang menjawab
400 invalid_grantuntukcodeyang salah/bekas/kedaluwarsa adalah Google BENAR menolak input attacker-controlled, bukan tanda Google unhealthy —google-oauth-client.ts'sexchangeAuthorizationCodeHANYArecordFailurepada 5xx/network error/timeout, tidak pernah pada respons 4xx yang valid. JWKS di-cache 1 jam (fetchGoogleJwks) — jangan fetch JWKS setiap request. - JANGAN pernah INSERT/UPDATE dengan
tenantIdyang belum divalidasi SEBELUMSELECTyang aman — security review PR #598 menemukanGET .../startawalnya langsungINSERT INTO awcms_oidc_auth_requestsdengantenantIddari query param tak terautentikasi TANPA mengecek tenant itu benar-benar ada lebih dulu.tenant_idpunya FK keawcms_tenants— tenant palsu memicu foreign-key violation, dan exception itu ditangkapwithTenant's catch-all lalu di-record kegetDatabaseCircuitBreaker(), breaker tunggal APLIKASI-LEBAR (beda dari breaker per-provider seperti punya Turnstile/Google sendiri — breaker ini dipakai SEMUA endpoint, SEMUA tenant). Lima request dengantenantIdacak dari penyerang tak terautentikasi bisa membuka breaker ini dan menjatuhkan SELURUH aplikasi 30 detik, diulang tanpa henti — blast radius lebih besar dari bug Turnstile PR #596. Diperbaiki denganSELECT status FROM awcms_tenants WHERE id = tenantId(aman, tidak pernah throw untuk baris kosong) SEBELUM memanggilcreateOAuthRequest, plus rate limiting (checkRateLimit, pola samalogin.ts) sebagai lapis kedua. Fitur online lain (mis. #591 generic SSO) yang punya endpoint tak terautentikasi dengan INSERT/UPDATE ber-FK ke tabel tenant-scoped WAJIB pola yang sama — cek keberadaan/status via SELECT dulu, jangan pernah biarkan sebuah write yang bisa gagal FK constraint jadi baris pertama yang menyentuh DB untuk input tak terautentikasi. Regression test:google-oidc-flow.integration.test.ts§"start rejects a nonexistent tenant WITHOUT tripping the shared database circuit breaker". POST .../link/.../unlink: high-risk, diaudit (google_account_linked/google_account_unlinked);callback.tslogin sukses diauditgoogle_login_succeeded.- Error code i18n:
error.google_login_disabled/_oauth_state_invalid/_token_exchange_failed/_id_token_invalid/_account_not_linked/_already_linked/_not_linked/_misconfigured(error-messages.ts,i18n/en.po+id.po).
Generic tenant OIDC SSO provider (Issue #591, src/modules/identity-access/application/tenant-sso.ts)
Generalizes #590's Google-specific login into a tenant-CONFIGURED
provider model, WITHOUT touching Google's own code/tables — a deliberate
PARALLEL implementation, not a refactor of google-oidc.ts:
- Reuses
awcms_oidc_auth_requests/awcms_identity_provider_accounts(migration 035) as-is — both were already generic (provider text, no CHECK constraining it to'google') specifically so this issue wouldn't need a schema change to them. Generic SSO storesprovider = <providerKey>in the exact same rows Google's own flow storesprovider = 'google'in. New tables (migration 036) are onlyawcms_auth_providers(tenant-configured provider config:provider_key,issuer_url,client_id, client secret — encrypted at rest (AUTH_SSO_CREDENTIAL_ENCRYPTION_KEY, AES-256-GCM, SEPARATE key from MFA's ownAUTH_MFA_SECRET_ENCRYPTION_KEY) OR an env-var-name reference, exactly one via CHECK constraint, NEVER returned plaintext by any endpoint;scopes,allowed_email_domainsjsonb,enabled, soft delete) andawcms_tenant_auth_policies(one row per tenant:password_login_enabled,sso_enabled,sso_required,auto_link_verified_email,allowed_email_domainsjsonb,break_glass_identity_idsjsonb,mfa_requiredreserved for future #589 compatibility, not yet enforced). Both RLSENABLE+FORCE. - Gate gabungan
isSsoRequired(env)(src/lib/auth/sso-config.ts) =isFullOnlineSecurityActive(env)(#587) ∧AUTH_SSO_ENABLED=true— same shape as every other feature's gate in this epic. - OIDC discovery is unavoidable here (unlike Google's hardcoded
endpoint constants) —
discoverOidcConfiguration/fetchProviderJwks(src/lib/auth/generic-oidc-client.ts) fetch.well-known/openid-configuration+ JWKS from each provider's ownissuer_url, cached 1h, bounded byAUTH_SSO_DISCOVERY_TIMEOUT_MS(issue's own acceptance criterion: "OIDC discovery and JWKS fetches have bounded timeout"). Circuit breakers are keyed PER PROVIDER (sso-oidc-discovery:<providerKey>/sso-oidc-jwks:<providerKey>/sso-oidc-token:<providerKey>) — a slow/unhealthy provider on one tenant must never affect another tenant's or provider's login. Same PR #596/#598 rule applied from day one: only a genuine transport failure (5xx/network/timeout) trips the breaker, never a well-formed 4xx (bad/reused/expiredcode) — the provider correctly rejecting attacker-controlled input is a healthy-provider signal. - Endpoints mirror Google's own shape exactly:
GET /auth/sso/{providerKey}/start(unauthenticated, tenant resolved from header/cookie/?tenantId=, tenant existence/statusSELECTed BEFORE any INSERT into the reusedawcms_oidc_auth_requests— applying PR #598's fix from the very first version of this endpoint, not as an afterthought),GET .../callback(re-checksprovider.enabledagain at callback time — an admin may have disabled the provider betweenstartand the user completing the flow at the external provider),POST .../link/.../unlink(identical session/audit shape to Google's). - Admin CRUD is IN SCOPE for #591 (unlike #590 which had none) —
identity_access.sso_providers.{read,create,update,delete}andidentity_access.sso_policy.{read,update}(migration 037 permission seed) protect/api/v1/identity/sso/providers(/{id}) and/api/v1/identity/sso/policy. Deliberately NOT gated byisSsoRequired()— an admin may configure a provider ahead of flipping the deployment-level gate on, same allowancecheckGoogleOidcConfig/checkTurnstileConfigalready grant for their own credentials; no network/provider call happens in the CRUD path itself. Admin UI pages for this API are Issue #592, not this issue — "minimal UI... unless needed for verification" in the issue's own scope note was read as "API only", since the API itself IS the verification surface (curl/fetch), full UI is explicitly tracked separately. - Break-glass enforcement is at POLICY-SAVE time, not just login
time (issue's own acceptance criterion) —
saveTenantAuthPolicy(tenant-auth-policy.ts) re-readsbreak_glass_identity_idsfrom the request against a FRESH DB query (countEligibleBreakGlassIdentities) confirming each id is a currentlyactiveidentity with anactiveawcms_tenant_usersmembership — a request that would leavesso_required=trueorpassword_login_enabled=falsewith zero eligible break-glass identities is rejected409 BREAK_GLASS_REQUIREDand never persisted.login.tsitself only enforcespassword_login_enabled=falsewhenisSsoRequired(env)is ALSO active (isPasswordLoginDisabledForIdentity) — every local/offline/LAN deployment that never flips the #591 gate on runs zero extra queries and has zero behavior change, exactly like every other feature in this epic. - Auto-link by email, two independent fail-closed layers (domain:
tenant-sso-policy.ts'sisAutoLinkAllowedForProvider): the PROVIDER's ownallowed_email_domains(mirrorsAUTH_GOOGLE_ALLOWED_DOMAINS, per-tenant-per-provider instead of a deployment env var) AND the tenant POLICY'sauto_link_verified_emailmaster switch, which must be explicitlytrue— unlike Google (whose auto-link only needed the domain allow-list to be non-empty), generic SSO requires the tenant to opt in twice: enable the provider's own domain list AND flip the policy's master switch. google-oidc-policy.ts'sevaluateOAuthRequest/validateIdTokenClaimsare reused VERBATIM here (imported directly, not copied) — both were already pure and provider-agnostic.oauth-state-token.ts'sbuildOAuthStateParam/parseOAuthStateParam/generateOAuthState/hashOAuthState/generateOidcNonceare reused the same way. What is NOT reused:google-oidc.ts's owncreateOAuthRequest/consumeOAuthRequest/findIdentityByProviderSubject/linkProviderAccount/unlinkProviderAccountall hardcodeprovider = 'google'in their SQL —tenant-sso.tshas its own small parameterized duplicates of these instead of refactoring Google's (keeps the already-tested Google flow untouched).- Error code i18n:
error.sso_disabled/_provider_not_found/_provider_disabled/_provider_unavailable/_oauth_state_invalid/_token_exchange_failed/_id_token_invalid/_account_not_linked/_already_linked/_not_linked/_misconfigured/_provider_key_conflict, plusbreak_glass_required/password_login_disabled(error-messages.ts,i18n/en.po+id.po).
Admin policy UI (Issue #592)
src/pages/admin/security.astro + src/lib/auth/auth-security-status.ts
(pure env-only status aggregator, no DB/network I/O). Consumes #591's
existing admin CRUD API as-is — no new API endpoint was added for this
issue, and none was needed:
- SSR reads
getTenantAuthPolicy/listAuthProviders(#591's own application-layer functions) directly inside the page's ownwithTenanttransaction, same "call the application layer directly instead of round-tripping through this app's own HTTP API" conventionadmin/settings.astro/admin/blog/settings.astroalready use. Mutations (policy save, provider create/update/delete) go through the REALPATCH /api/v1/identity/sso/policy/POST|PATCH|DELETE /api/v1/identity/sso/providers[/{id}]endpoints viasendJson/postJson(src/lib/ui/admin-form-client.ts) — every mutation still runs through those endpoints' own ABAC + break-glass + audit logic; this page never writes to the database directly. - Two independent gates control what renders (issue's own acceptance
criteria): (1) the deployment gate
isFullOnlineSecurityActive(env)(#587) — inactive on every local/offline/LAN deployment (the default), the page renders ONLY an informational hand-rolled<p class="state-notice" role="status">notice (same inline patternoffices.astro/roles.astrouse for their denied/error states) and nothing else, checked server-side in the page's own frontmatter BEFORE any of the status/policy/provider markup is generated — never just hidden with CSS; (2) ABAC (identity_access.sso_policy.*/sso_providers.*, migration 037, already seeded by #591) — gate active but neither permission held renders an access-denied<p class="state-notice">instead. Each section (policy form, provider table) additionally checks its OWN specific permission independently, same per-fieldset-permission conventionadmin/access-users.astroestablished. - Status summary never re-derives each feature's own gate —
resolveAuthSecurityStatusSummary(env)importsisTurnstileEnabled/isMfaEnabled/isGoogleLoginEnabled/isSsoEnabledplus each feature's own*_REQUIRED_WHEN_ENABLEDenv var name list (TURNSTILE_REQUIRED_WHEN_ENABLED,AUTH_MFA_REQUIRED_WHEN_ENABLED,GOOGLE_OIDC_REQUIRED_WHEN_ENABLED,SSO_REQUIRED_WHEN_ENABLED) directly from those features' own config modules rather than re-listing var names here —configured: booleanonly ever reflects whether the required var(s) are PRESENT, never a value (issue's own security note: "Avoid leaking whether a provider credential exists beyond safe status flags such asconfigured: true"). - Break-glass UX does not re-implement the eligibility check — the
form always shows the requirement inline next to
sso_required/ "disable password login", blocks an obviously-doomed submit client-side (zero break-glass identities selected at all) as a fast UX nicety, and always surfaces the server's authoritative409 BREAK_GLASS_REQUIREDrejection through the same translated error-message banner (error.break_glass_required, already inerror-messages.tssince #591) every other mutation on the page uses. The break-glass identity picker itself needsidentity_access.user_management.read(reused fromadmin/access-users.astro's own guard) to render a checkbox list of tenant users; without it, the page falls back to a plain comma-separated-UUID text input so the form stays usable under least privilege rather than disappearing entirely. - Client secret fields are write-only — never pre-filled or
round-tripped from the API on the provider edit form, matching #591's
own
AuthProviderViewnever exposingclient_secret_ciphertext. identity_access's module descriptor (module.ts) now declares anavigationentry (/admin/security,requiredPermission: "identity_access.sso_policy.read") — the existing module-navigation registry (#518) renders it in the admin sidebar automatically; noAdminLayout.astrohardcoding needed, same patterntenant_domain/module_management's own descriptors already use.- Playwright E2E specs (
tests/e2e/admin-security-disabled.e2e.ts/admin-security-enabled.e2e.ts) log in through the REAL/loginform (fill + submit + wait for the/adminredirect), notpage.request.post("/api/v1/auth/login")— empirically, in this environment, a SUCCESSFUL login'sSet-Cookieresponse headers going through Playwright'spage.requestAPI (as opposed to a real navigation/form submit) intermittently broke every subsequentpage.request/page.gotocall with an unrelated-lookingTypeError: "<path>" cannot be parsed as a URL.— reproduces even with a fully qualified absolute URL string, only after a 200 response carryingSet-Cookie; a failed login attempt (401/403, no cookie) never reproduces it. Root cause not fully isolated (did not reproduce fromBun.spawn,Bun.SQL, orBun.password.hashin isolation, only their combination through a specific call path) — logged here so a future issue that needspage.requestfor an authenticated flow in this repo doesn't have to re-discover it from scratch; driving the real login form sidesteps the whole class of that bug and is arguably the more faithful "browser E2E" exercise anyway. Both specs seed an isolated owner/tenant fixture directly via SQL (tests/e2e/helpers/seed-owner-tenant.ts, run in a SEPARATEbunsubprocess viaseed-owner-tenant-cli.ts— keeping the argon2/Postgres work out of the same process that drives Playwright regardless of the exact trigger above) rather thanPOST /api/v1/setup/initialize, which is a once-only singleton-locked endpoint (awcms_setup_state) almost always already claimed on any long-lived dev database.
Aturan lintas-issue yang wajib diikuti (#588-#593)
- Setiap fitur (#588-#592) WAJIB memanggil
isFullOnlineSecurityActive(env)sebelum melakukan apa pun online/provider-terkait — jangan cekAUTH_ONLINE_SECURITY_ENABLED/_PROFILElangsung atau bikin gate sendiri. Tidak aktifnya gate ini harus berarti: tidak ada panggilan Cloudflare/Google/OIDC apa pun, tidak ada MFA challenge, form login tetap seperti hari ini persis. AUTH_ONLINE_SECURITY_ENABLED=false/unset tidak boleh pernah mewajibkan credential provider apa pun —.env.exampledefault dan setiap deployment offline/LAN yang tidak pernah menyentuh varAUTH_ONLINE_SECURITY_*/AUTH_SSO_*/AUTH_MFA_*/AUTH_GOOGLE_*/TURNSTILE_*harus tetapconfig:validatePASS dan berperilaku identik dengan sebelum epic ini ada.APP_ENV=productionBUKAN setara dengan full-online — deployment offline/LAN bisa production-grade secara operasional (lihatdeployment-profiles.md) tanpa pernah mengaktifkan gate ini. Jangan pernah menjadikanAPP_ENV=productionsebagai proxy untukisFullOnlineSecurityActive.- Login password lokal tidak pernah dihapus/dinonaktifkan secara
default oleh fitur mana pun di epic ini —
sso_required/password_login_enabled=false(#591,awcms_tenant_auth_policies) hanya boleh aktif kalau ada break-glass local owner/account valid, dicek server-side (saveTenantAuthPolicy) sebelum kebijakan itu bisa disimpan — sudah diimplementasikan konkret, lihat §Generic tenant OIDC SSO provider di atas. - Kredensial provider (Google client secret, OIDC client secret,
Turnstile secret key, TOTP seed, recovery code) tidak pernah
disimpan plaintext — dari environment variable/secret manager, atau
dienkripsi at-rest dengan key dari environment (
AUTH_SSO_CREDENTIAL_ENCRYPTION_KEY/AUTH_MFA_SECRET_ENCRYPTION_KEY, dsb.) — tidak pernah muncul di response API, log, atau audit attributes (awcms-sensitive-data). - Link/unlink provider account, perubahan kebijakan auth, enroll/disable
MFA, dan regenerate recovery code semuanya high-risk actions — wajib
diaudit (
awcms-audit-log) dan idempotent kalau mutation (awcms-idempotency). - Provider identifier stabil adalah
sub(subject OIDC), bukan email — auto-link by email wajib mensyaratkan email terverifikasi + kebijakan allowed-domain eksplisit, tidak pernah linking implisit murni dari kecocokan string email. Sudah diimplementasikan konkret di #590 (isEmailDomainAllowed, fail-closed) DAN #591 (generic tenant OIDC SSO,isAutoLinkAllowedForProvider— dua lapis: domain allow-list PER PROVIDER ditambah master switchauto_link_verified_emailper tenant policy). - Semua panggilan provider eksternal (OIDC discovery/JWKS, Turnstile
siteverify, Google token exchange) wajib timeout-bounded DAN circuit
breaker-nya hanya boleh trip pada kegagalan transport genuine — pola
sama seperti
cloudflare-dns-adapter.ts/mailketing-provider.ts/turnstile.ts/google-oauth-client.ts(withTimeout, circuit breakergetProviderCircuitBreaker), dan tidak pernah dipanggil di dalam DB transaction (ADR-0006). JANGAN treat respons 4xx yang valid (input attacker-controlled ditolak provider dengan benar) sebagai provider-failure — pelajaran dari bug Turnstile PR #596, diulang benar di #590'sexchangeAuthorizationCode. Aturan yang sama berlaku untuk breaker DATABASE bawaan (getDatabaseCircuitBreaker(), dipakaiwithTenantuntuk SEMUA endpoint/tenant, bukan sekadar satu provider) — endpoint tak terautentikasi TIDAK BOLEH melakukan INSERT/UPDATE ber-FK ke tabel tenant-scoped dengantenantIdyang belum divalidasi keberadaannya; exception (mis. foreign-key violation) dari input attacker-controlled akan ditangkapwithTenant's catch-all dan mentrip breaker aplikasi-lebar ini — blast radius JAUH lebih besar dari breaker per-provider mana pun (pelajaran PR #598, lihat §Google OIDC login di atas). SelaluSELECT(aman, tidak throw untuk baris kosong) sebelum write ber-FK di endpoint yang bisa dijangkau tanpa autentikasi. - Tabel baru yang tenant-scoped (
awcms_identity_provider_accounts,awcms_oidc_auth_requests— #590;awcms_auth_providers,awcms_tenant_auth_policies— #591;awcms_identity_mfa_factorsdkk. — #589) wajib RLSENABLE+FORCE— pola sama seperti setiap migration sejak 013 (awcms-new-migration). - MFA reset password tidak boleh jadi bypass MFA — reset password yang berhasil tidak otomatis menonaktifkan MFA milik identity itu.
Penutup epic — di micro vs di sini
Seluruh sub-bagian di bawah ini adalah catatan penutupan epic di awcms-micro (ditutup di sana 2026-07-10). Dipertahankan karena daftar celah residual yang ditemukannya berlaku umum. Path, nomor migrasi, dan nomor issue-nya TIDAK berlaku di awcms — lihat §Peta ke artefak nyata.
Padanan di awcms: gate + Turnstile (#186), MFA/TOTP (#184,
sql/024), OIDC/SSO generik + admin CRUD (#185,sql/025/026, endpoint/api/v1/auth/sso/*dan/api/v1/auth/sso-providers), admin policy UI (#274,src/pages/admin/security.astro). Google OIDC spesifik (/api/v1/auth/providers/google/*) TIDAK ADA di awcms dan itu memang keputusan — jangan menyimpulkan sebaliknya dari paragraf mana pun di bawah.
Catatan audit penutup (micro) — celah konkret yang ditemukannya:
docs/awcms/18_configuration_env_reference.mddandeployment-profiles.mdsebelumnya masih menulis "#592-#593 masih backlog" walau #592 (admin policy UI) sudah merge — diperbaiki (stale doc, ditemukan oleh audit #593 ini, bukan hipotetis).docs/awcms/20_threat_model_security_architecture.mdsebelumnya NOL menyebut Turnstile/MFA/Google OIDC/SSO/break-glass sama sekali — ditambah §Standar tambahan dipicu epic full-online auth security hardening (Issue #587-#593) memetakan tujuh kategori risiko yang diminta eksplisit issue ini (credential stuffing, bot abuse, OIDC callback abuse, provider outage, MFA recovery abuse, SSO lockout, offline dependency breakage) ke bukti konkret yang sudah ada.scripts/security-readiness.tsmenambahcheckSsoBreakGlassReady(critical) — celah residual yang sudah dicatat di atas (§Generic tenant OIDC SSO provider):saveTenantAuthPolicyhanya memvalidasi break-glass eligibility di titik SAVE; sebuah break-glass identity bisa dinonaktifkan (atau tenant membership-nya dicabut) OLEH AKSI LAIN setelahnya tanpa kebijakan itu sendiri pernah disimpan ulang. Check baru ini mem-verifikasi ULANG eligibility setiap tenant aktif dari DB di waktu readiness/go-live, memakai ulangcountEligibleBreakGlassIdentities(kini diekspor daritenant-auth-policy.ts) — bukan aturan kedua yang bisa divergen. Berbeda dari Issue #605 (break-glass picker/data-hygiene UX di admin form) yang tetap dibiarkan terbuka sebagai issue terpisah — check readiness ini mengaudit DB, bukan UX form..env.example,scripts/validate-env.ts, OpenAPI (openapi/awcms-public-api.openapi.yaml), dansrc/modules/identity-access/README.mdsudah akurat sejak #587-#591 masing-masing — dikonfirmasi ulang oleh #593, tidak diubah.
Issue #601 (SQLSTATE class 22 circuit-breaker exclusion), #605 (break-glass
picker/data-hygiene UX admin), #603 (SSRF hardening untuk issuer_url
OIDC tenant-configured), dan #610 (hardening tambahan atas keputusan
#603 — rate limit agregat per-providerKey + negative-TTL cache) sudah
selesai sebagai follow-up terpisah setelah #593 (lihat §Break-glass
picker/data-hygiene di bawah untuk #605, dan §SSRF/issuer_url —
keputusan accepted risk untuk #603, termasuk hardening #610 di
sub-bagian yang sama).
SSRF/issuer_url — keputusan accepted risk (Issue #603, selesai)
Diputuskan TIDAK menambah IP-range denylist (resolve hostname, tolak
private/loopback/link-local/metadata-endpoint) untuk issuer_url OIDC
tenant-configured (#591). Ini SATU-SATUNYA outbound URL di base ini yang
berasal dari data tenant-configured, bukan env server tepercaya (beda dari
setiap provider lain — R2, Mailketing, Cloudflare DNS/Turnstile — yang
semuanya SSRF-safe by convention: URL selalu dari process.env).
Kenapa TIDAK diblok — dikoreksi setelah audit keamanan PR #609 (versi
awal keputusan ini salah menyebut LAN-first/offline sebagai alasan;
faktanya fitur generic SSO ini HANYA aktif di profil full_online
(isFullOnlineSecurityActive) — KEBALIKAN dari LAN-first/offline, yang
tidak pernah memuat kode ini sama sekali karena gate-nya tidak aktif).
Alasan yang benar: deployment full_online (cloud/registry) tetap sering
perlu terhubung ke IdP enterprise milik tenant yang di-host on-prem dan
hanya reachable lewat VPN/tunnel privat — pola "bring-your-own-IdP" yang
umum di produk SaaS multi-tenant (WorkOS, Auth0 Enterprise Connections,
dst. — BUKAN Okta/Auth0/Azure AD sendiri sebagai IdP, yang bukan analogi
yang tepat karena AWCMS di sini berperan sebagai relying party yang
memanggil issuer pihak ketiga, bukan sebagai IdP). Blanket private-IP
block akan mematahkan skenario enterprise-IdP-via-VPN ini.
Batas mitigasi yang sebenarnya (dikoreksi) — PENTING, jangan anggap
sudah menutup risiko eksploitasi: gate ABAC
(identity_access.sso_providers.create/update) dan audit log HANYA
membatasi siapa yang bisa MENGONFIGURASI issuer_url jahat. Keduanya
TIDAK membatasi siapa yang bisa MEMICU fetch keluar setelah provider
dikonfigurasi — GET /api/v1/auth/sso/{providerKey}/start yang memicu
discoverOidcConfiguration bersifat tanpa autentikasi, hanya rate-limit
per-sumber+tenant (bukan per-providerKey), dan discovery cache hanya
terisi pada request SUKSES — target internal yang tak pernah membalas
JSON OIDC valid tak pernah ter-cache, sehingga endpoint publik ini bisa
dipakai probe berulang tanpa batas nyata. Ini risiko residual yang
diterima BERSAMA keputusan utama, bukan celah yang sudah tertutup oleh
ABAC — segmentasi jaringan level operator untuk service internal yang
sungguh sensitif tetap jadi lapis pertahanan yang sebenarnya, bukan ABAC.
Follow-up — selesai (Issue #610), revisi setelah DUA putaran security review:
- Fix Critical, sebenarnya bug pre-existing sejak #591: SEMUA
cache/circuit-breaker di
generic-oidc-client.ts(discoveryCache/jwksCache, breaker discovery/jwks/token) sebelumnya di-key HANYA olehproviderKey.provider_keycuma unik PER TENANT (unique index migration 036 adalah(tenant_id, provider_key)), jadi dua tenant berbeda yang sama-sama menamai provider mereka"okta"(sangat umum) BERBAGI entry cache/breaker yang sama — tenant admin jahat bisa mendaftarkan provider dengan slug vendor umum yangissuer_url-nya menunjuk server attacker, memicu satu fetch, danauthorization_endpoint/jwks_uriattacker itu ter-serve ke tenant LAIN yang punya provider"okta"sungguhan — bukan sekadar kebocoran ketersediaan, tapi primitif pengambilalihan SSO lintas-tenant (redirect ke phishing + ID token palsu yang lolos verifikasi JWKS attacker sendiri). Diperbaiki:discoverOidcConfiguration/fetchProviderJwks/exchangeAuthorizationCodekini menerimatenantIddan meng-key semua cache/breaker dengan${tenantId}:${providerKey}(scopedProviderKey). Test:tests/unit/generic-oidc-client.test.ts's "CRITICAL: two DIFFERENT tenants using the SAME providerKey..." dantests/integration/tenant-sso-flow.integration.test.ts's "CRITICAL: two DIFFERENT tenants both naming their provider 'okta'...". - Koreksi desain — draft awal PR ini menambah bug baru: draft awal
menambah rate limit AGREGAT (bukan per-sumber) di
start.ts, di-key${tenantId}:${providerKey}, untuk membatasi prober yang merotasi source IP. Putaran security-auditor KEDUA menemukan budget BERSAMA ini sendiri adalah vektor DoS tanpa privilege apa pun: siapa pun, dari sesedikit 3 source IP, bisa menghabiskan seluruh budget dan mengunci SEMUA user sah tenant itu dari login SSO selama jendela rate limit, berulang — test yang ditambahkan draft awal untuk membuktikan mekanisme ini justru secara tidak sengaja MEMBUKTIKAN DoS tersebut. Rate limit agregat ini dihapus total. Pertahanan sebenarnya terhadap probing berkelanjutan adalah circuit breaker (kini benar-benar di-scope tenant+provider) + negative-TTL cache di bawah — keduanya HANYA membatasi percobaan yang GAGAL, jadi tidak pernah bisa memblokir login sah ke provider yang sehat, beda dari rate limit HTTP-level bersama yang memblokir semua request regardless hasil. - Negative/short-TTL cache (
NEGATIVE_CACHE_TTL_MS = 30s,discoveryFailureCache/jwksFailureCachedigeneric-oidc-client.ts, kini di-key${tenantId}:${providerKey}) untuk percobaan discovery/JWKS yang GAGAL — target yang tak pernah membalas JSON valid tidak lagi memicu fetch baru di setiap hit, hanya sekali per jendela 30 detik. - Rekomendasi infra-layer blokir egress ke
169.254.169.254(metadata endpoint cloud) khusus deploymentfull_onlinedidokumentasikan dideployment-profiles.md§Generic tenant OIDC SSO (residual yang tetap di luar cakupan aplikasi — tanggung jawab operator, bukan kode). Follow-up — selesai (Issue #612): tanpa cap, tenant admin jahat bisa mendaftarkan banyak barisawcms_auth_providers(masing-masing dapat budget cache/breaker independen sendiri-sendiri, sudah benar di-scope sejak #610) untuk melipatgandakan volume probing total secara linear dengan jumlah baris. Diperbaiki dengan capAUTH_SSO_MAX_PROVIDERS_PER_TENANT(default 20) dicreateAuthProvider(auth-provider-directory.ts) — `POST /api/v1
Best for
- Use Awcms Auth Online Hardening when this documented workflow matches the task.
Tips and best practices
- Review the source instructions and adapt inputs before running the workflow.
What This Skill Can Do
AI-generated examples showing real capabilities
Was this skill useful?
Be the first to share a result.