Appearance
Casdoor & OAuth2/OIDC — detail lengkap
Dipisah dari README.md (§ Auth: empat pilihan) supaya README tidak makin membengkak — README cukup rangkuman singkat + link ke sini.
Dua jalur yang dibahas di sini sengaja terpisah total satu sama lain (tidak berbagi kode): Core\Auth\Casdoor\*/Middleware\CasdoorAuth khusus aurora-sso (Casdoor), sedangkan Core\Auth\OAuth2\*/Middleware\OAuth2Auth generik untuk provider OAuth2/OIDC mana pun (termasuk Casdoor lewat config, kalau app lebih suka satu abstraksi yang sama dipakai multi-provider).
Casdoor: middleware (API) vs SDK (session-based login)
Middleware\CasdoorAuth untuk guard API bearer-token — app client sudah punya token (mis. dari SPA), middleware cuma verifikasi. Untuk app session-based yang perlu jalankan authorization-code flow penuh (redirect user ke Casdoor, terima callback dengan code, tukar jadi token, simpan user di session) pakai Core\Auth\Casdoor\Sdk — menggabungkan Client (acquire token, logic CLIENT bukan issuing) dan JwtVerifier (verifikasi signature) supaya app tidak perlu implementasi curl + JWT decode sendiri (pernah ada bug nyata: app decode id_token pakai base64_decode manual tanpa cek signature sama sekali — token bisa dipalsukan bebas).
php
$sdk = new Core\Auth\Casdoor\Sdk([
'api' => 'https://sso.example.com',
'clientId' => '...',
'secret' => '...', // JANGAN taruh fallback hardcoded di source
'redirect' => 'https://app.example.com/auth/callback',
'issuer' => 'https://casdoor-api.example.com', // dari /.well-known/openid-configuration
'jwksUrl' => 'https://casdoor-api.example.com/.well-known/jwks',
]);
// Controller loginAction():
Response::redirect($sdk->getAuthorizeUrl());
// Controller callbackAction():
$result = $sdk->exchangeAndVerify(Request::getQuery('code'));
if ($result === null) { /* code/token invalid, tolak */ }
$claims = $result['claims']; // sudah lolos verifikasi signature access_token
$token = $result['token']; // simpan access_token/refresh_token di session kalau perluPENTING: exchangeAndVerify()/refreshAndVerify() verifikasi signature dari klaim access_token, BUKAN id_token — JwtVerifier menolak token dengan klaim tokenType selain 'access-token'. Pastikan deployment aurora-sso Anda mengisi access_token dengan klaim identitas user (preferred_username/name/email/id), bukan cuma id_token.
Refresh token juga lewat Sdk (bentuk return identik {token, claims}, verifikasi signature ulang otomatis) — cocok dipanggil dari middleware guard sebelum access_token lama expired:
php
// Middleware, sebelum access_token kadaluarsa (mis. sisa < 5 menit):
$result = $sdk->refreshAndVerify(Session::get('refresh_token'));
if ($result === null) {
// refresh_token juga sudah invalid/revoked -- paksa login ulang
Session::remove('user');
return Response::redirect($sdk->getAuthorizeUrl());
}
Session::set('access_token', $result['token']['access_token']);
Session::set('token_expires_at', time() + ($result['token']['expires_in'] ?? 3600));
if (!empty($result['token']['refresh_token'])) {
Session::set('refresh_token', $result['token']['refresh_token']); // Casdoor bisa rotate refresh_token
}Core\Auth\Casdoor\Client (dipakai Sdk di baliknya) juga bisa dipakai langsung kalau cuma butuh mekanisme OAuth-nya tanpa verifikasi JWT bundel (getAuthorizeUrl(), exchangeCode(), refreshToken() — masing-masing ?array mentah dari Casdoor, belum diverifikasi).
OAuth2: middleware (API) vs SDK (session-based login), multi-provider
Config app/config/oauth2.php (multi-provider, mirip database.connections di README § Database):
php
return [
'default' => 'keycloak',
'providers' => [
'keycloak' => [
'issuer' => env('OAUTH2_KEYCLOAK_ISSUER', ''), // https://kc.example.com/realms/aurora
'clientId' => env('OAUTH2_KEYCLOAK_CLIENT_ID', ''),
'clientSecret' => env('OAUTH2_KEYCLOAK_CLIENT_SECRET', ''), // JANGAN hardcode fallback
'redirectUri' => env('OAUTH2_KEYCLOAK_REDIRECT', ''),
'scopes' => ['openid', 'profile', 'email'],
'audience' => env('OAUTH2_KEYCLOAK_AUDIENCE', null), // null => clientId
'algorithms' => ['RS256'],
'cacheTtl' => 3600,
// Opsional -- isi salah satu/semua buat skip OIDC discovery endpoint itu:
'authorizationEndpoint' => env('OAUTH2_KEYCLOAK_AUTH_URL', null),
'tokenEndpoint' => env('OAUTH2_KEYCLOAK_TOKEN_URL', null),
'jwksUri' => env('OAUTH2_KEYCLOAK_JWKS_URL', null),
],
// Provider lain, mis. 'auth0' => [...], route beda bisa guard beda provider.
],
];Middleware\OAuth2Auth untuk guard API bearer-token (@middleware("OAuth2Auth", "keycloak") per-route — args lewat annotation cuma jalan lewat jalur spread Route/BeforeMiddlewareHandler.php, sama seperti RateLimit; global tanpa args di app/config/config.php → 'middlewares' selalu pakai provider 'default'). Klaim tersimpan di registry key 'oauth2User.{provider}' — akses lewat oauth2_user($provider = null) (beda dari casdoor_user() yang flat/single-provider, helper ini butuh nama provider karena bisa lebih dari satu aktif sekaligus).
Untuk app session-based yang perlu jalankan authorization-code flow penuh pakai Core\Auth\OAuth2\Sdk — pola identik Casdoor\Sdk:
php
$sdk = new Core\Auth\OAuth2\Sdk([
'issuer' => 'https://keycloak.example.com/realms/aurora',
'clientId' => '...',
'clientSecret' => '...',
'redirectUri' => 'https://app.example.com/auth/callback',
// authorizationEndpoint/tokenEndpoint/jwksUri opsional -- auto-discovery
// dari 'issuer' kalau tidak diisi manual.
]);
// Controller loginAction():
Response::redirect($sdk->getAuthorizeUrl());
// Controller callbackAction():
$result = $sdk->exchangeAndVerify(Request::getQuery('code'));
if ($result === null) { /* code/token invalid, tolak */ }
$claims = $result['claims']; // sudah lolos verifikasi signature access_token
$token = $result['token']; // simpan access_token/refresh_token di session kalau perluBeda dengan Casdoor\JwtVerifier, Core\Auth\OAuth2\JwtVerifier tidak mengasumsikan klaim tokenType atau bentuk roles tertentu — itu quirk khusus deployment Casdoor, provider OIDC generik lain tidak punya klaim itu. Kalau butuh normalisasi klaim untuk provider tertentu, lakukan di app, bukan di package ini.
Catatan performa: kalau konfigurasi cuma kasih issuer (tanpa endpoint manual), Sdk/Client butuh OIDC discovery (.well-known/openid-configuration) sebelum bisa jalan — satu round-trip tambahan yang TIDAK di-cache di Sdk (dipakai jarang, ~2x/sesi login, sama seperti Casdoor\Sdk). Middleware\OAuth2Auth SELALU cache discovery + JWKS lewat dataCache (hot path, tiap request API).
authorizationEndpoint/tokenEndpoint/jwksUri manual override: berguna bukan cuma buat skip discovery, tapi juga buat kerja-sama dengan deployment Casdoor yang discovery document-nya sendiri salah/tidak konsisten — di organisasi tirta-patriot misalnya, origin/originFrontend Casdoor sempat tertukar (backend API di casdoor-api.app.tirtapatriot.co, tapi authorization_endpoint di discovery document balik ke domain portal, app.tirtapatriot.co, bukan domain login sso.tirtapatriot.co). Kalau ini terjadi di deployment Anda, isi authorizationEndpoint manual di config (endpoint lain tetap boleh lewat discovery seperti biasa — override cuma yang salah) sampai config originFrontend Casdoor dibetulkan di sisi maintainer-nya. Jangan lupa cabut lagi override-nya setelah dikonfirmasi discovery document sudah KONSISTEN benar di banyak request berturut-turut (deployment dengan beberapa replika backend bisa flip-flop antara config lama/baru selama rolling restart belum selesai).
Core\Auth\OAuth2\SsoLoginActions — trait controller login-redirect + callback
Login-redirect + callback glue siap pakai buat app session-based yang sebelumnya masing-masing hand-roll flow-nya sendiri (curl manual ke token endpoint, decode id_token TANPA verifikasi signature, state OAuth di-generate tapi tidak pernah dicek balik di callback — celah CSRF nyata). Trait ini bungkus Core\Auth\OAuth2\Sdk (yang verifikasi signature access_token lewat JWKS) jadi controller glue, dan tambah verifikasi state yang sebelumnya hilang.
Kenapa trait, bukan abstract base controller: app konsumen biasanya sudah extend base controller sendiri (mis. App\Modules\Defaults\Middleware\Controller) — PHP single inheritance tidak izinkan sekaligus extend base class lain kalau dipaksa lewat abstract class. Method @routeGet/@routePost yang didefinisikan di trait tetap ke-detect otomatis oleh Core\Route\AnnotationRoute sama seperti method biasa (reflection resolve method trait sama seperti method yang didefinisikan langsung di class), TAPI class-level @routeGroup TETAP wajib dideklarasi ulang di controller pemakai (tidak diwarisi — lihat AGENTS.md § Jebakan teknis).
php
class Controller extends BaseController
{
use \Core\Auth\OAuth2\SsoLoginActions;
// Satu-satunya hook WAJIB: klaim sudah lolos verifikasi signature,
// cocokkan ke user lokal, balikin array session-user atau null kalau
// tidak terdaftar.
protected function resolveLocalUser(array $claims): ?array
{
$appUser = ModelSso::findFirst([
'conditions' => 'id_aplikasi = :app: AND id_user = :id_user:',
'bind' => ['app' => 9, 'id_user' => $claims['id'] ?? null],
]);
return $appUser ? [
'id' => $appUser->id_user_kepegawaian,
'username' => $appUser->username,
// ...bentuk session bebas, app yang tentukan
] : null;
}
}Method yang disediakan trait (route otomatis lewat annotation di trait-nya sendiri, tidak perlu didefinisikan ulang):
loginCasdoorAction()(GET/POST /login-casdoor) — generatestate(disimpan di session, one-time use), redirect ke$sdk->getAuthorizeUrl($state).callbackAction()(GET /callback) — validasicodeada, validasistatecocok (hash_equals()), tukar code→token+verifikasi lewatSdk, panggilresolveLocalUser(),establishSsoSession(), lalu redirect ke "intended URL" (baca dari session, dibersihkan, fallback ke/panel/dashboard).
Hook lain yang boleh di-override (opsional, ada default yang masuk akal):
protected function ssoProvider(): string— default'casdoor', key diapp/config/oauth2.php→providers.protected function onSsoFailed(string $reason): ResponseInterface— default redirect/panel/auth/login?error={reason}; override kalau app pakai flash message, bukan query string.protected function establishSsoSession(array $sessionUser, array $token): void— default setSession::set('user', ...)+IS_USING/access_token/dst. Kalau app butuh session key tambahan, override method ini, panggil$this->defaultEstablishSsoSession($sessionUser, $token)dulu, baru tambah key lain (nama method beda sengaja, supaya tidak perlu trait conflict-resolution syntaxinsteadof/as).
resolveLocalUser()/onSsoFailed()/establishSsoSession() dites lewat DI mock manual (bukan network/DB asli) — lihat tests/Auth/OAuth2/SsoLoginActionsTest.php untuk pola (Client/JwtVerifier di-stub lewat subclass kosong, cukup dua method yang benar-benar dipanggil Sdk yang perlu di-override).