Skip to content

sauth — Gambaran Umum & Latar Belakang

sauth adalah kumpulan tiga library — satu Composer package PHP untuk penerbitan token, satu untuk validasi, dan satu npm package TypeScript untuk browser — yang bersama-sama membentuk satu lapisan autentikasi terpusat di seluruh gateway dan resource server.

TL;DR

BpmJWTsauth
Kepemilikan keyPrivate/public key yang sama disalin ke setiap repoSetiap gateway punya key pair sendiri; resource server hanya menyimpan public key
Siapa yang bisa menerbitkan tokenService mana pun yang berjalan secara lokalHanya gateway (GWA, GWC) — resource server tidak pernah menerbitkan token
Dev mockingSalin key + debug endpoint isLocal(), permission default diam-diam diset ke 'wm'Env var SAUTH_BYPASS + fake claims JSON file, tidak perlu key
M2M tokenTidak ada konsep — bentuknya sama dengan user tokenPemisahan struktural: tidak ada snm = M2M; middleware sauth.m2m yang khusus
Akses pihak ketiga (3p)Tidak mungkin — tidak ada claim scopeOAuth PKCE + claim scope lengkap; developer eksternal bisa mengakses resource server dengan aman
Long-lived tokenToken dengan exp tinggi secara ad-hoc, tanpa mekanisme pencabutanAPI key JWT dengan jti UUID + tabel blacklist per-service
MiddlewareJwtVerification disalin-tempel ke setiap service, logikanya menyimpang seiring waktuSatu library sauth-client, lima middleware bernama dengan tanggung jawab yang jelas
ValidasiPengecekan string pada iss + verifikasi dengan shared keyPublic key per-issuer + signature kriptografis + pengecekan claim aud

Singkatnya: BpmJWT adalah JWT helper berbasis shared key di mana service mana pun yang memiliki key bisa memalsukan token untuk service lain. sauth memberikan key pair tersendiri untuk setiap gateway, menstandarisasi bentuk token di seluruh service, menambahkan OAuth flow yang proper untuk M2M dan akses pihak ketiga, serta menggantikan long-lived token ad-hoc dengan API key JWT yang bisa diaudit.


Masalah Sebelum sauth

Sebelum sauth, setiap service menyimpan salinannya sendiri dari sebuah helper class JWT bernama BpmJWT, beserta pasangan private key dan public key secara lengkap.

Cara Kerja BpmJWT

Sebuah resource server seperti PNR menyimpan BpmJWT.php, private key, dan public key di direktori storage/-nya sendiri. Saat pengembangan lokal, ada debug endpoint (hanya terdaftar ketika app()->isLocal()) yang memungkinkan developer menerbitkan token langsung dari resource server itu sendiri:

php
// routes/api.php — hanya di lokal
if (app()->isLocal()) {
    Route::prefix('/token')->controller(JwtController::class)->group(function () {
        Route::post('/issue', 'apiIssue');   // terbitkan token
        Route::post('/claim', 'apiClaims');  // baca claims
        Route::post('/refresh', 'apiRefresh');
        Route::post('/destroy', 'revokeToken');
    });
}

issueToken() membangun payload dan menandatanganinya dengan private key resource server itu sendiri:

php
$accessPayload = [
    "iss" => "03-pnr",   // PNR menerbitkan tokennya sendiri
    "aud" => $target,
    "iat" => now()->timestamp,
    "exp" => now()->addMinutes(10)->timestamp,
    "sid" => $userId,
    "snm" => $userName,
    "fet" => $permissions ?? 'wm',   // default: webmaster bypass
];

Akar Masalahnya

Shared Key Pair

GWA dan GWC menggunakan pasangan private/public key yang sama, dan setiap resource server menyimpan salinan keduanya. Sebuah JWT yang ditandatangani oleh PNR secara kriptografis identik dengan yang ditandatangani GWA — key-nya sama, signature-nya sama. Satu-satunya perlindungan hanyalah pengecekan string pada claim iss.

Local Mode Memperluas Daftar Issuer yang Dipercaya

php
// JwtVerification.php
if (!in_array($decoded->iss, app()->isLocal() ? $code : ['01-gwa', '02-gwc'])) {
    throw new \Exception('Invalid issuer', 403);
}

Di local mode, $code mencakup semua kode service termasuk '03-pnr'. Artinya, service apa pun yang berjalan secara lokal bisa menerbitkan token yang lolos validasi di service mana pun.

Default Webmaster yang Diam-diam

php
public function issueToken(string $userId, string $target = '', string|array $permissions = null): array
{
    // ...
    $accessToken = $this->theAccessToken(
        $userName,
        $userId,
        $permissions ?? 'wm',   // lupa isi permissions? dapat akses webmaster
        $targetStr
    );

Lupa mengisi $permissions secara diam-diam memberikan akses level webmaster ke mana-mana.

Tidak Ada Konsep M2M, Scope, maupun Dukungan 3p

BpmJWT tidak mengenal konsep machine-to-machine token, tidak ada OAuth scope claim, dan tidak ada cara yang aman untuk menerbitkan token ke developer eksternal. Setiap token terlihat sama, tidak peduli apakah yang memakainya adalah pengguna manusia atau background job.

Long-Lived Token Tidak Bisa Dicabut

Untuk integrasi yang stabil, developer menerbitkan access token berumur panjang (nilai exp tinggi) atau menjaga token tetap hidup via refresh. Satu-satunya "pencabutan" adalah Cache::forget() pada refresh cache key — yang hanya memblokir pembaruan token, tidak membatalkan access token yang sudah ada dan masih dalam window exp-nya. Tidak ada identifier jti, tidak ada blacklist.

Middleware yang Disalin-Tempel dengan Logika yang Berbeda-beda

JwtVerification diduplikasi ke setiap resource server dengan modifikasi lokal masing-masing — daftar issuer berbeda, pesan error berbeda, logika permission yang halus berbeda. Tidak ada satu sumber kebenaran.


Apa yang Digantikan sauth

Konsep BpmJWTPengganti di sauth
Shared key pair di setiap repoSetiap gateway punya key pair sendiri; resource server hanya menyimpan public key
Debug endpoint isLocal() untuk menerbitkan tokenSAUTH_BYPASS + fake claims JSON file — tanpa key, tanpa endpoint
Default permission 'wm' yang diam-diamTidak ada default — sid kosong di fake claims → 401
Tidak ada pembedaan M2MStruktural: snm tidak ada = M2M; middleware sauth.m2m
Tidak ada OAuth scopeDukungan penuh claim scope; membuka akses developer eksternal (3p)
Long-lived token tanpa mekanisme pencabutanAPI key JWT dengan jti UUID + tabel blacklist per-service
JwtVerification yang diduplikasi per serviceSatu library sauth-client yang dipasang di mana-mana
Long-lived token tanpa konsep formalbpm:sauth:apikey issue — API key JWT yang proper dengan audit trail

Arsitektur

┌──────────────────────────────────────────────────────────────┐
│  GWA (sa-gw-admin)              GWC (sa-gw-customer-2)       │
│                                                              │
│  sauth-server  ← menerbitkan JWT  sauth-server  ← menerbitkan│
│  sauth-client  ← memvalidasi      sauth-client  ← memvalidasi│
│  sauth-frontend (npm)             sauth-frontend (npm)       │
│                                                              │
│  OAuth IdP pusat — satu-satunya   Gateway pelanggan saja     │
│  otoritas M2M token dan           Tidak ada M2M, tidak ada   │
│  token exchange (RFC 8693)        token exchange             │
└────────────────────┬────────────────────┬────────────────────┘
                     │ Bearer JWT          │ Bearer JWT
                     ▼                     ▼
          ┌──────────────────────────────────────┐
          │  Resource Servers (SRF, PNR, PEL ...) │
          │  sauth-client saja                    │
          │  Tanpa Passport; tanpa DB untuk token │
          │  standar (DB hanya untuk JTI blacklist│
          │  Validasi via firebase/php-jwt        │
          └──────────────────────────────────────┘

GWA adalah satu-satunya otoritas M2M. Semua token client_credentials dan permintaan token exchange diarahkan ke GWA. GWC hanya menerbitkan user token.

Resource server tidak pernah menghubungi gateway saat validasi. Mereka menyimpan public key gateway di config dan memverifikasi signature JWT secara lokal — tanpa network hop. DB hanya diakses ketika token membawa claim jti (API key JWT), dan hanya jika service tersebut sudah menjalankan migrasi JTI blacklist.


Tiga Library

LibraryPackagePeran
sauth-serverbpmlib/sauth-serverPenerbitan token — membungkus Passport, menyuntikkan ecosystem claims
sauth-clientbpmlib/sauth-clientValidasi token — stateless JWT guard dan middleware
sauth-frontend@bpmlib/sauth-frontendUtilitas permission frontend + authenticated HTTP client

Siapa Memasang Apa

Aplikasisauth-serversauth-clientsauth-frontend
GWA✓ (via dependensi sauth-server)
GWC✓ (via dependensi sauth-server)
Resource server (SRF, PNR, …)

sauth-frontend menggantikan utils-sarequest

Jangan pasang @bpmlib/sauth-frontend dan @bpmlib/utils-sarequest sekaligus dalam satu aplikasi.


Kontrak Token Claim

Nama-nama claim ini adalah kontrak bersama di ketiga library. Mengganti nama claim mana pun mengharuskan pembaruan ketiganya dalam satu rilis yang sama.

ClaimTipeAda di
issstringSemua token — kode singkat (gwa/gwc) untuk token 1p; APP_URL untuk token 3p dan API key JWT
audstringSemua token — kode service tujuan (srf, pnr, pel, dll.)
iatintSemua token
expintSemua token kecuali API key JWT
sidstringSemua token — user ID / client ID / nama aplikasi
snmstringHanya user token — tidak ada di M2M token
fetstring|arrayHanya user token — "wm" = webmaster bypass; array permission atau kode role. Tidak pernah ada di M2M token
scopestringToken 3p user + 3p M2M + API key JWT — dipisah spasi, diawali kode service
jtistringHanya API key JWT — UUID yang dipakai sebagai kunci blacklist
actobject|nullHanya token exchange — {sub: calling_client_id}
fpstringSemua token (v0.3+) — '1p' atau '3p'

Membaca Jenis Token

fpsnmscopejtiJenis tokenGunakan middleware
'1p'adatidak adatidak ada1p user tokensauth.gate
'3p'adaadatidak ada3p user tokensauth.gate + sauth.scope
'1p'tidak adatidak adatidak ada1p M2Msauth.gate (tanpa params) atau sauth.m2m
'3p'tidak adaadatidak ada3p M2M (OAuth)sauth.scope
'3p'tidak adaadaadaAPI key JWTsauth.scope
apa punadaapa puntidak adaDelegated (token exchange)sauth.gate

Untuk token yang diterbitkan sebelum v0.3 (tanpa fp), gunakan inferensi dari snm/scope/jti.


Token Flows

Metafora Ticketbooth

Nama ticketbooth terinspirasi dari cara kerja taman hiburan — dan analogi ini sangat pas untuk menjelaskan dua flow utama yang melibatkan pengguna.

Gateway adalah pintu masuk taman hiburan. Anda melakukan autentikasi sekali di sana (halaman login). Bukti masuk adalah gelang tanganmu — dalam istilah sauth, HttpOnly session cookie yang dikelola oleh server.

Ticketbooth (POST /api/sauth/token) adalah loket di dalam taman. Ketika ingin menaiki wahana tertentu (memanggil SRF, PNR, dll.), anda berjalan ke loket, menunjukkan gelang tangan, dan bilang "satu tiket untuk SRF." Loket mencetak tiket wahana berumur pendek (JWT dengan aud=srf). Tiket itu anda tunjukkan di pintu masuk wahana — resource server — yang memeriksanya secara lokal tanpa menghubungi pintu masuk utama.

Ketika tiket kedaluwarsa, anda kembali ke ticketbooth — bukan ke pintu masuk. Gelang tanganmu (session) masih berlaku. Anda mendapat tiket baru dalam satu perjalanan singkat. Tidak perlu login ulang.

Inilah session mode. Gelang tangan adalah credential jangka panjang; tiket wahana adalah JWT berumur pendek.


PKCE mode berbeda — bayangkan pemegang annual pass.

Pemegang annual pass tidak perlu antre di pintu masuk setiap hari. Mereka punya kartu keanggotaan (access token) yang langsung mengizinkan masuk. Kartu itu punya tanggal kedaluwarsa.

Ketika kartu kedaluwarsa, pemegang menggunakan kupon perpanjangan (refresh token) di loket keanggotaan (POST /oauth/token { grant_type: refresh_token }) untuk mendapat kartu baru — tanpa harus kembali ke pintu masuk atau login ulang.

Jika kartu dan kuponnya hilang keduanya, pemegang harus mendaftar ulang dari awal di pintu masuk (ulangi PKCE). Inilah yang ditangani oleh onAuthFailure().

Inilah PKCE mode. Refresh token ada di sini justru karena tidak ada gelang tangan — tidak ada server session untuk diandalkan. Session mode sengaja tidak memakai refresh token karena gelang tangan sudah mengisi peran itu, dan refresh token yang bisa dibaca JavaScript akan menambah permukaan serangan tanpa manfaat apa pun.


M2M adalah kartu staf. Pekerja tidak perlu antre di ticketbooth sama sekali. Mereka punya credential staf permanen sendiri (client_id + client_secret) dan menggunakan pintu belakang (/oauth/token { grant_type: client_credentials }). Kartu mereka tidak ada namanya (snm tidak ada), tidak ada hak khusus personal (fet tidak ada) — kepercayaan bersifat struktural. Jika kartu yang di-cache mendapat 401, mereka otomatis mengambil yang baru.

API key JWT adalah vendor season pass. Operator stan makanan (partner eksternal) mendapat lencana laminasi yang diterbitkan langsung oleh manajemen taman (bpm:sauth:apikey issue). Tidak ada tanggal kedaluwarsa — berlaku selamanya. Tapi ada nomor seri (jti). Jika lencana hilang atau dicuri, manajemen memblokir nomor seri itu di setiap wahana yang terpengaruh (bpm:sauth:jti block {jti}) tanpa menarik lencana orang lain.


Session-backed frontend (GWA / GWC Inertia)

Pengguna autentikasi via Laravel Breeze session. Ketika frontend perlu memanggil resource server:

1. Pengguna login → HttpOnly session cookie diset oleh gateway
2. Frontend perlu memanggil SRF:
     POST /api/sauth/token { target: 'srf' }   ← auth:sanctum (GWA: baris 130 api.php)
     ← { access: <jwt>, expires_in: 900 }
3. sauth-frontend menyimpan JWT di cookie tkac_srf (TTL = 80% dari expires_in)
4. Frontend memanggil SRF langsung:
     GET https://srf.internal/api/data
     Authorization: Bearer <jwt>
5. SRF memvalidasi: signature + exp + aud — tanpa DB, tanpa panggilan Passport

Tidak ada refresh token di session mode

Gelang tangan (HttpOnly session cookie) adalah credential jangka panjang. Menerbitkan ulang dari ticketbooth itu murah dan aman dari XSS. Refresh token yang bisa dibaca JavaScript hanya menambah permukaan serangan tanpa manfaat — session sudah menjalankan peran itu.

PKCE standalone SPA (1st-party, tanpa server session)

Digunakan ketika standalone SPA tidak punya server session untuk diandalkan — model pemegang annual pass.

1. SPA memulai PKCE: GET /oauth/authorize?client_id=...&code_challenge=...
2. Pengguna login di halaman login gateway
3. Gateway redirect balik dengan auth code
4. SPA menukar code:
     POST /oauth/token { grant_type: authorization_code, code_verifier, ... }
     ← { access_token, refresh_token, expires_in }
5. sauth-frontend (pkce mode) menyimpan kedua token di cookie:
     tkac_{service} — TTL = 80% dari expires_in
     tkrf_{service} — TTL = refreshTokenTtl (default 7 hari)
6. SPA memanggil resource server dengan Bearer token
7. Saat kedaluwarsa: POST /oauth/token { grant_type: refresh_token, refresh_token: <cookie> }
8. Jika refresh gagal → onAuthFailure() → redirect ke /oauth/authorize (ulangi PKCE)

Refresh token ada di sini — tapi tidak di session mode

PKCE mode menyimpan refresh token karena tidak ada server session untuk diandalkan. Refresh token adalah kupon perpanjangan. Kehilangan keduanya → harus autentikasi ulang dari awal.

M2M — PHP service worker

Worker menggunakan trait FetchesClientToken:
  POST /oauth/token { grant_type: client_credentials, client_id, client_secret }
  ← { access_token, expires_in }
  Token di-cache di Laravel Cache pada 80% dari expires_in.
  Jika dapat 401 dari downstream: forgetClientToken() → ambil ulang sekali.

M2M token hanya membawa sid (berupa client_id) — tanpa snm, tanpa fet, tanpa scope. Kepercayaan bersifat struktural: signature yang valid + aud yang benar sudah cukup.

Token Exchange — antar-service dengan konteks pengguna

Ketika SRF perlu memanggil PNR atas nama pengguna manusia:

1. SRF menerima permintaan pengguna (token: aud=srf, sid=user123)
2. SRF meminta GWA untuk menukar:
     POST /oauth/token {
       grant_type: urn:ietf:params:oauth:grant-type:token-exchange,
       subject_token: <token pengguna>,
       resource: 'pnr'
     }
     dengan credential client SRF sendiri
3. GWA menerbitkan delegated token: sid=user123, aud=pnr, act={sub: srf_client_id}
4. SRF memanggil PNR dengan delegated token tersebut
5. PNR melihat pengguna asli (sid) dan tahu SRF adalah perantaranya (act.sub)

Token exchange hanya ada di GWA. GWC tidak memiliki endpoint ini.

Developer Eksternal 3p (PKCE + scope)

Flow ini tidak ada di BpmJWT — membutuhkan dukungan scope.

1. Aplikasi 3p memulai PKCE di GWA dengan scope yang diminta
2. Pengguna login dan melihat layar persetujuan (consent screen)
3. Aplikasi 3p menukar code dengan token
   Token membawa: sid (user ID), snm, fet (permission pengguna), scope (grant client)
4. Aplikasi 3p memanggil resource server
5. Resource server memeriksa KEDUANYA:
     sauth.gate:srf.read  → pengguna punya srf.read di fet ✓
     sauth.scope:srf.read → client mendapat grant srf.read di scope ✓

Keduanya harus lolos — logika AND. Token di mana pengguna punya srf.read tapi client tidak pernah mendapat grant-nya (atau sebaliknya) akan ditolak. Inilah jaminan keamanan yang membuat akses pihak ketiga bisa dilakukan tanpa membocorkan permission internal.

API Key JWT — akses mesin partner jangka panjang

Ini adalah pengganti formal untuk penerbitan long-lived token secara ad-hoc. Sebelumnya tidak ada mekanisme terstruktur — developer menerbitkan token dengan exp tinggi dan berharap yang terbaik.

1. Admin GWA menjalankan: php artisan bpm:sauth:apikey issue
   → diminta: nama aplikasi, aud (service tujuan), scope
   → menerbitkan JWT bertanda tangan: iss=APP_URL, aud=srf, jti=uuid, sid=nama-app,
                                      scope=srf.read, fp='3p', TANPA exp
   → baris audit terenkripsi ditulis ke sauth_jti_tokens di GWA

2. Partner menggunakan JWT sebagai Bearer token — selamanya, atau sampai dicabut

3. Resource server memvalidasi: signature + issuer + aud + scope
   + memeriksa jti di tabel lokal sauth_blacklisted_jti_tokens (cache 5 menit)

4. Jika terjadi kebocoran:
   GWA: php artisan bpm:sauth:apikey revoke {jti}
   Setiap resource server: php artisan bpm:sauth:jti block {jti}

Satu key per service

Setiap API key JWT menarget tepat satu aud. Partner yang butuh akses ke SRF dan PNR mendapat dua key terpisah.

Pencabutan bersifat per resource server — desinkronisasi yang disengaja

Service X bisa memblokir key tanpa mempengaruhi service Y. Library memicu event JtiTokenRevoked ketika key dicabut di GWA. Resource server merespons lewat CLI atau dengan mendaftarkan HTTP listener otomatis.


Dev Mocking

Cara lama (BpmJWT)

  • Salin private + public key ke storage/ resource server
  • Daftarkan debug endpoint isLocal() yang menerbitkan token
  • Default permission ke 'wm' jika tidak diisi
  • Daftar issuer yang dipercaya di local mode mencakup semua kode service — service mana pun bisa menerbitkan token yang lolos di mana pun

Cara baru (sauth)

Set SAUTH_BYPASS di .env. Tanpa key, tanpa endpoint, tanpa jebakan default.

ini
# .env (hanya lokal — diabaikan di production)
SAUTH_BYPASS=true
SAUTH_FAKE_CLAIMS_FILE=sauth-fake-claims.json

Publikasikan template fake claims:

bash
php artisan bpm:sauth:fake publish

Edit storage/sauth-fake-claims.json sesuai bentuk pengguna yang dibutuhkan:

json
{
    "1p_user": {
        "sid": "user-123",
        "snm": "Dev User",
        "fet": ["user.read", "report.view"],
        "fp": "1p"
    },
    "1p_m2m": {
        "sid": "srf-worker",
        "fp": "1p"
    },
    "3p_m2m": {
        "sid": "partner-app",
        "scope": ["srf.read"],
        "fp": "3p"
    }
}

SAUTH_BYPASS adalah multi-value enum — gunakan level bypass minimum yang diperlukan:

NilaiSignatureCek fetCek scope
false/nonediberlakukandiberlakukandiberlakukan
sigdilewatidiberlakukandiberlakukan
fetdilewatidilewatidiberlakukan
scopedilewatidiberlakukandilewati
permdilewatidilewatidilewati
true/totaldilewatidilewatidilewati

Tidak pernah bypass di production

Konfigurasi bypass diabaikan sepenuhnya ketika app()->isLocal() bernilai false, tidak peduli apa nilai SAUTH_BYPASS.


Referensi Middleware

MiddlewareTujuanJenis token yang diterima
sauth.gateValidasi JWT + cek permission fet1p user, 3p user, 1p M2M (tanpa params saja)
sauth.wmMewajibkan fet === 'wm' persis1p/3p user dengan level webmaster
sauth.dualSession-first, JWT sebagai fallbackSama dengan sauth.gate
sauth.scopeMemeriksa claim scope3p user, 3p M2M, API key JWT
sauth.m2mMenerima M2M apa pun, menolak user token1p M2M, 3p M2M, API key JWT

Tabel keputusan lengkap

Middleware1p user token3p user token1p M2M3p M2M / API key JWT
sauth.gateLolos jika fet cocok (atau wm bypass)Lolos jika fet cocok (atau wm bypass)Lolos hanya jika tanpa paramsDitolak (tidak ada fet)
sauth.wmLolos jika fet === 'wm' persisLolos jika fet === 'wm' persisDitolakDitolak
sauth.dualLolos (session atau JWT)Lolos (session atau JWT)Lolos hanya jika tanpa paramsDitolak
sauth.scopeDitolak (tidak ada scope)Lolos jika scope cocok (OR)Ditolak (tidak ada scope)Lolos jika scope cocok (OR)
sauth.m2mDitolak (snm ada)Ditolak (snm ada)Selalu lolosLolos jika semua scope ada (AND)

Endpoint 3p user butuh kedua middleware

php
// Logika AND — keduanya harus lolos
Route::middleware(['sauth.gate:user.read', 'sauth.scope:srf.read'])->group(function () {
    // pengguna harus punya user.read di fet DAN client harus punya grant srf.read di scope
});

Arah wildcard permission

Pattern dibangun dari entry fet pengguna dan diuji terhadap permission yang diperlukan — bukan sebaliknya.

pengguna punya fet: ['user.*']    diperlukan: 'user.read'
  → pattern /^user\..*$/ → cocok ✓   (permission luas lolos syarat yang sempit)

pengguna punya fet: ['user.read'] diperlukan: 'user.*'
  → pattern /^user\.read$/ → tidak cocok ✗ (permission sempit tidak bisa lolos syarat yang luas)

Aturan ini identik di sauth-client PHP (hasAnyPermission()) dan sauth-frontend TypeScript (permissionCheck()).


Keputusan Desain Utama

Token adalah JWT bertanda tangan sungguhan, bukan opaque Passport token. lcobucci/jwt membangun dan menandatangani token di dalam AccessToken::toString(). Resource server memverifikasi signature secara kriptografis — tanpa DB lookup saat validasi.

Session mode tidak punya refresh token. Gateway session adalah credential jangka panjang. Menerbitkan ulang dari ticketbooth itu murah. Refresh token yang bisa dibaca JavaScript menambah permukaan serangan tanpa manfaat. PKCE mode menggunakan refresh token karena tidak ada server session untuk diandalkan.

Aturan 80% TTL berlaku di mana-mana. FetchesClientToken (PHP M2M) dan RequestInstance (browser) keduanya meng-cache access token pada 80% dari expires_in. Token diperbarui sebelum kedaluwarsa.

Claim fp membuat pihak (party) menjadi eksplisit. Semua token yang diterbitkan sauth-server v0.3+ membawa fp: '1p' atau fp: '3p'. Untuk token lama (tanpa fp), kedua library jatuh kembali ke inferensi dari snm/scope. Gunakan isM2M() (bukan isFirstParty()) untuk membedakan pengguna manusia dari machine client — isFirstParty() mengembalikan true untuk 1p user token sejak v0.3.

ServiceCodes hanya berisi invariant ekosistem. Kode issuer (gwa, gwc) dan kode role (wm, psn, cpy, mit) tempatnya di library. Kode audience service (srf, pnr, pel) tidak — setiap service tahu kodenya sendiri dari config. Hardcoding di library memaksa version bump setiap kali service baru ditambahkan.

Urutan deployment penting untuk v0.3 (perubahan iss 3p). Token 1p tetap menggunakan kode singkat sebagai iss. Token 3p menggunakan APP_URL sebagai iss mulai v0.3. sauth-client v0.3 harus sudah terpasang di semua resource server sebelum sauth-server v0.3 di-deploy ke gateway. Urutan terbalik menyebabkan UnknownIssuerException pada token 3p.


Versioning

Kosakata claim adalah kontrak bersama. Mengganti nama claim di sauth-server akan langsung merusak validasi sauth-client dan TypeScript types di sauth-frontend secara bersamaan. Ketiga library harus version-bump bersama ketika nama claim, nama exception class, atau kode issuer berubah.

LibraryRegistry
bpmlib/sauth-serverPrivate Satis (Composer)
bpmlib/sauth-clientPrivate Satis (Composer)
@bpmlib/sauth-frontendPrivate npm registry

Untuk pengembangan lokal: gunakan entry "type": "path" di composer.json; gunakan file: path atau npm link untuk npm package.