Sauth Server — Changelog
v0.3.7 (2026-06-13)
TL;DR
Perubahan:
- 🔧
client_credentialsgrant — 1p M2M token menghormati parameterresourceuntuk overrideaud(Low)
Impact: 🔵 Low: 1
Backward Compatible: ✅ Ya — caller yang tidak mengirim resource tetap mendapat aud = default_audience
🔵 Low Impact
client_credentials grant — parameter resource untuk audience targeting
AccessTokenRepository kini membaca parameter resource dari request saat menerbitkan 1p M2M token. Jika resource hadir dan non-empty, nilai tersebut digunakan sebagai aud di JWT — menggantikan sauth-server.default_audience.
Sebelumnya, semua 1p M2M token mendapat aud = default_audience tanpa memperhatikan audience yang dimaksud caller. Token yang dimaksudkan untuk pnr tetap berisi aud = internal-services, sehingga sauth.gate di PNR menolaknya jika aud diperiksa secara ketat.
Caller kini dapat mengirim parameter resource bersama request client_credentials:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=<client-id>
&client_secret=<client-secret>
&resource=pnrPenamaan resource konsisten dengan konvensi RFC 8693 yang sudah dipakai TokenExchangeGrant.
Poin utama:
- Hanya berlaku untuk 1p M2M token (
is_first_party = true,userId = null) — 3p M2M tidak terpengaruh - Caller yang tidak mengirim
resourcemendapataud = default_audience(identik dengan sebelumnya) - Resource server tetap menjadi penjaga akhir — validasi
auddi sisi penerima tidak berubah
Upgrade
composer update bpmlib/sauth-serverBreaking changes: Tidak ada.
v0.3.6 (2026-06-08)
TL;DR
Perubahan:
- 🐛 JWKS —
kidtidak ada di response/.well-known/jwks.json(Low)
Impact: 🔵 Low: 1
Backward Compatible: ✅ Ya — penambahan field kid di JWKS tidak breaking. OIDC clients yang sudah berjalan tidak terpengaruh; clients yang membutuhkan kid untuk key lookup kini berfungsi.
🐛 Bug Fix
JWKS — kid tidak ada di response /.well-known/jwks.json
JwksController mengembalikan key tanpa field kid, menyebabkan OIDC clients yang menggunakan kid untuk mencocokkan signing key gagal validasi. kid sekarang dihitung sebagai RFC 7638 JWK thumbprint (SHA-256 dari required members dalam urutan leksikografis, base64url-encoded) — stabil selama key tidak dirotasi.
v0.3.5 (2026-06-08)
TL;DR
Perubahan:
- 🐛 OIDC
nonce— tidak diteruskan dari authorization request keid_token(Low)
Impact: 🔵 Low: 1
Backward Compatible: ✅ Ya — tidak ada perubahan schema, interface, atau konfigurasi. Consuming app yang menggunakan manual-approve path harus menambahkan satu baris session storage.
🐛 Bug Fix
OIDC nonce — tidak diteruskan dari authorization request ke id_token
Infrastruktur setNonce()/getNonce() sudah ada sejak v0.3.4 tapi nonce dari authorization request tidak pernah di-set — client yang memvalidasi nonce menerima MissingClaimError: Missing 'nonce' claim.
OidcAuthCodeRepository (baru) menangkap nonce saat auth code dibuat dan menyimpannya ke cache (TTL 10 menit, key oidc_nonce_{userId}_{clientId}). OidcBearerTokenResponse membaca cache sebelum IdTokenIssuer berjalan — nonce otomatis hadir di id_token.
Auto-approve path bekerja tanpa perubahan apapun. Manual-approve path membutuhkan consuming app menyimpan nonce ke session['oidc_nonce'] saat merender consent form.
Upgrade
composer update bpmlib/sauth-serverBreaking changes: Tidak ada.
Action required (manual-approve only): tambahkan session(['oidc_nonce' => request()->query('nonce')]) di authorization view response class consuming app.
v0.3.4 (2026-06-05)
TL;DR
Perubahan:
- ✨ OIDC support opt-in — discovery, JWKS,
/userinfo,id_tokenuntuk client 3p yang ditandaiis_oidc=true(High) - 🔧
bpm:sauth:client --oidc— flag baru + validasi kombinasi flag + prompt interaktif untuk--user(Low) - 🔧
ClientRegistrar—create()danupdate()mendapat parameter$isOidc(Low) - 🔧
AccessToken— gettergetSid(),getSnm(); infrastruktur noncesetNonce()/getNonce()(Low) - 🐛
UserInfoController— Eloquent user lookup gagal di Bearer token context;OidcClaimsProviderInterfacetidak pernah dipanggil meskipun sudah di-bind (Low)
Impact: 🟡 High: 1 | 🔵 Low: 4
Backward Compatible: ✅ Ya — semua fitur OIDC opt-in via SAUTH_OIDC_ENABLED=true; default false. Tidak ada perubahan token, interface, atau migrasi wajib.
🟡 High Impact
OIDC Support
Tiga endpoint terdaftar otomatis oleh SauthServerServiceProvider saat SAUTH_OIDC_ENABLED=true:
GET /.well-known/openid-configuration— discovery documentGET /.well-known/jwks.json— public key dalam format JWK Set (native OpenSSL, tanpa dependency baru)GET /userinfo— mengembalikansub+name; claim tambahan viaOidcClaimsProviderInterfaceopsional
Client yang ditandai is_oidc = true mendapat id_token di samping access_token saat authorization_code flow + openid scope. Access token tidak berubah — sid/snm/fet/fp tetap sama.
Kolom is_oidc di oauth_clients dipublish oleh bpm:sauth:migration (idempotent). Boot-time schema guard aktif saat SAUTH_OIDC_ENABLED=true — melempar RuntimeException dengan pesan actionable jika kolom belum ada.
🔵 Low Impact
bpm:sauth:client --oidc
Flag baru. Kombinasi --oidc --first dan --oidc tanpa --user ditolak dengan hard fail. Saat --user digunakan tanpa --oidc, command meminta konfirmasi interaktif. Jika is_oidc=true sementara SAUTH_OIDC_ENABLED=false, client tetap dibuat dengan peringatan.
ClientRegistrar — $isOidc
create() mendapat bool $isOidc = false; update() mendapat ?bool $isOidc = null (null = tidak disentuh).
AccessToken — getter + nonce
getSid() dan getSnm() ditambahkan sebagai getter publik. setNonce()/getNonce() menyediakan infrastruktur nonce untuk id_token; full passthrough dari auth request dijadwalkan untuk versi berikutnya.
🐛 Bug Fix
UserInfoController — Eloquent user lookup di Bearer token context
OidcClaimsProviderInterface::getOidcClaims() membutuhkan instance Eloquent user, bukan MicroserviceUser. Controller sebelumnya mengambilnya via auth->guard('web')->user() — yang selalu null pada request Bearer token karena tidak ada web session yang aktif. Diperbaiki menggunakan auth->createUserProvider(...)->retrieveById($user->id), konsisten dengan pola yang sama di AccessTokenRepository.
Dampak: tanpa fix ini, /userinfo hanya mengembalikan sub + name meskipun OidcClaimsProviderInterface sudah di-bind — extra claims (email, dll.) tidak pernah di-merge.
Upgrade
composer update bpmlib/sauth-serverBreaking changes: Tidak ada.
Untuk mengaktifkan OIDC:
- Set
SAUTH_OIDC_ENABLED=truedi.env - Jalankan
php artisan bpm:sauth:migration - Buat OIDC client via
php artisan bpm:sauth:client --user --oidc - Opsional — implement
OidcClaimsProviderInterfaceuntuk extra claims di/userinfo
v0.3.3 (2026-06-01)
TL;DR
Perubahan:
- 🔧
SAUTH_APIKEY_SIGN_ALGO+SAUTH_APIKEY_PRIVATE_KEY_FILE— key dan algoritma signing terpisah untuk API key JWT (Low)
Impact: 🔵 Low: 1
Backward Compatible: ✅ Ya — tidak ada perubahan interface, migrasi, atau perilaku default; kedua env var opsional
🔵 Low Impact
SAUTH_APIKEY_SIGN_ALGO + SAUTH_APIKEY_PRIVATE_KEY_FILE — signing terpisah untuk API key JWT
Sebelumnya ApiKeyIssuer menggunakan key dan algoritma yang sama dengan token Passport reguler (SAUTH_SIGN_ALGO / SAUTH_PRIVATE_KEY_FILE). Dua env var baru memungkinkan API key JWT ditandatangani dengan key pair dan algoritma yang independen — berguna ketika partner perlu memverifikasi token langsung (tanpa sauth-client) atau memerlukan rotasi key yang independen dari key utama gateway.
Fallback chain algo:
| Konteks | Urutan resolusi |
|---|---|
| API key JWT signing | SAUTH_APIKEY_SIGN_ALGO → SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 |
| Token Passport reguler | SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 (tidak berubah) |
Fallback chain key file:
| Konteks | Urutan resolusi |
|---|---|
| API key JWT key | SAUTH_APIKEY_PRIVATE_KEY_FILE → SAUTH_PRIVATE_KEY_FILE → oauth-private.key |
| Token Passport reguler | SAUTH_PRIVATE_KEY_FILE → oauth-private.key (tidak berubah) |
Ketika kedua env var tidak di-set, ApiKeyIssuer menggunakan key dan algo yang sama seperti sebelumnya — tidak ada perubahan perilaku.
# API key JWT menggunakan EC key terpisah; token Passport reguler tetap RSA
SAUTH_APIKEY_SIGN_ALGO=ES256
SAUTH_APIKEY_PRIVATE_KEY_FILE=keys/apikey-private.key# Generate key pair terpisah untuk API key JWT
php artisan bpm:sauth:keygen ES256 --path=keys --file-prefix=apikeyPoin utama:
- Kedua env var opsional — tanpa keduanya, perilaku identik dengan v0.3.2
ApiKeyIssuerkini menggunakanKeyLoader(bukanInMemory::file()langsung) — deteksi mismatch key/algo aktif untuk API key JWT juga- Public key yang dibagikan ke partner: public key dari
SAUTH_APIKEY_PRIVATE_KEY_FILE, bukan public key utama gateway - Dua key baru di
config/sauth-server.php:apikey_sign_algorithmdanapikey_private_key_file
Upgrade
composer update bpmlib/sauth-serverBreaking changes: Tidak ada.
Opsional: Set SAUTH_APIKEY_SIGN_ALGO dan SAUTH_APIKEY_PRIVATE_KEY_FILE di .env gateway jika ingin menggunakan key pair terpisah untuk API key JWT.
v0.3.2 (2026-05-26)
TL;DR
Perubahan:
- 🔧
SAUTH_SIGN_ALGO— override algoritma signing sauth-server yang independen dariSAUTH_ALGO(Low)
Impact: 🔵 Low: 1
Backward Compatible: ✅ Ya — tidak ada perubahan interface, migrasi, atau perilaku default
🔵 Low Impact
SAUTH_SIGN_ALGO — override algoritma signing per gateway
SAUTH_ALGO adalah env var bersama yang dibaca oleh sauth-server dan sauth-client. Saat gateway ingin beralih algoritma signing (misalnya ke ES256), mengubah SAUTH_ALGO juga berdampak ke resource server yang belum di-update. SAUTH_SIGN_ALGO memberikan override khusus sauth-server: ketika di-set, ia mengalahkan SAUTH_ALGO untuk semua operasi signing dan key generation di gateway. Resource server tidak membaca SAUTH_SIGN_ALGO — mereka tetap menggunakan SAUTH_ALGO sampai siap untuk diperbarui.
Resolusi algo secara berurutan:
| Konteks | Urutan resolusi |
|---|---|
Token signing (SignerResolver) | SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 |
bpm:sauth:keygen (tanpa argumen) | SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 |
bpm:sauth:init (tanpa --algo) | SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 |
Saat --algo diberikan secara eksplisit ke bpm:sauth:keygen atau bpm:sauth:init, argumen tersebut selalu menang.
bpm:sauth:keygen — argumen {algo} kini opsional. Sebelumnya argumen wajib diisi. Sekarang, jika tidak diisi, algo di-resolve dari config (via chain di atas):
php artisan bpm:sauth:keygen # algo dari SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256
php artisan bpm:sauth:keygen ES256 # argumen eksplisit tetap menangPola migrasi algoritma gateway:
# .env di gateway — beralih ke ES256 tanpa mengubah SAUTH_ALGO yang dibaca resource server
SAUTH_SIGN_ALGO=ES256
SAUTH_ALGO=RS256php artisan bpm:sauth:keygen --force # generate ulang key pair dengan ES256Setelah resource server siap diperbarui, pindahkan ke SAUTH_ALGO=ES256 dan hapus SAUTH_SIGN_ALGO.
Poin utama:
SAUTH_SIGN_ALGOhanya dibaca oleh sauth-server — sauth-client tidak membacanya- Nilai default
null→ fallback keSAUTH_ALGO→ behavior identik dengan v0.3.1 tanpa konfigurasi tambahan - Key baru
sign_algorithmdi-publish viaconfig/sauth-server.php
Upgrade
composer update bpmlib/sauth-serverBreaking changes: Tidak ada.
Opsional: Set SAUTH_SIGN_ALGO di .env gateway jika ingin mengubah algoritma signing secara independen dari SAUTH_ALGO.
v0.3.1 (2026-05-25)
TL;DR
Perubahan:
- 💥
service_codedioauth_clients+ validasi targetServiceTokenIssuer— ticketbooth menolak target tidak terdaftar (Breaking) - ✨
bpm:sauth:keygen+KeyLoader— generate key EC dan EdDSA; deteksi mismatch key/algo (High) - 🔧
bpm:sauth:init— flag--algo,--path,--file-prefix; stop memanggilpassport:keys(Medium) - ✨
bpm:sauth:migration— command standalone untuk publish migrasi sauth tanpa full init (Medium) - 🔧
ClientRegistrar—create()danupdate()mendapat parameterserviceCode(Medium) - 🔧
bpm:sauth:client --first— prompt service code saat membuat first-party client (Low)
Impact: 🔴 Breaking: 1 | 🟡 High: 1 | 🟢 Medium: 3 | 🔵 Low: 1
Backward Compatible: ❌ Breaking — ServiceTokenIssuer menolak semua target yang belum punya service_code terdaftar di oauth_clients. Populate service_code di semua 1p client sebelum deploy. Lihat urutan deploy di bawah.
💥 Breaking Changes
service_code di oauth_clients + validasi target ServiceTokenIssuer
Kolom baru service_code (nullable, unique) ditambahkan ke oauth_clients. 1p resource server client yang menjadi tujuan ticketbooth harus didaftarkan dengan service_code sebelum v0.3.1 di-deploy.
ServiceTokenIssuer::issue() kini memvalidasi $targetService dengan query ke oauth_clients:
WHERE is_first_party = true
AND service_code IS NOT NULL
AND (service_code = $target OR id = $target)
AND deleted_at IS NULL- Ditemukan →
service_codedigunakan sebagaiauddi JWT (bukan client ID) - Tidak ditemukan →
\InvalidArgumentException— gateway controller harus catch dan return 422
Mengapa ini penting: sebelumnya ServiceTokenIssuer menerima string apapun sebagai aud tanpa validasi. Frontend bisa request { target: 'anything' } dan mendapat signed JWT — satu-satunya guard adalah bahwa resource server menolak aud yang tidak dikenal. Dengan validasi ini, token hanya bisa diterbitkan untuk service yang sudah diregistrasi secara eksplisit oleh admin.
Poin utama:
- Lookup by
idtersedia sebagai fallback — memudahkan transisi sebelum semua client punyaservice_code auddi JWT selaluservice_code, tidak pernah client IDClientRegistrar::create()danupdate()mendapat parameter?string $serviceCode = nullbpm:sauth:client --firstsekarang meminta service code saat membuat first-party client- Dua migration stub baru:
add_is_first_party_to_oauth_clients.stubdiperbarui (fresh install — tambah kedua kolom sekaligus);add_service_code_to_oauth_clients.stubbaru (upgrade path — tambahservice_codesaja)
Pola controller yang perlu diperbarui (di gateway):
public function issue(Request $request, ServiceTokenIssuer $issuer): JsonResponse
{
$request->validate(['target' => 'required|string']);
try {
return response()->json($issuer->issue($request->user(), $request->input('target')));
} catch (\InvalidArgumentException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
}🟡 High Impact
bpm:sauth:keygen + KeyLoader — support EC dan EdDSA
Command baru bpm:sauth:keygen men-generate key pair untuk algoritma yang dipilih dan menyimpannya ke storage/:
php artisan bpm:sauth:keygen RS256 # storage/oauth-private.key + oauth-public.key
php artisan bpm:sauth:keygen ES256 --path=keys # storage/keys/oauth-private.key + ...
php artisan bpm:sauth:keygen EdDSA --file-prefix=gwa # storage/gwa-private.key + gwa-public.key
php artisan bpm:sauth:keygen RS256 --force # overwrite (konfirmasi prompt)| Family | Method | Format output |
|---|---|---|
| RS256/384/512 | openssl_pkey_new(), 4096-bit RSA | PEM (-----BEGIN RSA PRIVATE KEY-----) |
| ES256 | openssl_pkey_new(), curve prime256v1 | PEM (-----BEGIN EC PRIVATE KEY-----) |
| ES384 | openssl_pkey_new(), curve secp384r1 | PEM |
| ES512 | openssl_pkey_new(), curve secp521r1 | PEM |
| EdDSA | sodium_crypto_sign_keypair() | Base64 single-line |
SignerResolver mendapat arm baru untuk EDDSA → new Eddsa().
KeyLoader — helper baru src/Support/KeyLoader.php. Dipakai oleh ServiceTokenIssuer untuk menggantikan hardcoded InMemory::file():
KeyLoader::privateKey(string $path, string $algo): InMemory
KeyLoader::publicKey(string $path, string $algo): InMemoryDeteksi format otomatis (PEM vs base64) + validasi terhadap $algo. Mismatch melempar RuntimeException dengan pesan actionable:
Key at oauth-private.key is PEM but SAUTH_ALGO=EdDSA expects base64-encoded Ed25519.
Run: php artisan bpm:sauth:keygen EdDSAPoin utama:
--pathnon-root mencetak warning.gitignore— Laravel hanya mengcover/storage/*.keyby default--forcetanpa konfirmasiyestidak overwrite (exit 0, bukan failure)- EdDSA keys berbentuk satu baris base64 — format berbeda dari PEM;
KeyLoaderyang menangani routing keInMemory::base64Encoded()vsInMemory::file()
🟢 Medium Impact
bpm:sauth:init — flag --algo, --path, --file-prefix
bpm:sauth:init tidak lagi memanggil passport:keys. Key generation didelegasikan ke bpm:sauth:keygen via flag baru:
php artisan bpm:sauth:init # algo dari sauth-server.algorithm, path dari private_key_file config
php artisan bpm:sauth:init --algo=ES256 # generate EC keys
php artisan bpm:sauth:init --algo=EdDSA --path=keys --file-prefix=gwaResolusi default:
--algo→sauth-server.algorithmconfig →RS256--path→ derivasi darisauth-server.private_key_fileconfig (dirname)--file-prefix→ derivasi darisauth-server.private_key_fileconfig (basename tanpa-private)
Ini memperbaiki celah yang ada sebelumnya: SAUTH_ALGO=ES256 akan menerbitkan token bertanda tangan EC tapi key generation tetap menghasilkan RSA key — mismatch yang baru ketahuan saat runtime. Sekarang algo dan key generation selalu konsisten.
bpm:sauth:migration — command standalone
Command baru bpm:sauth:migration mempublish dan menjalankan tiga migrasi sauth secara idempotent:
php artisan bpm:sauth:migrationLangkah-langkah (masing-masing dengan cek skip):
is_first_partydioauth_clients— fresh install stub (tambah kedua kolom) jika belum adaservice_codedioauth_clients— upgrade stub jikais_first_partysudah ada tapiservice_codebelum- Tabel
sauth_jti_tokens— JTI migration jika tabel belum ada - Jalankan
migrate --forcesekali jika ada yang baru dipublish
bpm:sauth:init kini mendelegasikan langkah 2–4 ke command ini. Gunakan bpm:sauth:migration langsung pada upgrade rolling (tanpa generate ulang key).
ClientRegistrar — parameter serviceCode
create() dan update() menerima ?string $serviceCode = null:
// create — null tidak menyentuh kolom, '' tidak diset (kosong diabaikan)
$registrar->create('SRF Worker', isFirstParty: true, serviceCode: 'srf');
// update — null = biarkan existing; '' = set null di DB; non-empty = set nilai
$registrar->update($client, 'SRF Worker', serviceCode: 'srf'); // set
$registrar->update($client, 'SRF Worker', serviceCode: ''); // clear ke null
$registrar->update($client, 'SRF Worker'); // tidak disentuh🔵 Low Impact
bpm:sauth:client --first — prompt service code
Saat --first diset, command kini meminta service code secara interaktif. Input kosong akan abort dengan error — service code wajib untuk first-party client agar bisa menjadi target ticketbooth.
Output summary kini menyertakan baris Service Code:
Client ID : 12
Client Secret : xxxx
Service Code : srfUpgrade
composer update bpmlib/sauth-serverUrutan deploy yang wajib diikuti:
- Jalankan migrasi di GWA dan GWC:bash
php artisan bpm:sauth:migration # atau php artisan bpm:sauth:init (jika juga perlu generate key baru) - Populate
service_codedi semua 1p resource server client yang menjadi target ticketbooth:php// Tinker atau migration data \Laravel\Passport\Client::where('is_first_party', true) ->whereNull('service_code') ->each(fn ($c) => $c->update(['service_code' => 'srf'])); // sesuaikan per client - Deploy kode v0.3.1 ke GWA dan GWC
Jangan deploy kode v0.3.1 sebelum langkah 2 selesai — ServiceTokenIssuer akan melempar InvalidArgumentException untuk semua target yang belum punya service_code, menyebabkan semua ticketbooth request return 422.
Tidak ada perubahan wajib untuk:
- Resource server — tidak ada perubahan di sauth-client
- Gateway
ClaimsProvider(GwaClaimsProvider,GwcClaimsProvider) — interface tidak berubah - Worker yang menggunakan
FetchesClientToken— tidak ada perubahan - Token yang sudah diterbitkan sebelum v0.3.1 — tetap valid
v0.3.0 (2026-05-25)
TL;DR
Perubahan:
- ✨ Claim
fp— party marker eksplisit'1p'/'3p'di semua token (High) - 🔧 Claim
issuntuk token 3p —APP_URLsebagai issuer; token 1p tetap menggunakan short code (High) - 🐛
FetchesClientTokenfake token — hapusfet: []yang salah + fixforUnsecuredSigneryang sudah dihapus di lcobucci/jwt v5 (Medium) - 🐛
ServiceTokenIssuer—fptidak di-emit karena build JWT langsung tanpa lewatAccessTokenRepository(Medium)
Impact: 🟡 High: 2 | 🟢 Medium: 2
Backward Compatible: ⚠️ Partial — deploy sauth-client v0.3.0 ke semua resource server lebih dulu sebelum deploy server ini ke gateway. Membalik urutan menyebabkan token 3p dengan iss berformat URL gagal dikenali resource server lama.
🟡 High Impact
Claim fp — party marker eksplisit di semua token
Semua token yang diterbitkan kini menyertakan claim fp berisi '1p' (first-party) atau '3p' (third-party). Sebelumnya, sauth-client harus menyimpulkan party dari ada/tidaknya snm dan scope — pendekatan yang ambigu untuk token edge case.
| Token type | fp |
|---|---|
| 1p user | '1p' |
| 3p user | '3p' |
| 1p M2M | '1p' |
| 3p M2M | '3p' |
| API key JWT | '3p' |
| Token exchange delegated | '1p' (diambil dari subject token; fallback '1p' untuk token lama) |
fp di-set oleh AccessTokenRepository dari kolom is_first_party — tidak ada DB query tambahan. TokenExchangeGrant membaca fp dari subject token dan meneruskannya ke token hasil exchange.
Token lama (tanpa fp): sauth-client v0.3.0 tetap menerima token tanpa fp — fallback ke inferensi snm/scope untuk token yang diterbitkan sebelum v0.3. Tidak ada hard failure selama token masih berlaku dan belum expired.
Poin utama:
AccessTokenmendapat method barusetFp(string $fp)— dipanggil repository sebelumtoString()MicroserviceTokenClaimsProviderInterfacetidak berubah — gateway implementation tidak perlu diubahfpditulis ke JWT secara kondisional (tidak hadir jika null) — identik dengan polasid,snm,fet
Claim iss untuk token 3p — APP_URL sebagai issuer
Token 3p (user, M2M, API key JWT) kini menggunakan APP_URL gateway sebagai nilai iss, bukan short code (gwa/gwc). Token 1p tidak berubah — tetap menggunakan short code. Ini mengikuti RFC 9068 yang mengharuskan iss berupa URL untuk token yang diterbitkan ke pihak ketiga.
| Token type | iss sebelum v0.3 | iss mulai v0.3 |
|---|---|---|
| 1p user | gwa / gwc | gwa / gwc (tidak berubah) |
| 1p M2M | gwa / gwc | gwa / gwc (tidak berubah) |
| 3p user | gwa / gwc | https://admin.example.com |
| 3p M2M | gwa / gwc | https://admin.example.com |
| API key JWT | gwa / gwc | https://admin.example.com |
iss di-resolve di dalam AccessToken::toString() dari nilai fp — tidak ada config baru. APP_URL adalah Laravel config standar yang sudah ada di setiap gateway.
Resource server yang menerima token 3p harus upgrade ke sauth-client v0.3.0 dan menambahkan trusted_issuer_url_map ke config/sauth-client.php:
'trusted_issuer_url_map' => [
'gwa' => env('GWA_APP_URL'),
'gwc' => env('GWC_APP_URL'),
],Resource server yang hanya menerima token 1p tidak perlu perubahan apapun.
🟢 Medium Impact
FetchesClientToken fake token — dua bug sekaligus
Bug 1 — fet: [] di token M2M. Token M2M palsu (bypass mode) sebelumnya menyertakan withClaim('fet', []). fet adalah permission set yang terikat ke user — tidak relevan untuk worker M2M. Klaim ini telah dihapus agar shape token palsu konsisten dengan shape token 1p M2M nyata (hanya iss, aud, iat, exp, sid, fp).
Bug 2 — Configuration::forUnsecuredSigner() dihapus di lcobucci/jwt v5. Metode ini tidak lagi tersedia sejak lcobucci/jwt v5.x. Token palsu sekarang menggunakan Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText(...)) dengan throwaway key 32-byte. sauth-client bypass mode tetap melewati verifikasi signature — nilai key tidak berpengaruh. Bug ini hanya berdampak di local dev (app()->isLocal() && SAUTH_BYPASS=true), bukan di production.
Poin utama:
- Tidak ada perubahan perilaku yang terlihat dari luar di dev bypass mode
- Token palsu sekarang memiliki shape yang benar:
fp='1p', tanpafet, tanpascope - Production path (
fetchClientToken) tidak berubah sama sekali
ServiceTokenIssuer — fp tidak di-emit untuk ticketbooth token
ServiceTokenIssuer membangun JWT langsung via lcobucci/jwt tanpa melewati Passport grant flow maupun AccessTokenRepository — sehingga setFp() tidak pernah dipanggil dan claim fp tidak hadir di ticketbooth token. Ticketbooth selalu menerbitkan token 1p user; fp='1p' kini ditambahkan eksplisit ke builder chain.
Poin utama:
- Hanya berdampak pada token yang diterbitkan via
ServiceTokenIssuer::issue()(endpointPOST /api/sauth/token) - Token dari Passport grant flow (
AccessTokenRepository) tidak terpengaruh — sudah benar sejak implementasi awalfp - Tidak ada perubahan interface atau konfigurasi
Upgrade
composer update bpmlib/sauth-serverUrutan deploy yang wajib diikuti:
- Deploy
bpmlib/sauth-client v0.3.0ke semua resource server terlebih dulu- Client v0.3.0 backward-compatible: token lama tanpa
fptetap diterima,issshort code tetap dikenali - Resource server yang menerima token 3p: tambahkan
trusted_issuer_url_mapdan envGWA_APP_URL/GWC_APP_URL
- Client v0.3.0 backward-compatible: token lama tanpa
- Deploy
bpmlib/sauth-server v0.3.0ke GWA dan GWC- Setelah ini, semua token baru membawa
fp; token 3p menggunakanAPP_URLsebagaiiss
- Setelah ini, semua token baru membawa
Jangan deploy server v0.3.0 sebelum client v0.3.0 — resource server lama tidak dapat memetakan iss berformat URL ke public key yang benar, sehingga token 3p akan ditolak dengan UnknownIssuerException.
Wajib untuk resource server yang menerima token 3p:
GWA_APP_URL=https://admin.example.com
GWC_APP_URL=https://customer.example.comTidak ada perubahan wajib untuk:
- Gateway implementation (
GwaClaimsProvider,GwcClaimsProvider) — tidak berubah - Resource server yang hanya menerima token 1p —
isstidak berubah - Worker/job yang menggunakan
FetchesClientToken— API tidak berubah
v0.2.0 (2026-05-22)
TL;DR
Perubahan:
- 🔧 1p M2M token shape diperbaiki —
fetdihapus dari token M2M (High) - ✨ Claim
scopeditambahkan ke token 3p (High) - ✨
ApiKeyIssuer+bpm:sauth:apikey— infrastruktur API key JWT untuk integrasi partner (High) - 🐛
FetchesClientToken— bug cache key salah + TTL override hilang (Medium)
Impact: 🟡 High: 3 | 🟢 Medium: 1
Backward Compatible: ⚠️ Partial — deploy bersama sauth-client v0.2.0. Client harus di-deploy lebih dulu (backward-compatible dengan token lama). Lihat bagian Upgrade.
🟡 High Impact
1p M2M token shape — fet dihapus
Token client_credentials dari client first-party sebelumnya membawa fet: []. fet adalah permission set yang terikat ke user — tidak relevan untuk worker/job M2M. Klaim ini sekarang dihapus sepenuhnya.
Token 1p M2M v0.2.0 hanya membawa: iss, aud, iat, exp, sid (= client_id). Tidak ada snm, tidak ada fet, tidak ada scope.
Implikasi di sauth-client: isFirstParty() berubah dari discriminator permissions !== null (cek keberadaan fet) menjadi scope === null (cek ketidakhadiran scope). Keduanya harus di-deploy bersama — lihat urutan deploy di bawah.
Poin utama:
- Worker/job yang menggunakan
FetchesClientTokentidak perlu perubahan apapun - Resource server yang menggunakan
sauth.gateatausauth.m2mtidak perlu perubahan - Hanya
isFirstParty()di sauth-client yang berubah cara kerjanya
Claim scope untuk token 3p
Token dari client third-party (user maupun M2M) kini membawa claim scope berupa string space-separated. Sebelumnya claim ini tidak di-embed oleh AccessToken::toString().
| Token type | scope |
|---|---|
| 1p user | Tidak ada |
| 1p M2M | Tidak ada |
| 3p user | Ada — scope yang di-grant saat consent |
| 3p M2M | Ada — scope yang di-assign ke client |
| API key JWT | Ada — scope yang di-tentukan saat issuance |
Middleware sauth.scope di sauth-client sekarang dapat membaca scope dari JWT secara langsung untuk semua token 3p.
ApiKeyIssuer + bpm:sauth:apikey — API key JWT
Infrastruktur baru untuk menerbitkan JWT long-lived bagi integrasi partner (machine-to-machine tanpa OAuth flow). Token ditandatangani dengan private key gateway yang sama — resource server memvalidasinya identik dengan token OAuth biasa. Tidak ada exp — revokasi dilakukan via blacklist jti di masing-masing resource server.
Tiga command baru:
# Terbitkan API key baru
php artisan bpm:sauth:apikey issue acme-erp srf --scopes="srf.read srf.write"
# Lihat semua API key yang pernah diterbitkan
php artisan bpm:sauth:apikey list
# Tandai revoked (audit) — lanjutkan dengan block di resource server
php artisan bpm:sauth:apikey revoke {jti} --reason="Compromised"Tabel baru sauth_jti_tokens — dipublish otomatis oleh bpm:sauth:init. Menyimpan metadata API key (app, aud, issuer) beserta token dan scopes dalam bentuk terenkripsi (Crypt::encryptString). Token plaintext hanya terlihat saat issuance dan saat diambil via endpoint webmaster.
Event JtiTokenRevoked — dilempar saat revoke() dipanggil. GWA dapat mendaftarkan listener di EventServiceProvider untuk propagasi otomatis ke resource server. Tanpa listener, revokasi dilakukan manual via bpm:sauth:jti block {jti} di masing-masing resource server.
Poin utama:
- Satu API key per service (satu
audper key) - JWT ditandatangani RSA — tidak ada mekanisme autentikasi terpisah
- Revokasi di GWA (audit) dan revokasi di resource server (blacklist) adalah dua langkah terpisah
bpm:sauth:apikey revokemencetak reminder untuk menjalankanbpm:sauth:jti blockdi resource server
🟢 Medium Impact
FetchesClientToken — perbaikan cache key dan TTL
Dua bug sekaligus di fetchClientToken():
Bug 1 — Cache key salah. Cache key yang ditulis saat fetch token menggunakan $response['client_id'] (field yang tidak ada di response token endpoint OAuth), sementara clientToken() dan forgetClientToken() membaca dari key yang dibangun dari config('sauth-server.client_credentials.client_id'). Akibatnya:
- Cache yang ditulis
fetchClientToken()tidak pernah dibaca olehclientToken()— setiap panggilan selalu hit ke GWA forgetClientToken()menghapus key yang berbeda dari yang di-cache — tidak berpengaruh sama sekali- Di PHP 8.5, akses key yang tidak ada di array dilempar sebagai error (sebelumnya hanya warning yang diabaikan)
Bug 2 — TTL override hilang. clientToken() menggunakan Cache::remember($key, 720, callback). Cache::remember memanggil callback, lalu menimpa hasilnya dengan TTL=720. Padahal fetchClientToken() sudah melakukan Cache::put($key, $token, 80% of expires_in) dengan TTL yang benar dari server. Urutan eksekusi: fetchClientToken menulis cache dengan TTL benar → Cache::remember menimpa dengan TTL=720.
Perbaikan: clientToken() diganti dari Cache::remember ke Cache::get() ?? fetchClientToken(). fetchClientToken() menerima $cacheKey sebagai parameter dan melakukan Cache::put langsung dengan TTL 80% dari server. TTL fallback clientTokenTtl() yang sudah tidak dipakai dihapus.
Dampak di production: Worker/job yang sudah berjalan tidak perlu perubahan konfigurasi. Setelah upgrade, token M2M akan di-cache dengan TTL yang benar (80% dari expires_in GWA) dan forgetClientToken() akan benar-benar menghapus token yang di-cache. Tidak ada perubahan perilaku yang terlihat dari luar — hanya perbaikan keandalan cache.
Upgrade
composer update bpmlib/sauth-serverUrutan deploy yang direkomendasikan:
- Deploy
bpmlib/sauth-client v0.2.0ke semua resource server terlebih dulu- Client v0.2.0 backward-compatible dengan token lama (yang masih punya
fet: [])
- Client v0.2.0 backward-compatible dengan token lama (yang masih punya
- Deploy
bpmlib/sauth-server v0.2.0ke GWA dan GWC- Setelah ini, token 1p M2M baru tidak lagi membawa
fet
- Setelah ini, token 1p M2M baru tidak lagi membawa
Jangan deploy server v0.2.0 sebelum client v0.2.0 — resource server dengan client lama akan gagal mendeteksi token 1p M2M sebagai first-party karena fet tidak lagi hadir.
Wajib:
- Jalankan
php artisan bpm:sauth:initdi GWA untuk mempublish dan menjalankan migrasisauth_jti_tokens - Pastikan
APP_KEYsudah dikonfigurasi — dibutuhkan olehCrypt::encryptStringuntuk menyimpan token API key
Opsional:
- Daftarkan listener
JtiTokenRevokeddiEventServiceProviderGWA untuk propagasi revokasi otomatis ke resource server
v0.1.3 (2026-05-21)
TL;DR
Perubahan:
- ✨
FetchesClientToken—$audienceparam + dev bypass mode (High) - 🔧 Config
bypasskey —SAUTH_BYPASSuntuk local dev (Medium)
Impact: 🟡 High: 1 | 🟢 Medium: 1
Backward Compatible: ✅ Ya — clientToken() dan forgetClientToken() tanpa argumen tetap berfungsi seperti sebelumnya
🟡 High Impact
FetchesClientToken — $audience param + dev bypass mode
clientToken() dan forgetClientToken() kini menerima argumen $audience opsional. Default ke sauth-server.default_audience jika tidak diisi — tidak ada perubahan perilaku untuk caller yang tidak mengisi argumen.
Cache key kini menyertakan audience: sauth_client_token_{client_id}_{audience}. Job yang memanggil dua service berbeda (clientToken('srf') dan clientToken('pnr')) mendapat cache entry yang terpisah.
Dev bypass mode — saat SAUTH_BYPASS=true dan app()->isLocal(), clientToken() tidak memanggil GWA. Sebuah JWT palsu dibuat lokal menggunakan alg:none dengan claim iss, aud, sid, fet: []. JWT ini di-cache 720 detik. Di sisi penerima, sauth-client bypass mode menerima JWT ini tanpa verifikasi signature — tidak ada GWA yang dibutuhkan dari kedua sisi.
Poin utama:
- Argumen
$audienceopsional — caller lama tanpa argumen tidak berubah forgetClientToken('srf')hanya menghapus cache untuk audiencesrf- Bypass hanya aktif saat
app()->isLocal()— nilaitruedi production tidak berpengaruh - JWT palsu menggunakan
lcobucci/jwtyang sudah jadi transitive dependency via Passport
Konfigurasi caller (service worker):
SAUTH_BYPASS=true
SAUTH_CLIENT_ID=srf-worker-local
SAUTH_ISSUER_CODE=gwa🟢 Medium Impact
Config bypass — SAUTH_BYPASS untuk local dev
Key baru bypass di config/sauth-server.php (backed by SAUTH_BYPASS, default false) mengaktifkan FetchesClientToken bypass mode. Tidak ada efek di luar FetchesClientToken — ServiceTokenIssuer dan Passport grant flow tidak terpengaruh.
Upgrade
composer update bpmlib/sauth-serverBreaking changes: Tidak ada.
Opsional:
- Tambahkan
SAUTH_BYPASS=truedi.envservice worker lokal untuk dev tanpa GWA - Update caller dari
clientToken()keclientToken('srf')agar cache per-audience — tidak wajib, tapi direkomendasikan jika satu job memanggil beberapa service
v0.1.2 (2026-05-21)
TL;DR
Perubahan:
- 🔧
SAUTH_PRIVATE_KEY_FILE(Medium) — konfigurasi path private key relatif kestorage_path()untukServiceTokenIssuer; mempermudah key rotation tanpa mengubah.envbertipe inline PEM - 🔧 Bump dependency
bpmlib/sauth-clientke^0.1.3(Low)
Impact: 🟢 Medium: 1 | 🔵 Low: 1
Backward Compatible: ✅ Ya
🟢 Medium Impact
private_key_file — Konfigurasi Path Private Key
ServiceTokenIssuer kini membaca lokasi private key dari config('sauth-server.private_key_file') yang di-back oleh SAUTH_PRIVATE_KEY_FILE. Path bersifat relatif terhadap storage_path() — misalnya keys/gwa_private.pem akan resolve ke storage/keys/gwa_private.pem.
Sebelumnya path key di-hardcode ke oauth-private.key (konvensi Passport). Default baru tetap 'oauth-private.key' sehingga tidak ada perubahan perilaku untuk deployment yang sudah ada.
Kapan ini berguna:
- Menyimpan key di subdirektori khusus (
storage/keys/) untuk pemisahan tanggung jawab - Key rotation — ganti file di server tanpa mengubah
.envatau restart service - Deployment multi-gateway di mana setiap gateway memiliki nama file key berbeda
Poin utama:
- Hanya berlaku untuk
ServiceTokenIssuer(ticketbooth) — Passport grant flow menggunakan key loading Passport sendiri, tidak terpengaruh - Default
'oauth-private.key'identik dengan konvensi Passport lama — tidak ada perubahan perilaku - Path yang dikonfigurasi harus dapat dibaca oleh web server process
Konfigurasi:
# Contoh penggunaan subdirektori
SAUTH_PRIVATE_KEY_FILE=keys/gwa_private.pem
# Default — identik dengan konvensi Passport lama
# SAUTH_PRIVATE_KEY_FILE=oauth-private.key🔵 Low Impact
Peningkatan:
- Dependency
bpmlib/sauth-clientdi-bump ke^0.1.3— ikuti rilis sauth-client terbaru
Upgrade
composer update bpmlib/sauth-serverBreaking changes: Tidak ada.
Opsional: Set SAUTH_PRIVATE_KEY_FILE di .env jika ingin menyimpan private key di lokasi selain default storage/oauth-private.key.
v0.1.1 (2026-05-20)
TL;DR
Perubahan:
- ✨
ServiceTokenIssuer(High) — reusable ticketbooth logic untuk GWA dan GWC; terbitkan JWT sesi-ke-service tanpa Passport grant flow, tanpa DB write
Impact: 🟡 High: 1
Backward Compatible: ✅ Ya
🟡 High Impact
ServiceTokenIssuer — Ticketbooth JWT Issuance
Service baru ServiceTokenIssuer memusatkan logika ticketbooth yang sebelumnya harus diimplementasikan secara terpisah di controller masing-masing gateway. Dengan adanya service ini, GWA dan GWC cukup meng-inject ServiceTokenIssuer — tidak ada lagi duplikasi issuance logic.
ServiceTokenIssuer::issue($user, $targetService) menerbitkan JWT bertarget service tertentu langsung via lcobucci/jwt — melewati Passport grant flow sepenuhnya. Token tidak ditulis ke oauth_access_tokens. Tidak ada refresh token karena session gateway adalah long-lived credential.
Poin utama:
- Inject via constructor DI — auto-bound sebagai singleton oleh service provider
- Membaca
MicroserviceTokenClaimsProviderInterface::getClaimsForUser($user)— claims konsisten dengan token Passport - Token ditandatangani dengan private key gateway yang sama (
oauth-private.key) dan konfigurasi algorima yang sama (sauth-server.algorithm) - TTL mengikuti
sauth-server.token_ttl.access(default 900 detik) RuntimeExceptiondilempar jikasauth-server.issuer_codekosong
Pola controller yang direkomendasikan (di gateway — bukan di library ini):
Route::middleware('auth:sanctum')
->post('/api/sauth/token', [ServiceTokenController::class, 'issue']);public function issue(Request $request, ServiceTokenIssuer $issuer): JsonResponse
{
$request->validate(['target' => 'required|string']);
return response()->json($issuer->issue($request->user(), $request->input('target')));
}Upgrade
composer update bpmlib/sauth-serverBreaking changes: Tidak ada.
Opsional: Ganti issuance logic di controller ticketbooth GWA/GWC dengan ServiceTokenIssuer — inject dan delegate, hapus implementasi lokal.
v0.1.0 (2026-05-19)
TL;DR
Perubahan:
- ✨ Standardized JWT claim structure +
MicroserviceTokenClaimsProviderInterface(High) — contract terpusat untuk claim injection di setiap gateway - ✨
bpm:sauth:initcommand (High) — setup satu langkah: migrasi, is_first_party column, OAuth keys - ✨
ClientRegistrarservice +bpm:sauth:clientcommand (High) — manajemen lifecycle OAuth client - ✨
FetchesClientTokentrait (High) — M2M token caching otomatis untuk workers/jobs - ✨
TokenExchangeGrantRFC 8693 (High) — token exchange untuk service chaining dengan user context - 🔧 Configurable signing algorithm (Medium) — RS256, RS384, RS512, ES256, ES384, ES512
- 🔧 Configurable token TTL via environment variables (Medium)
- 🔧 1st-party vs 3rd-party client distinction via
is_first_party(Medium) - 📝 Published
config/sauth-server.php(Low) - 📝 Auto-discovery service provider (Low)
Impact: 🟡 High: 5 | 🟢 Medium: 3 | 🔵 Low: 2
Backward Compatible: ✅ Ya (initial release)
🟡 High Impact
Standardized JWT Claim Structure + MicroserviceTokenClaimsProviderInterface
Library ini memperkenalkan satu contract terpusat — MicroserviceTokenClaimsProviderInterface — yang menggantikan copy-paste JWT issuance logic di setiap gateway. Setiap gateway implement interface ini satu kali; library yang memanggil dan meng-inject claim standar (iss, sid, snm, fet, act) ke setiap token yang diterbitkan Passport.
Poin utama:
- Claim contract bersama antara
sauth-server,sauth-client, dansauth-frontend - User token selalu membawa
sid,snm,fet; M2M token hanyasid+fet = [] 'wm'padafetbypass semua permission checks disauth.gate
bpm:sauth:init
Command bpm:sauth:init menggantikan serangkaian langkah manual setup — publish Passport migrations, tulis migration is_first_party, jalankan migrate, dan generate OAuth keys — menjadi satu perintah. Migration is_first_party kini di-ship sebagai stub dalam library dan di-copy langsung ke database/migrations/ dengan timestamp prefix, tanpa perlu vendor:publish tag terpisah.
Poin utama:
- Idempotent — aman dijalankan berulang kali, setiap langkah dicek sebelum dieksekusi
--forceuntuk regenerasi OAuth keys (rotasi key)- Tidak perlu menulis migration
is_first_partysecara manual
ClientRegistrar + bpm:sauth:client
Service ClientRegistrar memusatkan seluruh logika OAuth client lifecycle — create, update, delete, dan rotasi secret — sehingga gateway controller dan artisan command menggunakan implementasi yang sama tanpa duplikasi. Command bpm:sauth:client adalah thin wrapper di atas ClientRegistrar untuk keperluan CLI.
Poin utama:
- 4 kombinasi flag untuk 4 tipe client (3p M2M, 1p M2M, 3p user, 1p user)
plainSecrethanya tersedia di instance yang dikembalikan — tidak bisa diambil lagi- Auto-bound sebagai singleton; inject via constructor DI di gateway controller
FetchesClientToken
Trait untuk worker/job base class yang memanggil service lain menggunakan M2M token. Menangani siklus fetch → cache → refresh secara transparan: token di-cache hingga 80% TTL-nya, lalu di-refresh otomatis sebelum kadaluarsa. forgetClientToken() digunakan untuk clear cache setelah menerima 401 dari downstream service.
Poin utama:
- Zero boilerplate di job — cukup panggil
$this->clientToken() - Cache key unik per
client_id; aman untuk multi-client deployment - Konfigurasi via
sauth-server.client_credentialsdi.env
TokenExchangeGrant (RFC 8693)
Custom grant type yang memungkinkan service menukar token user yang diterimanya dengan token baru bertarget service lain, tanpa kehilangan identitas user asli. Token hasil exchange membawa act.sub berisi client ID service pemanggil — PNR dapat mengetahui bahwa SRF-lah yang melakukan panggilan atas nama user tersebut.
Poin utama:
- Parameter
resourcewajib ada — satu token hanya untuk satu target audience (RFC 9700) - Dinonaktifkan secara default; aktifkan via
SAUTH_TOKEN_EXCHANGE_ENABLED=truedi GWA - GWA adalah satu-satunya token exchange authority — GWC token diterima sebagai subject token
🟢 Medium Impact
Peningkatan:
- Signing algorithm dapat dikonfigurasi via
SAUTH_ALGO— mendukung RS256, RS384, RS512, ES256, ES384, ES512; default RS256 - Token TTL dapat dikonfigurasi per gateway via
SAUTH_ACCESS_TOKEN_TTLdanSAUTH_REFRESH_TOKEN_TTL - Dukungan 1st-party vs 3rd-party client via kolom
is_first_partycustom dioauth_clients— menentukan claim yang di-embed dan middleware yang digunakan
🔵 Low Impact
Perubahan:
- Published
config/sauth-server.phpviaphp artisan vendor:publish --tag=sauth-config - Auto-discovery service provider via Laravel package discovery
Upgrade
composer require bpmlib/sauth-server:^0.1.0Breaking changes: Tidak ada (initial release)
Wajib: Tambahkan kolom is_first_party ke tabel oauth_clients dan implement MicroserviceTokenClaimsProviderInterface di gateway sebelum aplikasi dijalankan.