Sauth Server
OAuth Authorization Server untuk Laravel microservices — wraps Passport dengan standardized JWT claims.
Versi: 0.3.7 Changelog
TL;DR
Library ini menggantikan copy-paste JWT issuance logic di setiap gateway dengan satu implementasi terkontrol. Install di GWA (sa-gw-admin) dan GWC (sa-gw-customer-2) — dua gateway yang bertindak sebagai OAuth Authorization Server untuk seluruh ekosistem microservice. Services (SRF, PNR, PEL, dll.) hanya install sauth-client, bukan library ini.
Namespace:
use Bpmlib\SauthServer\Contracts\MicroserviceTokenClaimsProviderInterface;
use Bpmlib\SauthServer\Contracts\OidcClaimsProviderInterface; // v0.3.4
use Bpmlib\SauthServer\Concerns\FetchesClientToken;
use Bpmlib\SauthServer\Services\ApiKeyIssuer;
use Bpmlib\SauthServer\Services\ClientRegistrar;
use Bpmlib\SauthServer\Services\IdTokenIssuer; // v0.3.4
use Bpmlib\SauthServer\Services\ServiceTokenIssuer;
use Bpmlib\SauthServer\Support\KeyLoader;Artisan Commands:
php artisan bpm:sauth:init # Inisialisasi: migrasi, is_oidc column, sauth_jti_tokens, OAuth keys
php artisan bpm:sauth:migration # Publish dan jalankan sauth migrations saja (tanpa keygen)
php artisan bpm:sauth:keygen # Generate key pair untuk algoritma tertentu (RS256, ES256, EdDSA, dll.)
php artisan bpm:sauth:client # Buat OAuth client baru — --oidc untuk OIDC client (v0.3.4)
php artisan bpm:sauth:apikey issue # Terbitkan API key JWT untuk partner
php artisan bpm:sauth:apikey list # Lihat semua API key JWT yang diterbitkan
php artisan bpm:sauth:apikey revoke # Cabut API key JWTKonfigurasi:
config/sauth-server.php— Lihat lengkap
Installation & Setup
Requirements
PHP:
- Minimum: 8.4
Composer Dependencies:
composer require bpmlib/sauth-clientFramework Requirements:
- Laravel 12.0+ atau 13.0+
- Laravel Passport 12.0+ atau 13.0+
Composer Install
composer require bpmlib/sauth-serverAuto-Discovery
Service provider auto-registered via package discovery. Jika tidak menggunakan auto-discovery:
// config/app.php
'providers' => [
Bpmlib\SauthServer\SauthServerServiceProvider::class,
Bpmlib\SauthClient\SauthClientServiceProvider::class,
],Publish Commands
php artisan vendor:publish --tag=sauth-server-configUntuk konfigurasi lengkap, lihat Configuration.
Jalankan perintah init untuk setup database dan generate OAuth keys dalam satu langkah:
php artisan bpm:sauth:initCommand ini menangani: publish Passport migrations, publish sauth migrations (is_first_party, service_code, sauth_jti_tokens), jalankan migrate, dan generate OAuth key pair. Lihat bpm:sauth:init untuk detail.
IMPORTANT
Sebelum deploy v0.3.1: jalankan bpm:sauth:migration terlebih dulu, lalu populate service_code di semua 1p resource server client. ServiceTokenIssuer akan menolak target yang belum terdaftar dengan InvalidArgumentException. Lihat Deployment Order.
Quick Start
Basic Usage
Implement MicroserviceTokenClaimsProviderInterface di gateway, lalu bind di AppServiceProvider:
<?php
namespace App\Services;
use Bpmlib\SauthServer\Contracts\MicroserviceTokenClaimsProviderInterface;
use Illuminate\Contracts\Auth\Authenticatable;
class GwaClaimsProvider implements MicroserviceTokenClaimsProviderInterface
{
public function getClaimsForUser(?Authenticatable $user): array
{
if ($user === null) {
return ['sid' => '', 'snm' => null]; // M2M — tidak dipanggil untuk 1p M2M di v0.2.0+
}
return [
'sid' => (string) $user->id,
'snm' => $user->name,
'fet' => $user->is_webmaster ? 'wm' : $user->getFeatures(),
];
}
}// App\Providers\AppServiceProvider::register()
$this->app->bind(
MicroserviceTokenClaimsProviderInterface::class,
GwaClaimsProvider::class,
);Key Points:
- Setiap gateway implement interface ini satu kali — library yang memanggilnya selama token issuance untuk user token
- Di v0.2.0+,
getClaimsForUser(null)tidak dipanggil untuk 1p M2M (client_credentials) — library setsid = client_idlangsung; tidak adafetatausnmdi token M2M - Null case tetap diperlukan untuk interface compliance; nilai return-nya diabaikan
'wm'padafetmemberikan bypass penuh ke semua route yang dijagasauth.gate
Configuration
Configuration File
php artisan vendor:publish --tag=sauth-server-configAvailable Options
| Option | Type | Default | Description |
|---|---|---|---|
issuer_code | string | - | Kode issuer gateway — di-embed sebagai iss di setiap JWT |
default_audience | string | 'internal-services' | Fallback aud claim jika tidak ada target service |
algorithm | string | 'RS256' | Algoritma signing JWT shared dengan sauth-client Lihat selengkapnya |
sign_algorithm | string|null | null | Override algoritma signing sauth-server — mengalahkan SAUTH_ALGO untuk token signing dan key generation. Null = fallback ke algorithm. Lihat selengkapnya |
apikey_sign_algorithm | string|null | null | Override algo signing khusus API key JWT — fallback: SAUTH_APIKEY_SIGN_ALGO → SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256. Null = ikuti chain reguler. Lihat selengkapnya |
apikey_private_key_file | string|null | null | Override path private key khusus API key JWT — fallback: SAUTH_APIKEY_PRIVATE_KEY_FILE → SAUTH_PRIVATE_KEY_FILE → oauth-private.key. Lihat selengkapnya |
token_exchange.enabled | bool | false | Aktifkan token exchange grant (RFC 8693) |
token_ttl.access | int | 900 | TTL access token dalam detik (default 15 menit) |
token_ttl.refresh | int | 604800 | TTL refresh token dalam detik (default 7 hari) |
private_key_file | string | 'oauth-private.key' | Path private key relatif ke storage_path() — digunakan ServiceTokenIssuer dan bpm:sauth:init untuk signing JWT ticketbooth |
client_credentials.token_url | string | - | URL /oauth/token di GWA — untuk FetchesClientToken |
client_credentials.client_id | string | - | Client ID — untuk FetchesClientToken |
client_credentials.client_secret | string | - | Client secret — untuk FetchesClientToken |
bypass | bool | false | Dev bypass: saat true + app()->isLocal(), FetchesClientToken tidak memanggil GWA — JWT palsu dibuat lokal (alg:none). Tidak aktif di production. Lihat selengkapnya |
oidc.enabled | bool | false | Aktifkan OIDC support — mendaftarkan endpoint /.well-known/openid-configuration, /.well-known/jwks.json, /userinfo, dan mengaktifkan id_token issuance untuk client is_oidc=true. Lihat selengkapnya |
Environment Variables
SAUTH_ISSUER_CODE=gwa
SAUTH_DEFAULT_AUDIENCE=internal-services
SAUTH_ALGO=RS256
SAUTH_SIGN_ALGO= # opsional — override SAUTH_ALGO untuk signing di sauth-server saja
SAUTH_APIKEY_SIGN_ALGO= # opsional — override algo signing khusus API key JWT
SAUTH_TOKEN_EXCHANGE_ENABLED=false
SAUTH_ACCESS_TOKEN_TTL=900
SAUTH_REFRESH_TOKEN_TTL=604800
# Path private key relatif ke storage_path() — default: storage/oauth-private.key
SAUTH_PRIVATE_KEY_FILE=keys/gwa_private.pem
SAUTH_APIKEY_PRIVATE_KEY_FILE= # opsional — override key file khusus API key JWT
# Untuk FetchesClientToken (services/workers — bukan gateway itu sendiri)
SAUTH_TOKEN_URL=https://gwa.example.com/oauth/token
SAUTH_CLIENT_ID=your-client-id
SAUTH_CLIENT_SECRET=your-client-secret
# Dev bypass — FetchesClientToken tidak memanggil GWA; JWT palsu dibuat lokal
# Hanya aktif saat APP_ENV=local. Jangan set true di production.
SAUTH_BYPASS=false
# OIDC support — aktifkan discovery, JWKS, userinfo, dan id_token issuance
# Jalankan bpm:sauth:migration terlebih dulu untuk menambah kolom is_oidc
SAUTH_OIDC_ENABLED=falsebypass
Saat true dan app()->isLocal(), FetchesClientToken::clientToken() tidak memanggil GWA. Sebuah JWT palsu dibuat secara lokal menggunakan HMAC-SHA256 dengan throwaway key (signature tidak diverifikasi di sisi penerima saat bypass aktif) dengan claim: iss = issuer_code, aud = target audience, sid = client_id, fp = '1p', expiry = 15 menit. JWT palsu ini di-cache 720 detik. Tidak ada fet, snm, atau scope — sesuai dengan shape token 1p M2M nyata.
Di sisi penerima, sauth-client bypass mode (SAUTH_BYPASS=true) menerima JWT ini tanpa verifikasi signature — tidak ada GWA yang dibutuhkan dari kedua sisi.
Kapan ini berguna: service worker lokal (SRF, PNR, dll.) perlu memanggil service lain tanpa GWA berjalan di lingkungan dev.
Guard: kondisi app()->isLocal() di-evaluate saat runtime — nilai true di production tidak akan mengaktifkan bypass selama APP_ENV != local.
oidc.enabled
TIP
NEW v0.3.4
Saat true, SauthServerServiceProvider mendaftarkan tiga endpoint OIDC secara otomatis — tidak perlu perubahan di routes/api.php consuming app:
| Endpoint | Keterangan |
|---|---|
GET /.well-known/openid-configuration | Discovery document — issuer, endpoints, algoritma, scopes |
GET /.well-known/jwks.json | Public key dalam format JWK Set |
GET /userinfo | Kembalikan sub + name; claim tambahan via OidcClaimsProviderInterface |
Juga mengaktifkan id_token issuance untuk client yang ditandai is_oidc = true dalam authorization_code flow + openid scope. Kolom is_oidc harus ada di oauth_clients sebelum flag ini diaktifkan — jalankan bpm:sauth:migration terlebih dulu. Boot-time guard aktif: jika kolom belum ada, RuntimeException dilempar dengan pesan actionable.
SAUTH_OIDC_ENABLED=truephp artisan bpm:sauth:migration # tambah kolom is_oidc ke oauth_clientssign_algorithm
TIP
NEW v0.3.2
Override algoritma signing khusus sauth-server. Ketika di-set, mengalahkan SAUTH_ALGO untuk token signing (SignerResolver) dan key generation (bpm:sauth:keygen, bpm:sauth:init). SAUTH_ALGO tetap menjadi shared fallback yang dibaca kedua library.
Urutan resolusi:
| Konteks | Urutan |
|---|---|
| Token signing | 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 |
Kapan digunakan: saat gateway ingin beralih algoritma signing (misalnya ke ES256) tanpa mengubah SAUTH_ALGO yang juga dibaca resource server — resource server terus membaca SAUTH_ALGO sampai siap diperbarui secara independen.
SAUTH_SIGN_ALGO=ES256 # gateway sign dengan ES256
SAUTH_ALGO=RS256 # resource server masih RS256 sampai diperbaruiSetelah semua resource server siap: pindahkan ke SAUTH_ALGO=ES256 dan hapus SAUTH_SIGN_ALGO.
apikey_sign_algorithm
TIP
NEW v0.3.3
Override algoritma signing khusus untuk API key JWT yang diterbitkan ApiKeyIssuer. Tidak memengaruhi token Passport reguler (user token, M2M token, token exchange).
Urutan resolusi:
| Konteks | Urutan |
|---|---|
| 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) |
Ketika tidak di-set (null), fallback ke chain reguler — perilaku identik dengan v0.3.2. Berguna ketika partner perlu memverifikasi API key JWT secara langsung (tanpa sauth-client) dengan algoritma atau key rotation yang independen dari key utama gateway.
SAUTH_APIKEY_SIGN_ALGO=ES256 # API key JWT sign dengan ES256
SAUTH_SIGN_ALGO=RS256 # token Passport reguler tetap RS256Gunakan bersama SAUTH_APIKEY_PRIVATE_KEY_FILE untuk key pair yang sepenuhnya terpisah.
apikey_private_key_file
TIP
NEW v0.3.3
Override path private key khusus untuk API key JWT yang diterbitkan ApiKeyIssuer. Path bersifat relatif terhadap storage_path().
Urutan resolusi:
| Konteks | Urutan |
|---|---|
| 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) |
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=apikey
# → storage/keys/apikey-private.key + storage/keys/apikey-public.keyDistribusikan apikey-public.key ke partner sebagai kunci verifikasi — terpisah dari public key utama gateway. Deteksi mismatch key/algo via KeyLoader aktif untuk key ini juga.
algorithm
TIP
EdDSA ditambahkan di v0.3.1 | SAUTH_SIGN_ALGO override ditambahkan di v0.3.2
Algoritma signing JWT shared antara sauth-server dan sauth-client. Harus cocok dengan konfigurasi di sauth-client pada setiap consuming service — ketidakcocokan menyebabkan signature verification gagal di semua services.
Untuk mengubah algoritma gateway secara independen tanpa memperbarui resource server sekaligus, gunakan sign_algorithm (SAUTH_SIGN_ALGO) — lihat sign_algorithm.
Values:
'RS256'— RSA + SHA-256 (default, paling kompatibel)'RS384'— RSA + SHA-384'RS512'— RSA + SHA-512'ES256'— ECDSA + SHA-256'ES384'— ECDSA + SHA-384'ES512'— ECDSA + SHA-512'EdDSA'— Ed25519 (key format base64, bukan PEM)
Use Case: Ganti ke ES256/ES384 untuk key size lebih kecil dan signing lebih cepat. EdDSA untuk key terkecil dengan performa terbaik. Pastikan semua services dan kedua gateways menggunakan algoritma yang sama sebelum mengganti.
Catatan EdDSA: key format berbeda dari RSA/EC — base64 single-line, bukan PEM. KeyLoader mendeteksi format otomatis dan melempar RuntimeException jika ada mismatch antara format file dan SAUTH_ALGO. Gunakan bpm:sauth:keygen EdDSA untuk generate key yang benar.
Core Concepts
Token Claim Structure
Setiap JWT yang diterbitkan library ini mengandung kumpulan claim standar berikut. Nama claim ini adalah shared contract antara sauth-server, sauth-client, dan sauth-frontend — jangan ubah tanpa memperbarui ketiganya.
| Claim | Type | Description |
|---|---|---|
iss | string | Issuer — 1p token: kode gateway (gwa atau gwc); 3p token + API key JWT: full URL gateway (APP_URL) sesuai RFC 9068 |
aud | string | Target service code (srf, pnr, pel, dll.) |
iat | int | Issued-at timestamp — ada di semua token |
exp | int | Expiry timestamp — ada di semua token kecuali API key JWT; API key JWT tidak ada expiry, revocation via jti blacklist |
fp | string | Party marker — '1p' untuk first-party token, '3p' untuk third-party token; ada di semua token |
sid | string | User ID (user token), client_id (M2M token), atau app name (API key JWT) |
snm | string | Display name user — tidak ada di M2M token dan API key JWT |
fet | string|array | Permissions user — "wm" bypass semua checks; array = daftar atomic permissions; tidak pernah ada di M2M token |
scope | string | Space-separated scopes — ada di 3p user token, 3p M2M token, dan API key JWT; tidak ada di 1p token |
jti | string | UUID — hanya ada di API key JWT; digunakan sebagai blacklist key di setiap resource server |
act | object|null | RFC 8693 actor — {sub: calling_client_id}; hanya ada di delegated token |
Perbedaan claim berdasarkan tipe token:
| Token type | iss | fp | sid | snm | fet | scope | jti | exp |
|---|---|---|---|---|---|---|---|---|
| 1p user token | issuer_code | 1p | user ID | ✓ | ✓ | tidak ada | tidak ada | ✓ |
| 3p user token | APP_URL | 3p | user ID | ✓ | ✓ | ✓ | tidak ada | ✓ |
| 1p M2M token | issuer_code | 1p | client_id | tidak ada | tidak ada | tidak ada | tidak ada | ✓ |
| 3p M2M token | APP_URL | 3p | client_id | tidak ada | tidak ada | ✓ | tidak ada | ✓ |
| API key JWT | APP_URL | 3p | app name | tidak ada | tidak ada | ✓ | ✓ | tidak ada |
fp adalah penanda eksplisit party type — '1p' untuk first-party, '3p' untuk third-party. snm tetap membedakan user actor dari system actor (M2M). jti + tidak ada exp adalah penanda API key JWT.
1st-Party vs 3rd-Party Clients
Perbedaan ini menentukan claim apa yang di-embed dan middleware mana yang digunakan untuk proteksi route.
First-party (is_first_party = true) — dimiliki tim gateway. Frontend GWA/GWC dan semua internal services. Dipercaya tanpa consent screen; menggunakan fet-based auth via sauth.gate.
Third-party — developer atau partner eksternal. Melalui OAuth2 consent screen dan scope-based authorization. M2M token mereka membawa scope, bukan fet.
is_first_party adalah kolom custom di tabel oauth_clients — Passport tidak punya konsep ini secara native. Library menambahkannya dan ClientRegistrar yang mengelolanya.
service_code — kolom baru di v0.3.1. Kolom nullable unique di oauth_clients yang mengidentifikasi service code 1p resource server. ServiceTokenIssuer memvalidasi target permintaan ticketbooth terhadap kolom ini sebelum menerbitkan token — target tanpa service_code terdaftar akan ditolak. Lihat Deployment Order v0.3.1.
Grant Types
| Grant | Use Case |
|---|---|
authorization_code + PKCE | Login user via GWA atau GWC |
client_credentials | M2M — service-to-service, selalu diterbitkan GWA |
token_exchange (RFC 8693) | Service chaining yang mempertahankan context user asli |
Token exchange dinonaktifkan secara default (SAUTH_TOKEN_EXCHANGE_ENABLED=false). Hanya relevan di GWA — GWC bukan token exchange authority.
client_credentials — parameter resource (v0.3.7)
1p M2M caller dapat menyertakan parameter resource untuk menentukan aud di JWT yang diterbitkan. Tanpa parameter ini, aud jatuh ke sauth-server.default_audience.
POST /oauth/token
grant_type=client_credentials
&client_id=<client-id>
&client_secret=<client-secret>
&resource=pnrBerlaku hanya untuk 1p M2M token (is_first_party = true). Penamaan resource konsisten dengan TokenExchangeGrant. Caller yang tidak mengirim resource tidak terpengaruh.
Key Management
TIP
bpm:sauth:keygen dan KeyLoader ditambahkan di v0.3.1
Setiap gateway generate dan memiliki key pair sendiri — private key GWA dan GWC sepenuhnya terpisah. Jika satu gateway dikompromikan, hanya token dari gateway tersebut yang terekspos.
- Private key — hanya ada di gateway masing-masing, tidak pernah dibagikan
- Public key — didistribusikan ke semua services, disimpan di
config/sauth-client.phpdi bawahtrusted_issuers - Claim
isspada token masuk memberitahusauth-clientpublic key mana yang digunakan untuk verifikasi
Generate key pair dengan bpm:sauth:keygen:
php artisan bpm:sauth:keygen # algo dari SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 (v0.3.2)
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 dengan konfirmasi prompt| Algorithm | Format key |
|---|---|
| RS256/384/512 | PEM (-----BEGIN RSA PRIVATE KEY-----) |
| ES256/384/512 | PEM (-----BEGIN EC PRIVATE KEY-----) |
| EdDSA | Base64 single-line (bukan PEM) |
Deteksi mismatch key/algo: KeyLoader mendeteksi ketidakcocokan format file dengan SAUTH_ALGO sebelum operasi kriptografi apapun dan 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 EdDSACatatan .gitignore: Laravel hanya mengcover /storage/*.key by default. Jika key disimpan di subdirektori (--path=keys), tambahkan /storage/keys/*.key ke .gitignore. bpm:sauth:keygen mencetak warning otomatis saat --path non-root.
Artisan Commands
bpm:sauth:init
TIP
Updated v0.3.1: flag --algo, --path, --file-prefix ditambahkan; tidak lagi memanggil passport:keys Updated v0.3.2: resolusi algo tanpa --algo kini: SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256
Inisialisasi sauth-server dalam satu langkah: publish Passport migrations, publish sauth migrations (is_first_party, service_code, sauth_jti_tokens), jalankan migrate, dan generate OAuth key pair.
Signature:
php artisan bpm:sauth:init [--force] [--algo=] [--path=] [--file-prefix=]Options:
| Option | Description |
|---|---|
--force | Regenerate OAuth keys meskipun sudah ada |
--algo= | Algoritma key generation — default: SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 |
--path= | Direktori output key relatif ke storage_path() — default: storage root |
--file-prefix= | Prefix nama file key — default: oauth |
Command mengecek setiap langkah sebelum dijalankan — aman dijalankan berulang kali (idempotent). Langkah yang dijalankan secara berurutan:
- Publish Passport migrations →
migrate(dilewati jika tabeloauth_clientssudah ada) - Delegasi ke
bpm:sauth:migration— menanganiis_first_party,service_code, dansauth_jti_tokensmigrations (masing-masing dilewati jika sudah ada) - Generate key pair via
bpm:sauth:keygen(dilewati jika file sudah ada kecuali--forcediberikan)
php artisan bpm:sauth:init # setup pertama kali — algo dari config
php artisan bpm:sauth:init --force # regenerate OAuth keys (rotasi key)
php artisan bpm:sauth:init --algo=ES256 # generate EC keys
php artisan bpm:sauth:init --algo=EdDSA --path=keys --file-prefix=gwabpm:sauth:migration
TIP
NEW v0.3.1
Publish dan jalankan tiga sauth migrations secara idempotent — tanpa keygen. Gunakan command ini saat upgrade rolling (tidak perlu generate ulang key).
Signature:
php artisan bpm:sauth:migrationLangkah-langkah (masing-masing dengan cek skip):
is_first_partycolumn — fresh install: publish stub yang menambahkan keduanya (is_first_party+service_code) sekaligus. Dilewati jika kolom sudah ada.service_codecolumn — upgrade path: publish stub yang menambahkanservice_codesaja jikais_first_partysudah ada tapiservice_codebelum. Dilewati otomatis pada fresh install (sudah ditangani langkah 1).is_oidccolumn — publish stubadd_is_oidc_to_oauth_clients. Dilewati jika kolom sudah ada.sauth_jti_tokenstable — publish JTI migration. Dilewati jika tabel sudah ada.- Jalankan
migrate --forcesekali jika ada yang baru dipublish.
# Gunakan saat upgrade tanpa keygen
php artisan bpm:sauth:migrationbpm:sauth:keygen
TIP
NEW v0.3.1 | Updated v0.3.2: argumen algo kini opsional — resolusi: arg → SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256
Generate key pair (private + public) untuk algoritma yang dipilih.
Signature:
php artisan bpm:sauth:keygen [algo] [--path=] [--file-prefix=] [--force]Arguments:
| Argument | Description |
|---|---|
algo | (Opsional) Algoritma — RS256, RS384, RS512, ES256, ES384, ES512, EdDSA. Jika tidak diisi, resolusi: SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 |
Options:
| Option | Default | Description |
|---|---|---|
--path= | storage root | Direktori output relatif ke storage_path() |
--file-prefix= | oauth | Prefix nama file — menghasilkan {prefix}-private.key dan {prefix}-public.key |
--force | - | Overwrite file yang sudah ada (dengan konfirmasi prompt) |
Output defaults:
storage/oauth-private.keystorage/oauth-public.key
php artisan bpm:sauth:keygen RS256 # RSA 4096-bit
php artisan bpm:sauth:keygen ES256 --path=keys # EC prime256v1, ke storage/keys/
php artisan bpm:sauth:keygen EdDSA --file-prefix=gwa # Ed25519, ke storage/gwa-private.key
php artisan bpm:sauth:keygen RS256 --force # overwrite dengan konfirmasiSetelah generate, command mengingatkan untuk mengupdate SAUTH_PRIVATE_KEY_FILE di .env. Jika --path non-root, mencetak warning .gitignore.
bpm:sauth:client
TIP
Updated v0.3.4: flag --oidc ditambahkan; validasi kombinasi flag; prompt interaktif untuk --user
Buat OAuth client baru untuk ekosistem sauth.
Signature:
php artisan bpm:sauth:client [--first] [--user] [--oidc]Options:
| Option | Description |
|---|---|
--first | Tandai sebagai first-party — skip consent screen, is_first_party=true, fet-based auth; prompt service code |
--user | Gunakan authorization_code + PKCE; tanpa flag ini default ke client_credentials |
--oidc | Tandai sebagai OIDC client (is_oidc=true) — menerbitkan id_token; wajib dipakai bersama --user |
Kombinasi flag:
| Kombinasi | Perilaku |
|---|---|
| (tanpa flag) | 3p M2M — client_credentials, scope-based |
--first | 1p M2M — client_credentials, fet-based; prompt service code |
--user | 3p user — authorization_code + PKCE; prompt "Issue OIDC id_token?" |
--first --user | 1p user — frontend GWA/GWC; prompt service code |
--user --oidc | 3p OIDC user — authorization_code + PKCE + id_token; skip prompt |
--oidc --first | Hard fail — OIDC dan first-party kontradiksi |
--oidc (tanpa --user) | Hard fail — id_token hanya ada di authorization_code flow |
Saat is_oidc=true dan SAUTH_OIDC_ENABLED=false, client tetap dibuat dengan peringatan. Command prompt untuk nama client, redirect URI jika --user, dan service code jika --first (wajib). Secret hanya tersedia sekali, simpan segera.
bpm:sauth:apikey
Kelola API key JWT — long-lived signed JWT untuk partner machine integration yang stabil. Tidak ada OAuth flow, tidak ada expiry, revocation via jti blacklist.
bpm:sauth:apikey issue
php artisan bpm:sauth:apikey issue {app} {aud} [--scopes=] [--issuer=]| Argument/Option | Description |
|---|---|
{app} | Nama integrasi (menjadi sid di JWT) — contoh: acme-erp |
{aud} | Kode target service — contoh: srf |
--scopes | Space-separated scopes — contoh: "srf.read srf.write" |
--issuer | Audit trail — siapa yang menerbitkan; default cli |
Mendelegasikan ke ApiKeyIssuer::issue(). JWT ditampilkan sekali ke stdout dengan peringatan untuk menyimpannya segera — tidak bisa diambil kembali tanpa webmaster decrypt endpoint.
php artisan bpm:sauth:apikey issue acme-erp srf --scopes="srf.read srf.write" --issuer=adminbpm:sauth:apikey list
php artisan bpm:sauth:apikey listMenampilkan tabel dengan kolom: jti, app, aud, issuer, created_at, revoked_at. Berguna untuk audit dan menemukan jti sebelum melakukan revoke.
bpm:sauth:apikey revoke
php artisan bpm:sauth:apikey revoke {jti} [--reason=]| Argument/Option | Description |
|---|---|
{jti} | UUID jti dari API key yang akan dicabut |
--reason | Alasan pencabutan untuk audit trail |
Menandai revoked_at di sauth_jti_tokens dan memfire JtiTokenRevoked event. Tidak memblokir secara otomatis di resource server — setelah revoke, jalankan di setiap resource server yang menerima key tersebut:
# Jalankan di setiap resource server (sauth-client command)
php artisan bpm:sauth:jti block {jti} --reason="Compromised"API Reference
MicroserviceTokenClaimsProviderInterface
interface MicroserviceTokenClaimsProviderInterface
{
public function getClaimsForUser(?Authenticatable $user): array;
}Contract yang diimplementasikan oleh setiap gateway. Di v0.2.0+, dipanggil library hanya untuk user token (1p dan 3p). 1p M2M token tidak memanggil interface ini — library set sid = client_id langsung dan tidak menyertakan fet atau snm. 3p M2M token dan API key JWT juga tidak memanggil interface ini.
Namespace: Bpmlib\SauthServer\Contracts\MicroserviceTokenClaimsProviderInterface
getClaimsForUser()
public function getClaimsForUser(?Authenticatable $user): arrayKembalikan claims yang akan di-embed ke access token.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$user | Authenticatable|null | - | User yang login; null saat M2M (client_credentials) flow |
Returns: array{sid: string, snm: string|null, fet: string|list<string>}
| Key | Type | Description |
|---|---|---|
sid | string | User ID — hanya dibaca untuk user token; untuk M2M (jika $user === null) nilai ini diabaikan |
snm | string|null | Display name user |
fet | string|array | Permissions user — "wm" bypass semua checks; array = daftar atomic permissions |
Catatan (v0.2.0+): Untuk 1p M2M (client_credentials), getClaimsForUser(null) tidak dipanggil sama sekali. Return value untuk $user === null hanya diperlukan agar interface tetap terpenuhi — tulis implementasi paling minimal yang valid, misalnya return ['sid' => '', 'snm' => null].
FetchesClientToken
Namespace: Bpmlib\SauthServer\Concerns\FetchesClientToken
Trait untuk worker/job base class yang memanggil service lain menggunakan M2M token. Menangani siklus fetch → cache → bypass secara otomatis.
Baca dari config('sauth-server.client_credentials'): token_url, client_id, client_secret. Cache key: sauth_client_token_{client_id}_{audience}. TTL = 80% dari expires_in pada token response (real mode) atau 720 detik (bypass mode).
Protected Methods
clientToken()
protected function clientToken(string $audience = ''): stringKembalikan M2M access token yang valid untuk audience yang dituju.
- Real mode — fetch dari GWA jika cache kosong; cache 80% TTL server
- Bypass mode (
SAUTH_BYPASS=true+app()->isLocal()) — JWT palsu dibuat lokal, tidak ada network call
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$audience | string | '' | Kode audience target service ('srf', 'pnr', dll.). Default ke sauth-server.default_audience jika kosong. |
Returns: string — Bearer token siap pakai
clientToken('srf') dan clientToken('pnr') di-cache secara terpisah — keduanya bisa dipakai bersama dalam satu job tanpa konflik.
forgetClientToken()
protected function forgetClientToken(string $audience = ''): voidHapus token dari cache. Panggil setelah menerima 401 dari downstream service agar token baru di-fetch pada request berikutnya.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$audience | string | '' | Harus cocok dengan audience yang dipakai saat clientToken() dipanggil. |
ClientRegistrar
TIP
Updated v0.3.4: isOidc parameter ditambahkan ke create() dan update()
Namespace: Bpmlib\SauthServer\Services\ClientRegistrar
Service untuk manajemen OAuth client lifecycle — digunakan oleh bpm:sauth:client command dan gateway controller. Logic terpusat di sini, bukan di command atau controller masing-masing.
Auto-bound sebagai singleton oleh service provider. Inject via constructor DI.
Contains:
create()
public function create(
string $name,
bool $isFirstParty = false,
bool $isUserClient = false,
string $redirectUri = '',
?string $serviceCode = null,
bool $isOidc = false,
): ClientBuat OAuth client baru.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$name | string | - | Nama client |
$isFirstParty | bool | false | Tandai sebagai first-party (is_first_party = true) |
$isUserClient | bool | false | true = authorization_code + PKCE; false = client_credentials |
$redirectUri | string | '' | Redirect URI — wajib jika $isUserClient = true |
$serviceCode | string|null | null | Service code untuk 1p resource server ('srf', 'pnr', dll.); null atau '' = tidak diset |
$isOidc | bool | false | Tandai sebagai OIDC client (is_oidc=true); hanya bermakna untuk 3p authorization_code client |
Returns: Laravel\Passport\Client — plainSecret tersedia di instance ini. Secret tidak bisa diambil lagi setelah ini.
update()
public function update(
Client $client,
string $name,
?string $redirectUri = null,
?bool $isFirstParty = null,
?string $serviceCode = null,
?bool $isOidc = null,
): ClientUpdate nama, redirect URI, flag first-party, atau service code. Tidak mengubah secret.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$client | Client | - | Client yang akan diupdate |
$name | string | - | Nama baru |
$redirectUri | string|null | null | Redirect URI baru; null = tidak diubah |
$isFirstParty | bool|null | null | Flag first-party baru; null = tidak diubah |
$serviceCode | string|null | null | null = tidak diubah; '' = set null di DB (hapus); non-empty = set nilai baru |
$isOidc | bool|null | null | null = tidak diubah; true/false = set nilai baru |
Returns: Laravel\Passport\Client — fresh instance dari database
delete()
public function delete(Client $client): voidRevoke semua token aktif dan hapus record oauth_clients.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$client | Client | - | Client yang akan dihapus |
rotateSecret()
public function rotateSecret(Client $client): ClientGenerate client secret baru. Caller (controller) bertanggung jawab untuk audit logging.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$client | Client | - | Client yang secret-nya akan dirotasi |
Returns: Laravel\Passport\Client — plainSecret tersedia di instance ini. Secret tidak bisa diambil lagi setelah ini.
list()
public function list(?bool $firstParty = null): \Illuminate\Database\Eloquent\CollectionKembalikan semua client aktif (tidak ter-soft-delete). Soft-deleted clients dikecualikan secara otomatis karena Passport Client model menggunakan SoftDeletes.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$firstParty | bool|null | null | true = first-party saja; false = third-party saja; null = semua |
Returns: Illuminate\Database\Eloquent\Collection<int, Laravel\Passport\Client>
ServiceTokenIssuer
TIP
Updated v0.3.1: validasi target terhadap service_code sebelum issuance
Namespace: Bpmlib\SauthServer\Services\ServiceTokenIssuer
Service untuk menerbitkan JWT sesi-ke-service tanpa melalui Passport grant flow — tidak ada DB write, tidak ada refresh token. Digunakan oleh endpoint ticketbooth (POST /api/sauth/token) di GWA dan GWC. Auto-bound sebagai singleton oleh service provider. Inject via constructor DI.
issue()
public function issue(Authenticatable $user, string $targetService): arrayTerbitkan JWT bertarget service tertentu untuk session user yang sedang login.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$user | Authenticatable | — | User yang sedang login (dari $request->user()) |
$targetService | string | — | Kode service target atau client ID — harus terdaftar sebagai service_code di oauth_clients |
Returns: array{access: string, expires_in: int}
| Key | Type | Description |
|---|---|---|
access | string | Signed JWT siap digunakan sebagai Bearer token |
expires_in | int | TTL token dalam detik — sama dengan sauth-server.token_ttl.access (default 900) |
Throws:
RuntimeException— jikasauth-server.issuer_codebelum dikonfigurasi\InvalidArgumentException— jika$targetServicetidak ditemukan dioauth_clientssebagaiservice_codeatauiddari 1p client yang aktif
Validasi target (v0.3.1+): sebelum membangun JWT, query ke oauth_clients untuk mencari 1p client dengan service_code = $targetService atau id = $targetService. Jika tidak ditemukan → InvalidArgumentException. Gateway controller harus catch dan return 422. aud di JWT selalu service_code dari record yang ditemukan — tidak pernah client ID raw.
Claims yang di-embed:
| Claim | Nilai |
|---|---|
iss | config('sauth-server.issuer_code') — kode gateway (selalu 1p) |
aud | service_code dari client yang ditemukan di DB |
iat / nbf / exp | Timestamps standar JWT |
fp | '1p' — ticketbooth selalu first-party |
sid | Dari ClaimsProvider::getClaimsForUser($user)['sid'] |
snm | Dari ClaimsProvider — tidak ada di token jika null |
fet | Dari ClaimsProvider — permissions atau role code user |
Token ini tidak ditulis ke tabel oauth_access_tokens — berbeda dari token yang diterbitkan melalui Passport grant flow. Revocation tidak berlaku; session gateway adalah mekanisme logout.
KeyLoader
TIP
NEW v0.3.1
Namespace: Bpmlib\SauthServer\Support\KeyLoader
Helper untuk memuat key pair lcobucci/jwt dengan format-aware factory dan deteksi mismatch key/algo sebelum operasi kriptografi apapun.
final class KeyLoader
{
public static function privateKey(string $path, string $algo): InMemory;
public static function publicKey(string $path, string $algo): InMemory;
}privateKey() / publicKey()
public static function privateKey(string $path, string $algo): InMemory
public static function publicKey(string $path, string $algo): InMemoryBaca key dari file dan kembalikan instance lcobucci/jwt InMemory yang sesuai.
Parameters
| Name | Type | Description |
|---|---|---|
$path | string | Path relatif ke storage_path() — contoh: 'oauth-private.key' atau 'keys/gwa-private.key' |
$algo | string | Algoritma — 'RS256', 'ES256', 'EdDSA', dll. |
Returns: Lcobucci\JWT\Signer\Key\InMemory
- EdDSA (
$algo = 'EDDSA') →InMemory::base64Encoded($contents) - RS*/ES* →
InMemory::file(storage_path($path))
Throws: RuntimeException untuk dua kondisi:
- File tidak ditemukan
- Mismatch format: PEM file +
SAUTH_ALGO=EdDSA, atau base64 file +SAUTH_ALGO=RS256/ES256/dll.
Pesan error menyertakan command bpm:sauth:keygen yang harus dijalankan untuk memperbaiki.
ApiKeyIssuer
TIP
Updated v0.3.3: algo dan key terpisah via SAUTH_APIKEY_SIGN_ALGO / SAUTH_APIKEY_PRIVATE_KEY_FILE
Namespace: Bpmlib\SauthServer\Services\ApiKeyIssuer
Service untuk menerbitkan API key JWT — long-lived JWT bertanda tangan untuk partner machine integration. Logic terpusat di sini, digunakan oleh bpm:sauth:apikey issue command dan admin controller GWA. Auto-bound sebagai singleton oleh service provider. Inject via constructor DI.
issue()
public function issue(
string $app,
string $aud,
string $issuer,
array $scopes,
): stringTerbitkan API key JWT baru dan simpan audit row ke sauth_jti_tokens.
Parameters
| Name | Type | Description |
|---|---|---|
$app | string | Nama integrasi — menjadi sid di JWT (contoh: 'acme-erp') |
$aud | string | Kode target service (contoh: 'srf', 'pnr') |
$issuer | string | Audit trail — user SID atau 'cli' |
$scopes | array | Array scope string (contoh: ['srf.read', 'srf.write']) |
Returns: string — plaintext JWT, hanya tersedia saat ini. Disimpan encrypted di database; dapat diambil kembali hanya melalui webmaster decrypt endpoint di GWA.
Claims yang di-embed:
| Claim | Nilai |
|---|---|
iss | config('app.url') — full URL gateway; API key JWT selalu 3p (RFC 9068) |
aud | $aud |
fp | '3p' — API key JWT selalu third-party |
sid | $app |
jti | UUID baru — digunakan sebagai blacklist key di resource server |
scope | implode(' ', $scopes) — space-separated |
iat | Timestamp saat issuance |
exp | Tidak ada — API key JWT tidak punya expiry |
Algoritma signing (v0.3.3+): Fallback chain SAUTH_APIKEY_SIGN_ALGO → SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256. Gunakan SAUTH_APIKEY_SIGN_ALGO dan SAUTH_APIKEY_PRIVATE_KEY_FILE untuk key pair yang independen dari token Passport reguler. Lihat apikey_sign_algorithm dan apikey_private_key_file.
Throws: RuntimeException jika app.url belum dikonfigurasi, atau jika format key file tidak cocok dengan algoritma yang digunakan.
SauthJtiToken
Namespace: Bpmlib\SauthServer\Models\SauthJtiToken
Eloquent model untuk tabel sauth_jti_tokens — audit record setiap API key JWT yang diterbitkan. Primary key UUID (id = jti di JWT).
Fillable: id, app, aud, issuer, encrypted_scopes, encrypted_token, revoke_reason, revoked_at
Cast: revoked_at → datetime
revoke()
public function revoke(?string $reason = null): voidTandai API key sebagai dicabut dan fire JtiTokenRevoked event.
| Parameter | Type | Description |
|---|---|---|
$reason | string|null | Alasan pencabutan untuk audit trail |
Mengupdate revoked_at = now() dan revoke_reason. Setelah memanggil ini, daftarkan listener untuk JtiTokenRevoked di GWA untuk propagasi otomatis ke resource server, atau jalankan bpm:sauth:jti block {jti} secara manual di setiap resource server.
OidcClaimsProviderInterface
TIP
NEW v0.3.4
Namespace: Bpmlib\SauthServer\Contracts\OidcClaimsProviderInterface
Contract opsional yang diimplementasikan consuming app untuk menyediakan extra OIDC claims di /userinfo dan id_token. Jika tidak di-bind, /userinfo tetap berfungsi dan mengembalikan sub + name saja.
Bind di AppServiceProvider::register():
$this->app->bind(OidcClaimsProviderInterface::class, GwaOidcClaimsProvider::class);getOidcClaims()
public function getOidcClaims(Authenticatable $user): arrayKembalikan extra standard OIDC claims. Jangan sertakan sub atau name — keduanya sudah di-set dari token.
Returns: array<string, mixed> — contoh: ['email' => '...', 'email_verified' => true, 'picture' => '...']
IdTokenIssuer
TIP
NEW v0.3.4 | Updated v0.3.5: nonce otomatis diteruskan dari authorization request
Namespace: Bpmlib\SauthServer\Services\IdTokenIssuer
Service yang membangun dan menandatangani OIDC id_token. Dipanggil otomatis oleh OidcBearerTokenResponse — tidak perlu diinject secara manual kecuali ada kebutuhan khusus. Auto-bound sebagai singleton.
issue()
public function issue(AccessToken $token): stringBangun dan tandatangani id_token dari access token entity yang diberikan. Menggunakan key dan algoritma yang sama dengan access token (SAUTH_SIGN_ALGO → SAUTH_ALGO).
Claims yang di-embed:
| Claim | Nilai |
|---|---|
iss | APP_URL — selalu URL untuk OIDC |
sub | sid dari access token (user ID) |
aud | client_id OAuth client yang meminta |
iat / exp | Timestamps; exp = sekarang + token_ttl.access |
name | snm jika ada; dihilangkan untuk M2M |
nonce | Dari parameter nonce authorization request — otomatis diteruskan ke id_token (v0.3.5); consuming app harus simpan ke session['oidc_nonce'] untuk manual-approve path (lihat docs/oidc-gwa-req.md) |
Throws: RuntimeException jika APP_URL belum dikonfigurasi atau sid kosong.
JtiTokenRevoked
Namespace: Bpmlib\SauthServer\Events\JtiTokenRevoked
Event yang difire oleh SauthJtiToken::revoke(). Memungkinkan GWA mendaftarkan listener untuk propagasi revocation otomatis ke resource server.
class JtiTokenRevoked
{
public function __construct(public readonly SauthJtiToken $token) {}
}GWA dapat mendaftarkan listener di EventServiceProvider yang POST ke internal JTI block endpoint setiap resource server menggunakan M2M token. Library menyediakan event-nya; topologi (service mana yang perlu dinotifikasi) ada di codebase GWA, bukan di library ini.
Examples
Contains:
- 1. GWA ClaimsProvider
- 2. GWC ClaimsProvider
- 3. Registrasi Client via ClientRegistrar
- 4. FetchesClientToken di Job
- 5. Rotasi Secret Client
- 6. Token Exchange Request
- 7. List Clients via ClientRegistrar
- 8. Ticketbooth Controller dengan ServiceTokenIssuer
- 9. FetchesClientToken dengan audience dan dev bypass
- 10. API Key JWT — Issue, List, Revoke
- 11. Generate Key Pair dengan bpm:sauth:keygen
- 12. Registrasi 1p Resource Server Client dengan service_code
- 13. Setup OIDC Client
NEW v0.3.4
1. GWA ClaimsProvider
ClaimsProvider untuk internal users. is_webmaster menghasilkan 'wm' yang bypass semua permission checks di sauth.gate.
<?php
namespace App\Services;
use Bpmlib\SauthServer\Contracts\MicroserviceTokenClaimsProviderInterface;
use Illuminate\Contracts\Auth\Authenticatable;
class GwaClaimsProvider implements MicroserviceTokenClaimsProviderInterface
{
public function getClaimsForUser(?Authenticatable $user): array
{
if ($user === null) {
return ['sid' => '', 'snm' => null]; // tidak dipanggil untuk 1p M2M di v0.2.0+
}
return [
'sid' => (string) $user->id,
'snm' => $user->name,
'fet' => $user->is_webmaster ? 'wm' : $user->getFeatures(),
];
}
}Bind di AppServiceProvider::register():
$this->app->bind(MicroserviceTokenClaimsProviderInterface::class, GwaClaimsProvider::class);2. GWC ClaimsProvider
ClaimsProvider untuk external customers. fet berisi satu role code yang merefleksikan tipe akun — sauth.gate memperlakukannya identik dengan atomic permission GWA.
<?php
namespace App\Services;
use Bpmlib\SauthServer\Contracts\MicroserviceTokenClaimsProviderInterface;
use Illuminate\Contracts\Auth\Authenticatable;
class GwcClaimsProvider implements MicroserviceTokenClaimsProviderInterface
{
public function getClaimsForUser(?Authenticatable $user): array
{
if ($user === null) {
return ['sid' => '', 'snm' => null]; // tidak dipanggil untuk 1p M2M di v0.2.0+
}
return [
'sid' => (string) $user->id,
'snm' => $user->name,
'fet' => [$user->user_type], // 'psn', 'cpy', atau 'mit'
];
}
}3. Registrasi Client via ClientRegistrar
TIP
Updated v0.3.1: parameter serviceCode ditambahkan
ClientRegistrar diinject langsung via constructor DI — tidak perlu manual instantiation.
<?php
namespace App\Http\Controllers;
use Bpmlib\SauthServer\Services\ClientRegistrar;
use Illuminate\Http\Request;
class OAuthClientController extends Controller
{
public function __construct(private ClientRegistrar $registrar) {}
public function store(Request $request)
{
$client = $this->registrar->create(
name: $request->string('name'),
isFirstParty: $request->boolean('first_party'),
isUserClient: $request->boolean('user_client'),
redirectUri: $request->string('redirect_uri'),
serviceCode: $request->string('service_code') ?: null,
);
// plainSecret hanya tersedia di instance ini — simpan atau tampilkan sekarang
return response()->json([
'client_id' => $client->getKey(),
'client_secret' => $client->plainSecret,
'service_code' => $client->service_code,
]);
}
}4. FetchesClientToken di Job
Worker yang memanggil service lain menggunakan M2M token. Sertakan audience sebagai argumen — token untuk setiap service di-cache secara terpisah.
<?php
namespace App\Jobs;
use Bpmlib\SauthServer\Concerns\FetchesClientToken;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Http;
class SyncDataJob implements ShouldQueue
{
use Queueable, FetchesClientToken;
public function handle(): void
{
$response = Http::withToken($this->clientToken('srf'))
->get('https://srf.internal/api/data');
if ($response->status() === 401) {
$this->forgetClientToken('srf');
$this->release(5);
return;
}
$response->throw();
// proses $response->json()...
}
}5. Rotasi Secret Client
Secret baru hanya tersedia di instance yang dikembalikan rotateSecret(). Controller bertanggung jawab untuk audit logging.
public function rotateSecret(Request $request, ClientRegistrar $registrar): JsonResponse
{
$client = Client::findOrFail($request->route('id'));
$client = $registrar->rotateSecret($client);
activity()
->on($client)
->causedBy($request->user())
->log('client_secret_rotated');
return response()->json([
'client_secret' => $client->plainSecret,
]);
}6. Token Exchange Request
SRF menukar token user (aud=srf) ke token baru untuk memanggil PNR, mempertahankan identitas user asli. Token hasil exchange: sid = user asli, act.sub = SRF client ID, aud = pnr.
$response = Http::asForm()->post('https://gwa.example.com/oauth/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:token-exchange',
'client_id' => config('sauth-server.client_credentials.client_id'),
'client_secret' => config('sauth-server.client_credentials.client_secret'),
'subject_token' => $incomingUserToken, // token GWA aud=srf yang diterima SRF
'subject_token_type' => 'urn:ietf:params:oauth:token-type:access_token',
'resource' => 'pnr', // target service — wajib ada (RFC 9700)
]);
$delegatedToken = $response->json('access_token');
// Gunakan $delegatedToken sebagai Bearer token ke PNRToken exchange harus diaktifkan di GWA: SAUTH_TOKEN_EXCHANGE_ENABLED=true.
7. List Clients via ClientRegistrar
Ambil semua client aktif — berguna untuk halaman manajemen OAuth client di GWA.
public function index(ClientRegistrar $registrar): JsonResponse
{
// Semua client aktif
$all = $registrar->list();
// Hanya first-party
$firstParty = $registrar->list(firstParty: true);
// Hanya third-party
$thirdParty = $registrar->list(firstParty: false);
return response()->json($all);
}8. Ticketbooth Controller dengan ServiceTokenIssuer
TIP
Updated v0.3.1: catch InvalidArgumentException untuk target tidak terdaftar
Controller thin di gateway (GWA atau GWC) yang mengeluarkan JWT bertarget service tertentu untuk session user yang sedang login. ServiceTokenIssuer diinject via constructor DI — tidak perlu instantiasi manual.
<?php
namespace App\Http\Controllers;
use Bpmlib\SauthServer\Services\ServiceTokenIssuer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceTokenController extends Controller
{
public function __construct(private ServiceTokenIssuer $issuer) {}
public function issue(Request $request): JsonResponse
{
$request->validate(['target' => 'required|string']);
try {
$result = $this->issuer->issue(
$request->user(),
$request->input('target'),
);
} catch (\InvalidArgumentException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
return response()->json($result);
// { "access": "<signed JWT>", "expires_in": 900 }
}
}Daftarkan route di routes/api.php — wajib dijaga auth:sanctum:
Route::middleware('auth:sanctum')
->post('/api/sauth/token', [ServiceTokenController::class, 'issue']);Tidak ada refresh token. Session gateway (HttpOnly cookie) adalah long-lived credential. sauth-frontend memanggil endpoint ini ulang saat token kedaluwarsa atau saat menerima 401 — tidak ada flow refresh terpisah.
Response yang dihasilkan:
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"expires_in": 900
}sauth-frontend dalam session mode menyimpan access di cookie dengan TTL 80% dari expires_in.
9. FetchesClientToken dengan audience dan dev bypass
Job yang memanggil dua service berbeda dalam satu handle(). Token untuk masing-masing service di-cache secara independen. Dengan SAUTH_BYPASS=true di local, tidak ada GWA yang dibutuhkan.
<?php
namespace App\Jobs;
use Bpmlib\SauthServer\Concerns\FetchesClientToken;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Http;
class ExamSyncJob implements ShouldQueue
{
use Queueable, FetchesClientToken;
public function handle(): void
{
// Token srf dan pnr di-cache secara terpisah
$srfResponse = Http::withToken($this->clientToken('srf'))
->get('https://srf.internal/api/sessions');
if ($srfResponse->status() === 401) {
$this->forgetClientToken('srf');
$this->release(5);
return;
}
$pnrResponse = Http::withToken($this->clientToken('pnr'))
->post('https://pnr.internal/api/sync', $srfResponse->json());
if ($pnrResponse->status() === 401) {
$this->forgetClientToken('pnr');
$this->release(5);
return;
}
}
}Setup .env untuk local dev (service worker):
# Service worker tidak perlu GWA berjalan lokal
SAUTH_BYPASS=true
SAUTH_CLIENT_ID=srf-worker-local
SAUTH_ISSUER_CODE=gwa
# sauth-client di service penerima juga harus bypass
SAUTH_BYPASS=true
SAUTH_AUDIENCE=srf10. API Key JWT — Issue, List, Revoke
Workflow lengkap penerbitan, audit, dan pencabutan API key JWT untuk partner machine integration.
Menerbitkan via Artisan:
# Terbitkan API key untuk acme-erp mengakses SRF
php artisan bpm:sauth:apikey issue acme-erp srf --scopes="srf.read srf.write" --issuer=admin
# Output:
# API key JWT issued successfully.
# JTI: 550e8400-e29b-41d4-a716-446655440000
#
# Token (shown once — store immediately):
# eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...Menerbitkan via controller (programmatic):
<?php
namespace App\Http\Controllers\Admin;
use Bpmlib\SauthServer\Services\ApiKeyIssuer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ApiKeyController extends Controller
{
public function __construct(private ApiKeyIssuer $issuer) {}
public function store(Request $request): JsonResponse
{
$request->validate([
'app' => 'required|string',
'aud' => 'required|string',
'scopes' => 'required|array',
]);
$token = $this->issuer->issue(
app: $request->string('app'),
aud: $request->string('aud'),
issuer: (string) $request->user()->sid,
scopes: $request->array('scopes'),
);
// Token hanya tersedia sekali — kembalikan ke caller untuk disimpan
return response()->json(['token' => $token]);
}
}List semua API key:
php artisan bpm:sauth:apikey list
# +--------------------------------------+-----------+-----+--------+---------------------+------------+
# | jti | app | aud | issuer | created_at | revoked_at |
# +--------------------------------------+-----------+-----+--------+---------------------+------------+
# | 550e8400-e29b-41d4-a716-446655440000 | acme-erp | srf | admin | 2026-01-15 10:00:00 | |
# +--------------------------------------+-----------+-----+--------+---------------------+------------+Mencabut API key:
# 1. Tandai sebagai dicabut di GWA
php artisan bpm:sauth:apikey revoke 550e8400-e29b-41d4-a716-446655440000 --reason="Compromised"
# 2. Blokir di setiap resource server yang menerima key ini
php artisan bpm:sauth:jti block 550e8400-e29b-41d4-a716-446655440000 --reason="Compromised"JWT yang diterbitkan memiliki struktur:
{
"iss": "https://admin.example.com",
"aud": "srf",
"fp": "3p",
"sid": "acme-erp",
"jti": "550e8400-e29b-41d4-a716-446655440000",
"scope": "srf.read srf.write",
"iat": 1736935200
}Tidak ada exp — API key JWT bersifat permanen sampai dicabut. Resource server menggunakan jti blacklist (via sauth-client's JtiBlacklist) untuk revocation.
11. Generate Key Pair dengan bpm:sauth:keygen
TIP
NEW v0.3.1 | Updated v0.3.2: argumen algo opsional
Generate key pair untuk berbagai algoritma dan konfigurasi path.
# Tanpa argumen — algo dari SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 (v0.3.2)
php artisan bpm:sauth:keygen
# Default eksplisit — RSA 4096-bit, storage/oauth-private.key + oauth-public.key
php artisan bpm:sauth:keygen RS256
# EC key ke subdirektori
php artisan bpm:sauth:keygen ES256 --path=keys
# Output: storage/keys/oauth-private.key + storage/keys/oauth-public.key
# Warning: tambahkan /storage/keys/*.key ke .gitignore
# EdDSA dengan prefix custom
php artisan bpm:sauth:keygen EdDSA --file-prefix=gwa
# Output: storage/gwa-private.key (base64) + storage/gwa-public.key (base64)
# Overwrite dengan konfirmasi
php artisan bpm:sauth:keygen RS256 --forceSetelah generate, update .env:
# Untuk RS256/ES256 default
SAUTH_PRIVATE_KEY_FILE=oauth-private.key
SAUTH_ALGO=RS256
# Untuk ES256 --path=keys
SAUTH_PRIVATE_KEY_FILE=keys/oauth-private.key
SAUTH_ALGO=ES256
# Untuk EdDSA --file-prefix=gwa
SAUTH_PRIVATE_KEY_FILE=gwa-private.key
SAUTH_ALGO=EdDSA
# Override signing gateway tanpa mengubah SAUTH_ALGO (v0.3.2)
SAUTH_SIGN_ALGO=ES256
SAUTH_ALGO=RS25612. Registrasi 1p Resource Server Client dengan service_code
TIP
NEW v0.3.1
Setiap 1p resource server yang menjadi target ticketbooth harus didaftarkan dengan service_code. Tanpa ini, ServiceTokenIssuer akan menolak semua request ke service tersebut.
Via Artisan:
# Buat client M2M first-party untuk SRF — prompt untuk service code
php artisan bpm:sauth:client --first
# Client name: SRF Service
# Service code: srf
#
# Output:
# Client ID : 5
# Client Secret: xxxx
# Service Code : srfVia Tinker (populate existing client):
// Populate service_code pada client yang sudah ada
\Laravel\Passport\Client::find(5)->update(['service_code' => 'srf']);Via ClientRegistrar (programmatic):
// Buat client baru dengan service_code
$client = $registrar->create(
name: 'SRF Service',
isFirstParty: true,
serviceCode: 'srf',
);
// Update client yang sudah ada
$registrar->update($client, 'SRF Service', serviceCode: 'srf');
// Hapus service_code (client tidak lagi menjadi target ticketbooth)
$registrar->update($client, 'SRF Service', serviceCode: '');13. Setup OIDC Client
TIP
NEW v0.3.4
Setup lengkap OIDC: aktifkan fitur, daftarkan client, dan implementasikan OidcClaimsProviderInterface untuk extra claims.
1. Aktifkan OIDC dan jalankan migrasi:
# .env
SAUTH_OIDC_ENABLED=true
php artisan bpm:sauth:migration2. Buat OIDC client:
php artisan bpm:sauth:client --user --oidc
# Client name: Acme Dashboard
# Redirect URI: https://acme.example.com/callback
#
# Client ID : 7
# Client Secret : xxxx
# OIDC : yes3. Implementasikan OidcClaimsProviderInterface (opsional):
<?php
namespace App\Auth;
use Bpmlib\SauthServer\Contracts\OidcClaimsProviderInterface;
use Illuminate\Contracts\Auth\Authenticatable;
class GwaOidcClaimsProvider implements OidcClaimsProviderInterface
{
public function getOidcClaims(Authenticatable $user): array
{
return [
'email' => $user->email,
'email_verified' => $user->email_verified_at !== null,
];
}
}// AppServiceProvider::register()
$this->app->bind(OidcClaimsProviderInterface::class, GwaOidcClaimsProvider::class);4. Flow authorization_code dengan openid scope:
GET /oauth/authorize?client_id=7&scope=openid+profile&response_type=code&...
→ user login + consent
→ POST /oauth/token { code, ... }
→ { access_token: "...", id_token: "eyJ...", expires_in: 900 }id_token membawa: sub (user ID), name, email (dari provider), iss = APP_URL, aud = client_id.
Laravel Integration
Service Provider
Auto-registered via package discovery. Mendaftarkan:
- Custom
AccessTokenRepository— meng-injectiss,fp,sid,snm,fet,scope,actke setiap token sesuai tipe ClientRegistrar,ServiceTokenIssuer,ApiKeyIssuer,IdTokenIssuersebagai singleton- Artisan commands:
bpm:sauth:init,bpm:sauth:migration,bpm:sauth:keygen,bpm:sauth:client,bpm:sauth:apikey issue/list/revoke TokenExchangeGrantjikatoken_exchange.enabled = true- Saat
oidc.enabled = true: endpoint/.well-known/openid-configuration,/.well-known/jwks.json,/userinfo;OidcBearerTokenResponseuntukid_tokenissuance;OidcAuthCodeRepositoryuntuk nonce passthrough (v0.3.5); boot-time schema guard untuk kolomis_oidc
Published Assets
php artisan vendor:publish --tag=sauth-server-config
# → config/sauth-server.phpRequired Migrations
Migration di-ship bersama library sebagai stub dan di-publish otomatis oleh bpm:sauth:init atau bpm:sauth:migration ke database/migrations/. Tidak perlu menulis migration secara manual.
is_first_party+service_codecolumns — fresh install: satu stub menambahkan keduanya keoauth_clients. Upgrade dari ≤v0.3.0: dua stub terpisah (is_first_partysudah ada → hanyaservice_codeyang ditambahkan).sauth_jti_tokenstable — audit table untuk API key JWT; kolom:id(UUID = jti),app,aud,issuer,encrypted_scopes,encrypted_token,revoke_reason,revoked_atis_oidccolumn — kolom boolean dioauth_clientsuntuk menandai OIDC client; ditambahkan olehbpm:sauth:migrationsecara idempotent
Deployment Order v0.3.1
IMPORTANT
Ikuti urutan ini — deploy kode sebelum populate service_code menyebabkan semua ticketbooth request return 422.
# Langkah 1 — tambahkan kolom ke database
php artisan bpm:sauth:migration
# Langkah 2 — populate service_code pada semua 1p resource server client
# (via Tinker, migration data, atau bpm:sauth:client --first)
php artisan tinker
>>> \Laravel\Passport\Client::where('is_first_party', true)->whereNull('service_code')->get()
# Identify masing-masing client, lalu:
>>> \Laravel\Passport\Client::find($id)->update(['service_code' => 'srf'])
# Langkah 3 — deploy kode v0.3.1
# ServiceTokenIssuer validasi aktif setelah iniTidak ada perubahan wajib untuk:
- Resource server — tidak ada perubahan di sauth-client
- Gateway
ClaimsProvider— interface tidak berubah - Worker yang menggunakan
FetchesClientToken— tidak ada perubahan - Token yang sudah diterbitkan — tetap valid
Links
- Registry: bpmlib — Private Composer Repository