Skip to content

Feature SSO Akses Panel Admin OpenSID dari OpenKab - #1104

Open
pandigresik wants to merge 10 commits into
rilis-devfrom
2502-opensid-sso-access
Open

Feature SSO Akses Panel Admin OpenSID dari OpenKab#1104
pandigresik wants to merge 10 commits into
rilis-devfrom
2502-opensid-sso-access

Conversation

@pandigresik

@pandigresik pandigresik commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Pull Request: Feature SSO Akses Panel Admin OpenSID dari OpenKab

Description

Menerapkan Single Sign-On (SSO) yang memungkinkan administrator OpenKab (sesi aktif + 2FA selesai) masuk ke panel admin OpenSID desa hanya dengan sekali klik "Masuk ke OpenSID", tanpa login manual. OpenKab menerbitkan JWT RS256 (RSA-2048) sekali pakai berumur pendek, OpenSID memverifikasinya kembali ke OpenKab (server-to-server) dan melakukan pengecekan ganda status/izin di datanya sendiri sebelum membuat sesi. Seluruh percobaan (berhasil/gagal) dicatat append-only di kedua sistem dan dapat direview super admin melalui dashboard audit.

Changes made:

  1. New: app/Services/SsoKeyManager.php — Manajemen kunci RS256: muat PEM private/public dari env/file, validasi RSA ≥2048-bit, dukungan beberapa public key untuk rotasi transisi
  2. New: app/Services/SsoTokenService.php — Terbitkan/verifikasi/konsumsi token JWT RS256 (klaim jti/iat/nbf/exp/sub/desa_id/iss/aud, TTL ≤600 detik default 300, leeway 30 detik, alg dipaksa RS256 anti downgrade), persisten ke openkab_sso_tokens, konsumsi atomik sekali pakai
  3. New: app/Services/SsoAuditLogger.php — Pencatatan audit immutabel (logAttempt & logVerification) ke openkab_sso_logs
  4. New: app/Services/OpenSidUrlResolver.php — Resolusi URL admin OpenSID server-side dari field attributes.website desa pada API database gabungan (/api/v1/wilayah/penduduk, cache singkat ±5 menit), validasi skema HTTPS; input klien tidak pernah menjadi sumber URL redirect (FR-022). Konfigurasi manual desa_sso_configs / SSO_OPENSID_BASE_URL dihapus
  5. New: app/Http/Controllers/Sso/SsoLoginController.php — Endpoint POST /sso/generate-session: gate kelayakan berurutan (role administrator, 2fa_verified, 2FA aktif, akun aktif/tidak terkunci, scope kabupaten), validasi origin, rate limit, resolusi URL dari website API, respons JSON {status, redirect_url, token, token_method, expires_at, retry_after}; error generik 'Autentikasi gagal.' (FR-018)
  6. New: app/Http/Controllers/Api/SsoVerifyController.php — Endpoint POST /api/v1/sso/verify-token: verifikasi RS256, cek sekali pakai/replay/kedaluwarsa, cek ulang state user, konsumsi atomik, log, respons minimal tanpa PII; validasi memakai app/Http/Requests/SsoVerifyRequest.php (FormRequest)
  7. New: app/Http/Middleware/SsoCallbackAuth.php + SsoIpWhitelist.php — Otentikasi callback server-to-server (sekret bersama + HMAC-SHA256 atas body + timestamp ±30 detik, hash_equals), whitelist IP opsional
  8. New: app/Http/Controllers/Sso/SsoAuditController.php — Dashboard audit (DataTables + filter); DesaSsoConfigController dihapus
  9. New: Model & migrasi — OpenKabSsoLog (append-only, tolak update/delete), OpenKabSsoToken (konsumsi atomik); tabel openkab_sso_logs, openkab_sso_tokens (tabel desa_sso_configs dihapus/di-rollback)
  10. New: Migration 2026_08_11_000001_add_sso_audit_permission.php — buat permission sso-audit-read, lampirkan ke role administrator, lalu admin:menu-update (logika seeder SsoPermissionsSeeder di-inline ke migration, seeder dihapus)
  11. New: config/sso.php — Konfigurasi fitur (kunci RS256, SSO_CALLBACK_SECRET, TTL, toleransi jam, rate limit, endpoint)
  12. New: resources/views/sso/ — View dashboard audit + partial filter (view konfigurasi desa dihapus)
  13. Modified: resources/views/desa/index.blade.php — Tombol "Masuk ke OpenSID" hanya untuk desa dengan website terisi (auto-submit form POST token, CSP nonce); desa tanpa website menampilkan keterangan "URL website belum diisi" (bukan tombol)
  14. Modified: routes/web.php (generate-session, audit; route sso-config dihapus), routes/apiv1.php (verify-token), bootstrap/app.php (alias middleware sso.callback/sso.ip-whitelist), app/Enums/Modul.php (submenu "Audit Akses SSO" di bawah "Pengaturan OpenSID"), app/Providers/AppServiceProvider.php (rate limiter sso-generate 5/menit per user+IP)
  15. New: app/Console/Commands/SsoPurgeTokensCommand.phpphp artisan sso:purge-tokens untuk pembersihan token kedaluwarsa
  16. New: app/Console/Commands/SsoGenerateKeysCommand.php (php artisan sso:generate-keys) + scripts/sso-keygen.sh — generate keypair RSA (default 2048, opsi --bits), tulis private key (chmod 0600) + public key, isi SSO_SIGNING_PRIVATE_KEY_FILE/SSO_SIGNING_PUBLIC_KEY_FILE di .env; menolak bila kunci sudah terisi kecuali --force
  17. New: docs/sso-opensid.md — Panduan deployment & operasional (sso:generate-keys, env, rotasi kunci, pembersihan)
  18. Modified: composer.json (+firebase/php-jwt ^7.0), .env.example (hapus SSO_OPENSID_BASE_URL), .gitignore (+/storage/sso/), README.md
  19. New: Tes — tests/Feature/Sso/SsoGenerateSessionTest.php, SsoSecurityTest.php, SsoVerifyTokenTest.php, SsoAuditDashboardTest.php, SsoGenerateKeysCommandTest.php (37 test) + tests/Browser/SmokeSsoAccessTest.php (3 smoke test) + fixture keypair di tests/fixtures/sso/

Reason for change:

  • Efisiensi: Menghilangkan login ulang ke setiap panel OpenSID desa merupakan nilai inti fitur sesuai issue Fitur Single Sign-On (SSO) dari OpenKab ke OpenSID untuk Administrator Desa #1103
  • Keamanan: Token ditandatangani RS256 asimetris — private key hanya di OpenKab, public key didistribusikan ke OpenSID, sehingga kompromi satu pihak tidak cukup untuk memalsukan token; ditambah lapisan otentikasi callback (sekret bersama + HMAC), TTL singkat, token sekali pakai, rate limiting, dan log audit immutabel
  • Verifikasi ganda: OpenSID tetap mengecek ulang status/izin user di datanya sendiri sebelum membuat sesi (mencegah privilege escalation & account takeover)

Impact of change:

  • Keamanan: Semua percobaan ditolak secara default-to-reject dengan pesan error generik tanpa bocor PII; percobaan tidak sah (non-admin, tanpa 2FA, token tamper/expired/replay, rate limit) tercatat lengkap
  • Kepatuhan: Audit trail komprehensif berhasil/gagal di kedua sistem, dapat disaring super admin di dashboard
  • Ops: Instalasi mudah via php artisan sso:generate-keys (atau scripts/sso-keygen.sh); rotasi kunci didukung tanpa memutus token yang masih berlaku

Related Issue

#1103

Steps to Reproduce

Sebelum (masalah):

  1. Administrator membuka panel admin OpenSID desa
  2. Login manual dengan kredensial OpenSID setiap kali berpindah desa
  3. ❌ Tidak ada satu klik, tidak ada audit terpusat, risiko kredensial tersebar

Sesudah (solusi):

  1. Login ke OpenKab sebagai administrator (2FA aktif)
  2. Buka halaman Data Desa → klik "Masuk ke OpenSID" pada desa target yang website-nya terisi (desa tanpa website menampilkan keterangan "URL website belum diisi")
  3. Token JWT RS256 diterbitkan (sekali pakai, ≤10 menit); URL redirect di-resolve server-side dari website desa di API gabungan
  4. Browser auto-submit POST token ke OpenSID → OpenSID memverifikasi balik ke OpenKab
  5. ✅ Ter-redirect ke panel admin OpenSID dalam keadaan login, tanpa memasukkan kredensial
  6. Percobaan tercatat di openkab_sso_logs (dan opensid_sso_logs di sisi OpenSID)

Testing pada fitur terkait:

  • Autentikasi & 2FA OpenKab ✅ (gate 2fa_verified, 2fa_enabled, isLocked)
  • Halaman Data Desa ✅ (tombol SSO + CSP nonce)
  • Rate limiting ✅ (429 + Retry-After setelah 5 percobaan/menit)

Checklist

  • I have complied with script writing rules
  • I have followed pull request review process
  • I have created unit test/integration test to verify the feature
  • Manual testing has been done in development environment
  • No console errors or warnings
  • Code has been reviewed by at least 1 person

Technical Details

Technical Explanation

Alur verifikasi dua lapis:

Browser ──POST sso_token──▶ OpenSID /admin/sso-login
OpenSID ──POST /api/v1/sso/verify-token (X-SSO-Callback-Key + HMAC + timestamp)──▶ OpenKab
OpenKab ──(verifikasi RS256 + status sekali pakai + cek ulang state user)──▶ 200 {success|error}
OpenSID ──(cekp ulang user lokal di datanya sendiri)──▶ buat sesi → redirect /admin
  • RS256 mengotentikasi issuer token (OpenKab); SSO_CALLBACK_SECRET mengotentikasi pemanggil endpoint verifikasi (OpenSID) — dua lapisan yang berbeda, keduanya wajib.
  • Konsumsi token sekali pakai secara atomik (UPDATE ... SET used_at = NOW() WHERE jti = ? AND used_at IS NULL).
  • SsoTokenService::verify() mencoba seluruh public key yang terdaftar (rotasi transisi) dan menegakkan alg=RS256 pada Key.
  • Log audit OpenKabSsoLog menolak update/delete pada level model (append-only).

Configuration changes

.env.example menambahkan:

  • SSO_SIGNING_PRIVATE_KEY / SSO_SIGNING_PRIVATE_KEY_FILE (RSA-2048, hanya OpenKab)
  • SSO_SIGNING_PUBLIC_KEY / SSO_SIGNING_PUBLIC_KEY_FILE (didistribusikan ke OpenSID)
  • SSO_SIGNING_PUBLIC_KEYS_FILE (public key lama untuk rotasi transisi)
  • SSO_CALLBACK_SECRET (≥32 byte, dibagikan ke OpenSID)
  • SSO_TOKEN_TTL, SSO_IP_WHITELIST, SSO_RATE_LIMIT_MAX, SSO_CLOCK_SKEW_TOLERANCE
  • (SSO_OPENSID_BASE_URL dihapus — URL OpenSID kini dari website desa di API gabungan)

Permission sso-audit-read + menu "Audit Akses SSO" dibuat otomatis oleh migration 2026_08_11_000001 saat php artisan migrate.

Dependencies added

  • firebase/php-jwt: ^7.0 (v6.x dikeluarkan karena terdampak security advisory)

Testing

Manual Testing

  • Login administrator + 2FA, klik "Masuk ke OpenSID" → masuk panel OpenSID tanpa login ulang
  • Desa tanpa website → tombol diganti keterangan "URL website belum diisi"
  • Klik dua kali token yang sama → percobaan kedua ditolak (replay), tidak ada sesi baru
  • Non-admin / tanpa 2FA / akun nonaktif → ditolak dengan pesan generik
  • Super admin membuka dashboard audit → filter status/tanggal/desa/admin berfungsi
  • Callback tanpa X-SSO-Callback-Key yang benar → 401
  • php artisan sso:generate-keys → file kunci + .env terisi; dijalankan lagi → ditolak tanpa --force

Automated Testing

  • tests/Feature/Sso/SsoGenerateSessionTest.php — alur berhasil, validasi desa_id, TTL, alg RS256, desa tanpa website/API gagal → CONFIGURATION_ERROR
  • tests/Feature/Sso/SsoSecurityTest.php — non-admin, tanpa 2FA, akun nonaktif/terkunci, origin invalid, desa tanpa website, rate limit 429, tanpa bocor PII
  • tests/Feature/Sso/SsoVerifyTokenTest.php — valid, replay, expired, tamper, kunci lain, HS256 (anti downgrade), callback unauthorized, tanpa bocor PII
  • tests/Feature/Sso/SsoAuditDashboardTest.php — akses super admin, filter, 403 non-permission
  • tests/Feature/Sso/SsoGenerateKeysCommandTest.php — keypair + isi env, perms 0600, JWT RS256 roundtrip, tolak tanpa --force, --force menimpa, tolak --bits<2048
  • tests/Browser/SmokeSsoAccessTest.php — tombol tampil, keterangan untuk desa tanpa website, auto-submit (Playwright/Pest Browser)

Status: php artisan test tests/Feature/Sso37 passed (136 assertions) (dengan gate kelayakan aktif); tests/Browser/SmokeSsoAccessTest.php3 passed.

Screenshots / Video

simplescreenrecorder-2026-08-11_09.31.14.mp4

Sebelum:

Belum ada — fitur baru (login manual ke tiap OpenSID).

Sesudah:

Tombol Masuk ke OpenSID pada halaman Data Desa

Breaking Changes

  • Dihapus: tabel & model desa_sso_configs, route sso-config, permission/menu CRUD konfigurasi desa, dan env SSO_OPENSID_BASE_URL/config('sso.opensid_base_url') — base URL OpenSID kini di-resolve dari field website desa pada API database gabungan. Admin yang sebelumnya memakai konfigurasi manual harus memastikan website desa terisi di data gabungan.
  • Seeder dihapus: SsoPermissionsSeeder dipindah (di-inline) ke migration 2026_08_11_000001_add_sso_audit_permission — cukup php artisan migrate untuk permission sso-audit-read + menu.
  • Endpoint & perilaku lain tidak berubah (aditif).

Migration Guide

  1. Jalankan composer install (dependency firebase/php-jwt ^7.0).
  2. Generate keypair: php artisan sso:generate-keys (atau bash scripts/sso-keygen.sh) → isi SSO_SIGNING_PRIVATE_KEY_FILE/SSO_SIGNING_PUBLIC_KEY_FILE di .env; distribusikan public key ke instalasi OpenSID (out-of-band).
  3. Set SSO_CALLBACK_SECRET (≥32 byte) dan bagikan ke OpenSID.
  4. php artisan migrate (migration 2026_08_11_000001 membuat permission sso-audit-read + menjalankan admin:menu-update).
  5. Pastikan field website desa terisi di API database gabungan (sumber base URL OpenSID).
  6. Jadwalkan php artisan sso:purge-tokens untuk pembersihan token kedaluwarsa (opsional).
  7. Implementasi sisi OpenSID mengikuti specs/001-opensid-sso-access/contracts/opensid-sso-contract.md (repo terpisah).

References

  • Kontrak API OpenKab: specs/001-opensid-sso-access/contracts/openkab-sso-api.md
  • Kontrak integrasi OpenSID: specs/001-opensid-sso-access/contracts/opensid-sso-contract.md
  • Panduan deployment: docs/sso-opensid.md
  • JWT Best Practices (RFC 7519)
  • OWASP Session Management / CSRF Prevention Cheat Sheet

Additional notes: Dokumentasi spesifikasi, plan, riset, dan data model tersimpan di specs/001-opensid-sso-access/ (diabaikan dari git). Bagian frontend tombol SSO menampilkan pesan generik via SweetAlert bila permintaan gagal. Screenshot pada bagian atas bersifat placeholder — dapat ditambahkan setelah demo di environment staging.

@github-actions

Copy link
Copy Markdown

🔄 AI PR Review sedang antri di server...

Proses review akan segera dimulai di background — hasil akan muncul sebagai komentar setelah selesai.
Powered by CrewAI · PR #1104

@pandigresik

Copy link
Copy Markdown
Contributor Author
image

@pandigresik

Copy link
Copy Markdown
Contributor Author

Panduan deployment: docs/sso-opensid.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant