Skip to content

Sauth Server — Changelog

v0.3.7 (2026-06-13)

TL;DR

Perubahan:

  • 🔧 client_credentials grant — 1p M2M token menghormati parameter resource untuk override aud (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=pnr

Penamaan 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 resource mendapat aud = default_audience (identik dengan sebelumnya)
  • Resource server tetap menjadi penjaga akhir — validasi aud di sisi penerima tidak berubah

Upgrade

bash
composer update bpmlib/sauth-server

Breaking changes: Tidak ada.


v0.3.6 (2026-06-08)

TL;DR

Perubahan:

  • 🐛 JWKS — kid tidak 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 ke id_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

bash
composer update bpmlib/sauth-server

Breaking 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_token untuk client 3p yang ditandai is_oidc=true (High)
  • 🔧 bpm:sauth:client --oidc — flag baru + validasi kombinasi flag + prompt interaktif untuk --user (Low)
  • 🔧 ClientRegistrarcreate() dan update() mendapat parameter $isOidc (Low)
  • 🔧 AccessToken — getter getSid(), getSnm(); infrastruktur nonce setNonce()/getNonce() (Low)
  • 🐛 UserInfoController — Eloquent user lookup gagal di Bearer token context; OidcClaimsProviderInterface tidak 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 document
  • GET /.well-known/jwks.json — public key dalam format JWK Set (native OpenSSL, tanpa dependency baru)
  • GET /userinfo — mengembalikan sub + name; claim tambahan via OidcClaimsProviderInterface opsional

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

bash
composer update bpmlib/sauth-server

Breaking changes: Tidak ada.

Untuk mengaktifkan OIDC:

  1. Set SAUTH_OIDC_ENABLED=true di .env
  2. Jalankan php artisan bpm:sauth:migration
  3. Buat OIDC client via php artisan bpm:sauth:client --user --oidc
  4. Opsional — implement OidcClaimsProviderInterface untuk 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:

KonteksUrutan resolusi
API key JWT signingSAUTH_APIKEY_SIGN_ALGOSAUTH_SIGN_ALGOSAUTH_ALGORS256
Token Passport regulerSAUTH_SIGN_ALGOSAUTH_ALGORS256 (tidak berubah)

Fallback chain key file:

KonteksUrutan resolusi
API key JWT keySAUTH_APIKEY_PRIVATE_KEY_FILESAUTH_PRIVATE_KEY_FILEoauth-private.key
Token Passport regulerSAUTH_PRIVATE_KEY_FILEoauth-private.key (tidak berubah)

Ketika kedua env var tidak di-set, ApiKeyIssuer menggunakan key dan algo yang sama seperti sebelumnya — tidak ada perubahan perilaku.

dotenv
# 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
bash
# Generate key pair terpisah untuk API key JWT
php artisan bpm:sauth:keygen ES256 --path=keys --file-prefix=apikey

Poin utama:

  • Kedua env var opsional — tanpa keduanya, perilaku identik dengan v0.3.2
  • ApiKeyIssuer kini menggunakan KeyLoader (bukan InMemory::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_algorithm dan apikey_private_key_file

Upgrade

bash
composer update bpmlib/sauth-server

Breaking 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 dari SAUTH_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:

KonteksUrutan resolusi
Token signing (SignerResolver)SAUTH_SIGN_ALGOSAUTH_ALGORS256
bpm:sauth:keygen (tanpa argumen)SAUTH_SIGN_ALGOSAUTH_ALGORS256
bpm:sauth:init (tanpa --algo)SAUTH_SIGN_ALGOSAUTH_ALGORS256

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):

bash
php artisan bpm:sauth:keygen          # algo dari SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256
php artisan bpm:sauth:keygen ES256    # argumen eksplisit tetap menang

Pola migrasi algoritma gateway:

dotenv
# .env di gateway — beralih ke ES256 tanpa mengubah SAUTH_ALGO yang dibaca resource server
SAUTH_SIGN_ALGO=ES256
SAUTH_ALGO=RS256
bash
php artisan bpm:sauth:keygen --force    # generate ulang key pair dengan ES256

Setelah resource server siap diperbarui, pindahkan ke SAUTH_ALGO=ES256 dan hapus SAUTH_SIGN_ALGO.

Poin utama:

  • SAUTH_SIGN_ALGO hanya dibaca oleh sauth-server — sauth-client tidak membacanya
  • Nilai default null → fallback ke SAUTH_ALGO → behavior identik dengan v0.3.1 tanpa konfigurasi tambahan
  • Key baru sign_algorithm di-publish via config/sauth-server.php

Upgrade

bash
composer update bpmlib/sauth-server

Breaking 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_code di oauth_clients + validasi target ServiceTokenIssuer — 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 memanggil passport:keys (Medium)
  • bpm:sauth:migration — command standalone untuk publish migrasi sauth tanpa full init (Medium)
  • 🔧 ClientRegistrarcreate() dan update() mendapat parameter serviceCode (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_code digunakan sebagai aud di 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 id tersedia sebagai fallback — memudahkan transisi sebelum semua client punya service_code
  • aud di JWT selalu service_code, tidak pernah client ID
  • ClientRegistrar::create() dan update() mendapat parameter ?string $serviceCode = null
  • bpm:sauth:client --first sekarang meminta service code saat membuat first-party client
  • Dua migration stub baru: add_is_first_party_to_oauth_clients.stub diperbarui (fresh install — tambah kedua kolom sekaligus); add_service_code_to_oauth_clients.stub baru (upgrade path — tambah service_code saja)

Pola controller yang perlu diperbarui (di gateway):

php
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/:

bash
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)
FamilyMethodFormat output
RS256/384/512openssl_pkey_new(), 4096-bit RSAPEM (-----BEGIN RSA PRIVATE KEY-----)
ES256openssl_pkey_new(), curve prime256v1PEM (-----BEGIN EC PRIVATE KEY-----)
ES384openssl_pkey_new(), curve secp384r1PEM
ES512openssl_pkey_new(), curve secp521r1PEM
EdDSAsodium_crypto_sign_keypair()Base64 single-line

SignerResolver mendapat arm baru untuk EDDSAnew Eddsa().

KeyLoader — helper baru src/Support/KeyLoader.php. Dipakai oleh ServiceTokenIssuer untuk menggantikan hardcoded InMemory::file():

php
KeyLoader::privateKey(string $path, string $algo): InMemory
KeyLoader::publicKey(string $path, string $algo): InMemory

Deteksi 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 EdDSA

Poin utama:

  • --path non-root mencetak warning .gitignore — Laravel hanya mengcover /storage/*.key by default
  • --force tanpa konfirmasi yes tidak overwrite (exit 0, bukan failure)
  • EdDSA keys berbentuk satu baris base64 — format berbeda dari PEM; KeyLoader yang menangani routing ke InMemory::base64Encoded() vs InMemory::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:

bash
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=gwa

Resolusi default:

  • --algosauth-server.algorithm config → RS256
  • --path → derivasi dari sauth-server.private_key_file config (dirname)
  • --file-prefix → derivasi dari sauth-server.private_key_file config (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:

bash
php artisan bpm:sauth:migration

Langkah-langkah (masing-masing dengan cek skip):

  1. is_first_party di oauth_clients — fresh install stub (tambah kedua kolom) jika belum ada
  2. service_code di oauth_clients — upgrade stub jika is_first_party sudah ada tapi service_code belum
  3. Tabel sauth_jti_tokens — JTI migration jika tabel belum ada
  4. Jalankan migrate --force sekali 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:

php
// 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  : srf

Upgrade

bash
composer update bpmlib/sauth-server

Urutan deploy yang wajib diikuti:

  1. Jalankan migrasi di GWA dan GWC:
    bash
    php artisan bpm:sauth:migration
    # atau php artisan bpm:sauth:init (jika juga perlu generate key baru)
  2. Populate service_code di 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
  3. 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 iss untuk token 3p — APP_URL sebagai issuer; token 1p tetap menggunakan short code (High)
  • 🐛 FetchesClientToken fake token — hapus fet: [] yang salah + fix forUnsecuredSigner yang sudah dihapus di lcobucci/jwt v5 (Medium)
  • 🐛 ServiceTokenIssuerfp tidak di-emit karena build JWT langsung tanpa lewat AccessTokenRepository (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 typefp
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:

  • AccessToken mendapat method baru setFp(string $fp) — dipanggil repository sebelum toString()
  • MicroserviceTokenClaimsProviderInterface tidak berubah — gateway implementation tidak perlu diubah
  • fp ditulis ke JWT secara kondisional (tidak hadir jika null) — identik dengan pola sid, 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 typeiss sebelum v0.3iss mulai v0.3
1p usergwa / gwcgwa / gwc (tidak berubah)
1p M2Mgwa / gwcgwa / gwc (tidak berubah)
3p usergwa / gwchttps://admin.example.com
3p M2Mgwa / gwchttps://admin.example.com
API key JWTgwa / gwchttps://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:

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', tanpa fet, tanpa scope
  • Production path (fetchClientToken) tidak berubah sama sekali

ServiceTokenIssuerfp 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() (endpoint POST /api/sauth/token)
  • Token dari Passport grant flow (AccessTokenRepository) tidak terpengaruh — sudah benar sejak implementasi awal fp
  • Tidak ada perubahan interface atau konfigurasi

Upgrade

bash
composer update bpmlib/sauth-server

Urutan deploy yang wajib diikuti:

  1. Deploy bpmlib/sauth-client v0.3.0 ke semua resource server terlebih dulu
    • Client v0.3.0 backward-compatible: token lama tanpa fp tetap diterima, iss short code tetap dikenali
    • Resource server yang menerima token 3p: tambahkan trusted_issuer_url_map dan env GWA_APP_URL/GWC_APP_URL
  2. Deploy bpmlib/sauth-server v0.3.0 ke GWA dan GWC
    • Setelah ini, semua token baru membawa fp; token 3p menggunakan APP_URL sebagai iss

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:

dotenv
GWA_APP_URL=https://admin.example.com
GWC_APP_URL=https://customer.example.com

Tidak ada perubahan wajib untuk:

  • Gateway implementation (GwaClaimsProvider, GwcClaimsProvider) — tidak berubah
  • Resource server yang hanya menerima token 1p — iss tidak berubah
  • Worker/job yang menggunakan FetchesClientToken — API tidak berubah

v0.2.0 (2026-05-22)

TL;DR

Perubahan:

  • 🔧 1p M2M token shape diperbaiki — fet dihapus dari token M2M (High)
  • ✨ Claim scope ditambahkan 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 FetchesClientToken tidak perlu perubahan apapun
  • Resource server yang menggunakan sauth.gate atau sauth.m2m tidak 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 typescope
1p userTidak ada
1p M2MTidak ada
3p userAda — scope yang di-grant saat consent
3p M2MAda — scope yang di-assign ke client
API key JWTAda — 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:

bash
# 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 aud per 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 revoke mencetak reminder untuk menjalankan bpm:sauth:jti block di 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 oleh clientToken() — 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

bash
composer update bpmlib/sauth-server

Urutan deploy yang direkomendasikan:

  1. Deploy bpmlib/sauth-client v0.2.0 ke semua resource server terlebih dulu
    • Client v0.2.0 backward-compatible dengan token lama (yang masih punya fet: [])
  2. Deploy bpmlib/sauth-server v0.2.0 ke GWA dan GWC
    • Setelah ini, token 1p M2M baru tidak lagi membawa fet

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:init di GWA untuk mempublish dan menjalankan migrasi sauth_jti_tokens
  • Pastikan APP_KEY sudah dikonfigurasi — dibutuhkan oleh Crypt::encryptString untuk menyimpan token API key

Opsional:

  • Daftarkan listener JtiTokenRevoked di EventServiceProvider GWA untuk propagasi revokasi otomatis ke resource server

v0.1.3 (2026-05-21)

TL;DR

Perubahan:

  • FetchesClientToken$audience param + dev bypass mode (High)
  • 🔧 Config bypass key — SAUTH_BYPASS untuk 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 $audience opsional — caller lama tanpa argumen tidak berubah
  • forgetClientToken('srf') hanya menghapus cache untuk audience srf
  • Bypass hanya aktif saat app()->isLocal() — nilai true di production tidak berpengaruh
  • JWT palsu menggunakan lcobucci/jwt yang sudah jadi transitive dependency via Passport

Konfigurasi caller (service worker):

dotenv
SAUTH_BYPASS=true
SAUTH_CLIENT_ID=srf-worker-local
SAUTH_ISSUER_CODE=gwa

🟢 Medium Impact

Config bypassSAUTH_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 FetchesClientTokenServiceTokenIssuer dan Passport grant flow tidak terpengaruh.


Upgrade

bash
composer update bpmlib/sauth-server

Breaking changes: Tidak ada.

Opsional:

  • Tambahkan SAUTH_BYPASS=true di .env service worker lokal untuk dev tanpa GWA
  • Update caller dari clientToken() ke clientToken('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 ke storage_path() untuk ServiceTokenIssuer; mempermudah key rotation tanpa mengubah .env bertipe inline PEM
  • 🔧 Bump dependency bpmlib/sauth-client ke ^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 .env atau 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:

dotenv
# 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-client di-bump ke ^0.1.3 — ikuti rilis sauth-client terbaru

Upgrade

bash
composer update bpmlib/sauth-server

Breaking 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)
  • RuntimeException dilempar jika sauth-server.issuer_code kosong

Pola controller yang direkomendasikan (di gateway — bukan di library ini):

php
Route::middleware('auth:sanctum')
    ->post('/api/sauth/token', [ServiceTokenController::class, 'issue']);
php
public function issue(Request $request, ServiceTokenIssuer $issuer): JsonResponse
{
    $request->validate(['target' => 'required|string']);
    return response()->json($issuer->issue($request->user(), $request->input('target')));
}

Upgrade

bash
composer update bpmlib/sauth-server

Breaking 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:init command (High) — setup satu langkah: migrasi, is_first_party column, OAuth keys
  • ClientRegistrar service + bpm:sauth:client command (High) — manajemen lifecycle OAuth client
  • FetchesClientToken trait (High) — M2M token caching otomatis untuk workers/jobs
  • TokenExchangeGrant RFC 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, dan sauth-frontend
  • User token selalu membawa sid, snm, fet; M2M token hanya sid + fet = []
  • 'wm' pada fet bypass semua permission checks di sauth.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
  • --force untuk regenerasi OAuth keys (rotasi key)
  • Tidak perlu menulis migration is_first_party secara 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)
  • plainSecret hanya 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_credentials di .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 resource wajib ada — satu token hanya untuk satu target audience (RFC 9700)
  • Dinonaktifkan secara default; aktifkan via SAUTH_TOKEN_EXCHANGE_ENABLED=true di 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_TTL dan SAUTH_REFRESH_TOKEN_TTL
  • Dukungan 1st-party vs 3rd-party client via kolom is_first_party custom di oauth_clients — menentukan claim yang di-embed dan middleware yang digunakan

🔵 Low Impact

Perubahan:

  • Published config/sauth-server.php via php artisan vendor:publish --tag=sauth-config
  • Auto-discovery service provider via Laravel package discovery

Upgrade

bash
composer require bpmlib/sauth-server:^0.1.0

Breaking changes: Tidak ada (initial release)

Wajib: Tambahkan kolom is_first_party ke tabel oauth_clients dan implement MicroserviceTokenClaimsProviderInterface di gateway sebelum aplikasi dijalankan.