Sauth Client
Stateless JWT verification middleware dan guard untuk Laravel microservice.
Versi: 0.3.4 Changelog
TL;DR
Library ini menyediakan guard sauth dan 5 middleware alias untuk memverifikasi JWT dari gateway (GWA/GWC) di Laravel microservice — tanpa Passport, tanpa database untuk jalur OAuth biasa. Guard memvalidasi token per request dan mengisi auth()->user() dengan objek stateless MicroserviceUser.
Namespace:
use Bpmlib\SauthClient\Auth\MicroserviceUser;
use Bpmlib\SauthClient\Auth\MicroserviceTokenGuard;
use Bpmlib\SauthClient\Contracts\IssuerResolverInterface;
use Bpmlib\SauthClient\Resolvers\ConfigDrivenIssuerResolver;
use Bpmlib\SauthClient\Services\JtiBlacklist;
use Bpmlib\SauthClient\Middleware\ValidateMicroserviceToken; // sauth.gate
use Bpmlib\SauthClient\Middleware\RequireWebmaster; // sauth.wm
use Bpmlib\SauthClient\Middleware\DualAuthenticate; // sauth.dual
use Bpmlib\SauthClient\Middleware\CheckServiceScope; // sauth.scope
use Bpmlib\SauthClient\Middleware\ValidateM2MToken; // sauth.m2m
use Bpmlib\SauthClient\Exceptions\UnknownIssuerException;
use Bpmlib\SauthClient\Exceptions\TokenExpiredException;
use Bpmlib\SauthClient\Exceptions\InvalidTokenException;
use Bpmlib\SauthClient\Constants\ServiceCodes;Artisan Commands:
php artisan bpm:sauth:lookup [--type=fet] [--json] # reverse-index route berdasarkan auth key
php artisan bpm:sauth:jti {block|unblock|list|publish-migration} [jti] # JTI revocation
php artisan bpm:sauth:fake publish [--path=] # publish template fake-claims JSONKonfigurasi:
config/sauth-client.php— Lihat lengkap
Installation & Setup
Requirements
PHP: 8.4+
Composer Dependencies:
# Ditarik otomatis oleh bpmlib/sauth-client
firebase/php-jwt ^7.0Framework: Laravel 12.0+ atau 13.0+
Composer Install
composer require bpmlib/sauth-clientAuto-Discovery
SauthClientServiceProvider auto-registered via Laravel package discovery. Tidak perlu mendaftarkan secara manual.
Publish Commands
php artisan vendor:publish --tag=sauth-client-configMembuat config/sauth-client.php. Untuk konfigurasi lengkap, lihat Configuration.
Quick Start
// config/auth.php
'guards' => [
'api' => ['driver' => 'sauth'],
],
'defaults' => [
'guard' => 'api',
],// routes/api.php
use Illuminate\Support\Facades\Route;
Route::middleware('sauth.gate')->group(function () {
Route::get('/profile', ProfileController::class);
});
// Dengan permission check
Route::middleware('sauth.gate:report.view')->group(function () {
Route::get('/reports', ReportController::class);
});// app/Http/Controllers/ProfileController.php
use Bpmlib\SauthClient\Auth\MicroserviceUser;
class ProfileController extends Controller
{
public function __invoke()
{
/** @var MicroserviceUser $user */
$user = auth()->user();
return response()->json([
'id' => $user->isM2M() ? $user->clientId() : $user->userId(),
'name' => $user->name,
]);
}
}Env vars minimal:
SAUTH_AUDIENCE=srf
GWA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
GWC_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"Configuration
php artisan vendor:publish --tag=sauth-client-configIssuer Keys
TIP
trusted_issuer_url_map baru di v0.3.0 — diperlukan jika service menerima token 3p dari sauth-server v0.3+, yang menggunakan APP_URL sebagai nilai iss.
| Option | Env Var | Default | Description |
|---|---|---|---|
trusted_issuers.gwa | GWA_PUBLIC_KEY | null | Inline PEM public key untuk GWA |
trusted_issuers.gwc | GWC_PUBLIC_KEY | null | Inline PEM public key untuk GWC |
trusted_issuer_files.gwa | GWA_PUBLIC_KEY_FILE | null | Path file PEM relatif ke storage/; prioritas atas inline |
trusted_issuer_files.gwc | GWC_PUBLIC_KEY_FILE | null | Path file PEM relatif ke storage/; prioritas atas inline |
trusted_issuer_url_map.gwa | GWA_APP_URL | null | URL gateway GWA untuk token 3p dengan iss berbasis URL |
trusted_issuer_url_map.gwc | GWC_APP_URL | null | URL gateway GWC untuk token 3p dengan iss berbasis URL |
File key (*_FILE) lebih diprioritaskan dari inline key untuk issuer yang sama — cocok untuk rotasi key berbasis Docker/K8s secret tanpa mengubah .env.
Audience & Algoritma
TIP
issuer_algorithms baru di v0.3.3 — gunakan saat GWA dan GWC menggunakan algoritma tanda tangan yang berbeda secara bersamaan.
| Option | Env Var | Default | Description |
|---|---|---|---|
audience | SAUTH_AUDIENCE | null | Audience code service ini (mis. srf, pnr) — harus cocok dengan claim aud token |
algorithms[0] | SAUTH_ALGO | RS256 | Algoritma JWT global; fallback jika tidak ada per-issuer override |
issuer_algorithms.gwa | SAUTH_GWA_ALGO | null | Override algoritma khusus GWA; null = gunakan SAUTH_ALGO |
issuer_algorithms.gwc | SAUTH_GWC_ALGO | null | Override algoritma khusus GWC; null = gunakan SAUTH_ALGO |
API Key JWT
TIP
apikey_issuer_files dan apikey_issuer_algorithms baru di v0.3.3 — dipakai saat GWA menandatangani API key JWT dengan key atau algoritma yang berbeda dari token OAuth reguler.
| Option | Env Var | Default | Description |
|---|---|---|---|
apikey_issuer_files | APIKEY_PUBLIC_KEY_FILE | null | Path file public key khusus API key JWT; fallback ke trusted_issuer_files['gwa'] |
apikey_issuer_algorithms | SAUTH_APIKEY_ALGO | null | Algoritma khusus API key JWT; fallback ke issuer_algorithms['gwa'] → SAUTH_ALGO |
Hanya relevan jika service menerima API key JWT (token dengan claim jti). Token OAuth biasa tidak terpengaruh.
Bypass Lokal
TIP
SAUTH_BYPASS menjadi multi-value enum sejak v0.3.0 — menggantikan nilai boolean dengan kontrol granular per lapisan validasi.
Nilai SAUTH_BYPASS | Signature / Exp | Permission (fet) | Scope | Shape check |
|---|---|---|---|---|
false / none | Enforced | Enforced | Enforced | Enforced |
sig | Skipped | Enforced | Enforced | Enforced |
fet | Skipped | Skipped | Enforced | Enforced |
scope | Skipped | Enforced | Skipped | Enforced |
perm | Skipped | Skipped | Skipped | Enforced |
true / total | Skipped | Skipped | Skipped | Skipped |
Bypass hanya aktif di app()->isLocal(). Nilai apapun diabaikan di production.
WARNING
SAUTH_SCOPE_BYPASS deprecated sejak v0.3.0 — akan dihapus di v0.4. Gunakan SAUTH_BYPASS=scope (atau perm / total).
WARNING
SAUTH_JTI_BYPASS deprecated sejak v0.3.3 — gunakan SAUTH_JTI_BLACKLIST_BYPASS. Keduanya masih berfungsi sementara; lama sebagai fallback.
| Option | Env Var | Default | Description |
|---|---|---|---|
bypass | SAUTH_BYPASS | false | Bypass lokal — lihat tabel di atas |
scope_bypass (deprecated) | SAUTH_SCOPE_BYPASS | false | Stack di atas bypass via OR logic untuk sauth.scope saja |
jti_bypass | SAUTH_JTI_BLACKLIST_BYPASS | false | Skip JTI blacklist DB check di lokal; hanya efektif saat bypass=false |
Fake Identity
TIP
fake_claims_file baru di v0.3.0 — satu file JSON dengan profil per middleware, menggantikan banyak flat env vars. Publish template via bpm:sauth:fake publish. Lihat selengkapnya
| Option | Env Var | Default | Description |
|---|---|---|---|
fake_claims_file | SAUTH_FAKE_CLAIMS_FILE | null | Path JSON relatif ke storage/; prioritas atas flat env vars |
bypass_fake_user.sid | SAUTH_FAKE_SID | null | Fake user ID untuk sauth.gate; null = tetap butuh token |
bypass_fake_user.snm | SAUTH_FAKE_SNM | Dev User | Fake display name untuk sauth.gate |
bypass_fake_user.fet | SAUTH_FAKE_FET | wm | Fake permission untuk sauth.gate; wm = lolos semua permission check |
bypass_fake_m2m.sid | SAUTH_FAKE_M2M_SID | null | Fake client ID untuk sauth.m2m; shape 1p M2M |
bypass_fake_scope.sid | SAUTH_FAKE_SCOPE_SID | null | Fake client ID untuk pure sauth.scope route |
bypass_fake_scope.scope | SAUTH_FAKE_SCOPE_SCOPES | [] | Fake scope (space-separated) untuk sauth.scope |
Prioritas injeksi per request: JSON profile → flat env vars → 401 (jika sid null dan tidak ada token).
Core Concepts
Tipe Token
TIP
Claim fp tersedia sejak v0.3.0 — marker eksplisit '1p'/'3p' dari sauth-server v0.3+. Token pra-v0.3 tidak memiliki fp; guard menggunakan inferensi snm/scope sebagai fallback.
| Tipe Token | fp | snm | scope | jti | exp | Middleware |
|---|---|---|---|---|---|---|
| 1p user | '1p' | ✓ | — | — | ✓ | sauth.gate, sauth.dual |
| 3p user | '3p' | ✓ | ✓ | — | ✓ | sauth.gate + sauth.scope (keduanya wajib) |
| 1p M2M | '1p' | — | — | — | ✓ | sauth.gate (tanpa params), sauth.m2m |
| 3p M2M | '3p' | — | ✓ | — | ✓ | sauth.scope, sauth.m2m |
| API key JWT | '3p' | — | ✓ | ✓ | — | sauth.scope, sauth.m2m |
Gunakan isM2M() untuk membedakan user vs. mesin. Gunakan isFirstParty() / isThirdParty() untuk asal token — bukan sebagai pengganti isM2M().
Alur Validasi JWT
Dijalankan sekali per request oleh MicroserviceTokenGuard:
- Baca Bearer token dari header
Authorization peekIssuer()— decode payload JWT tanpa verifikasi signature untuk membaca nilai rawississuerResolver->resolvePublicKey($iss)— resolusi public key; throwsUnknownIssuerExceptionjika tidak dikenal. Token 3p denganissberbasis URL di-map ke kode pendek viatrusted_issuer_url_mapsebelum lookup key- Resolusi algoritma per issuer:
issuer_algorithms[$code]→SAUTH_ALGO→RS256
TIP
Sejak v0.3.2, guard mendeteksi mismatch format key vs. algoritma di langkah ini sebelum JWT::decode(). PEM + EdDSA, atau base64 Ed25519 + RS256, melempar RuntimeException dengan pesan actionable — menghasilkan HTTP 500 (bukan 401), karena ini adalah kesalahan konfigurasi operator.
JWT::decode()viafirebase/php-jwt— verifikasi signature + expiry (skipexpjika tidak ada)- Cek claim
audcocok dengansauth-client.audience— dilewati untuk token 3p (fp=3p) karena token 3p dibatasi olehscopeviasauth.scope, bukanaud; throwsInvalidTokenExceptionuntuk token 1p/pra-v0.3 jika tidak cocok.audberupa array (valid per RFC 7519) diterima dengan benar - Jika
jtiada: cek tabelsauth_blacklisted_jti_tokens(cached 5 menit) — throwsInvalidTokenExceptionjika blacklisted - Build dan return
MicroserviceUserdari claims yang telah tervalidasi
Hasil dicache di $resolvedUser — tidak ada decode ulang pada request yang sama.
Tabel Keputusan Middleware
| Middleware | 1p User | 3p User | 1p M2M | 3p M2M / API key JWT |
|---|---|---|---|---|
sauth.gate | Lolos jika fet cocok (atau wm) | Lolos jika fet cocok (atau wm) | Lolos jika tanpa params | 403 (tidak ada fet) |
sauth.wm | Lolos jika fet === 'wm' | Lolos jika fet === 'wm' | 403 | 403 |
sauth.dual | Lolos (session atau JWT) | Lolos (session atau JWT) | Lolos jika tanpa params | 403 |
sauth.scope | 403 (tidak ada scope) | Lolos jika scope cocok (OR) | 403 (tidak ada scope) | Lolos jika scope cocok (OR) |
sauth.m2m | 403 (snm ada) | 403 (snm ada) | Selalu lolos | Lolos jika semua scope ada (AND) |
Token 3p user memerlukan kedua sauth.gate (cek fet) dan sauth.scope (cek scope) secara AND. Route tersebut harus dipisah dari route 1p equivalen-nya.
Wildcard Permission Matching
Pattern dibangun dari entri fet milik user, dicocokkan ke permission yang diminta di middleware.
User fet: ['user.*'] Required: 'user.read' → cocok ✓ (luas ke sempit)
User fet: ['user.read'] Required: 'user.*' → tidak cocok ✗ (sempit ke luas)User dengan permission luas (user.*) lolos requirement sempit. Tidak berlaku sebaliknya.
Bypass Mode
Bypass hanya aktif di app()->isLocal(). Production tidak terpengaruh oleh nilai SAUTH_BYPASS apapun.
Saat bypass aktif dan ada Bearer token: token di-decode tanpa verifikasi signature; user diisi dari claims nyata. Saat tidak ada Bearer token: user diinjeksikan dari JSON profile (Lihat bpm:sauth:fake) atau flat env vars; jika sid null, request diabort 401.
Artisan Commands
bpm:sauth:lookup
Reverse-index semua route berdasarkan sauth middleware key. Setara dengan artisan route:list tapi di-scope ke permission/scope sauth.
Signature:
php artisan bpm:sauth:lookup [--type=fet] [--json]Options:
| Option | Default | Description |
|---|---|---|
--type | fet | fet — permission gate; scope — OAuth scope; wm — route webmaster; m2m — M2M scope |
--json | — | Emit JSON ke stdout, pipeable ke jq |
Contoh output (--type=fet):
Permission Routes Route Names
------------ ------ ----------------------------------------
(none) 1 api.health
psn 3 api.profile.show, api.profile.update, api.applications.store
report.view 2 api.reports.index, api.reports.show
user.* 2 admin.users.index, admin.users.show(none) = middleware hadir tanpa params — token terautentikasi apapun diterima. Route dengan sauth.gate:psn,cpy muncul di baris psn dan cpy.
bpm:sauth:jti
Manajemen JTI blacklist untuk revokasi API key JWT per resource server.
Signature:
php artisan bpm:sauth:jti {subcommand} [jti] [--reason=]Subcommands:
| Subcommand | Description |
|---|---|
publish-migration | Copy stub migration ke database/migrations/ |
block {jti} [--reason=] | Insert ke blacklist; request berikutnya ditolak (401) |
unblock {jti} | Hapus dari blacklist; flush cache entry secara otomatis |
list | Tampilkan semua JTI yang diblokir |
Tabel sauth_blacklisted_jti_tokens hanya perlu disetup jika service menerima API key JWT. Token OAuth biasa tidak terpengaruh. Setelah unblock, token mungkin masih lolos hingga 5 menit akibat TTL cache.
bpm:sauth:fake
Publish template JSON fake claims untuk dev lokal.
Signature:
php artisan bpm:sauth:fake publish [--path=]TIP
Flag --path baru di v0.3.1 — path kustom relatif ke storage/. Subdirektori dibuat otomatis. Command juga menambahkan entry ke .gitignore secara otomatis setelah publish.
Skip jika file sudah ada (delete dulu untuk reset). Setelah publish, set di .env:
SAUTH_FAKE_CLAIMS_FILE=sauth-fake-claims.jsonTemplate JSON yang dihasilkan:
{
"1p_user": {
"sid": "user-123",
"snm": "Dev User",
"fet": ["user.read", "report.view"],
"fp": "1p"
},
"1p_m2m": {
"sid": "srf-worker",
"fp": "1p"
},
"3p_m2m": {
"sid": "partner-app",
"scope": ["srf.read"],
"fp": "3p"
},
"3p_user": {
"sid": "user-123",
"snm": "Dev User",
"fet": ["user.read"],
"scope": ["srf.read"],
"fp": "3p"
}
}Mapping profil per middleware:
| Middleware | Kunci JSON |
|---|---|
sauth.gate, sauth.dual | 1p_user |
sauth.m2m | 1p_m2m |
sauth.scope | 3p_m2m |
3p_user ada di template untuk kelengkapan — tidak dibaca otomatis oleh middleware manapun.
API Reference
MicroserviceUser
Stateless Authenticatable. Tidak ada Eloquent, tidak ada DB. Dibangun dari JWT claims yang telah tervalidasi.
Namespace: Bpmlib\SauthClient\Auth\MicroserviceUser
Constructor:
public function __construct(
public readonly string $id, // sid claim
public readonly ?string $name, // snm claim — null untuk semua M2M
public readonly string|array|null $permissions, // fet claim — null untuk semua M2M
public readonly ?string $actor = null, // act.sub — calling client ID saat token exchange
public readonly ?array $scope = null, // parsed scope array — null untuk 1p token
public readonly ?string $fp = null, // 'fp' claim — '1p', '3p', atau null (pra-v0.3)
)Methods:
isM2M()
public function isM2M(): booltrue jika name === null — token diterbitkan via client_credentials. Gunakan ini (bukan isFirstParty()) untuk membedakan user vs. mesin.
userId()
public function userId(): stringReturns id untuk user token. Throws RuntimeException jika dipanggil pada M2M token.
clientId()
public function clientId(): stringReturns id untuk M2M token. Throws RuntimeException jika dipanggil pada user token.
isFirstParty() / isThirdParty()
public function isFirstParty(): bool
public function isThirdParty(): boolTIP
Sejak v0.3.0, kedua method ini membaca claim fp jika ada — termasuk untuk 1p user token. Sebelumnya isFirstParty() selalu false untuk user token. Fallback ke inferensi snm/scope untuk token pra-v0.3.
isFirstParty() — true jika fp === '1p'; fallback: isM2M() && scope === null
isThirdParty() — true jika fp === '3p'; fallback: isM2M() && scope !== null
hasScope()
public function hasScope(string ...$required): boolAND logic — semua scope yang disebutkan harus ada di claim scope token. Selalu false untuk 1p M2M (tidak punya claim scope).
IssuerResolverInterface
Namespace: Bpmlib\SauthClient\Contracts\IssuerResolverInterface
public function resolvePublicKey(string $issuer): string;Mengembalikan raw string public key untuk issuer yang diberikan. Throws UnknownIssuerException jika tidak dikenal.
ConfigDrivenIssuerResolver adalah implementasi default. Untuk resolusi key dinamis (mis. JWKS endpoint), bind implementasi kustom di AppServiceProvider::register():
$this->app->singleton(IssuerResolverInterface::class, MyJwksIssuerResolver::class);ConfigDrivenIssuerResolver
Namespace: Bpmlib\SauthClient\Resolvers\ConfigDrivenIssuerResolver
Implementasi default IssuerResolverInterface. Resolusi 3 langkah per issuer:
- File path —
trusted_issuer_files.{$issuer}(prioritas; path relatif kestorage/) - Inline value —
trusted_issuers.{$issuer}(fallback) - URL map — iterasi
trusted_issuer_url_map, cocokkan URL ke kode pendek, ulangi langkah 1 dan 2
TIP
Langkah 3 baru di v0.3.0 — diperlukan untuk menerima token 3p dari sauth-server v0.3+ yang menggunakan APP_URL sebagai iss. Konfigurasi selengkapnya.
JtiBlacklist
Namespace: Bpmlib\SauthClient\Services\JtiBlacklist
Singleton service untuk manajemen JTI blacklist. Bound otomatis oleh SauthClientServiceProvider — injectable via container.
public function block(string $jti, ?string $reason = null): void;
public function unblock(string $jti): void;
public function list(): Collection;
public function isBlocked(string $jti): bool; // cached 5 menitMicroserviceTokenGuard memanggil isBlocked() secara otomatis — tidak perlu dipanggil dari middleware atau controller. Gunakan block()/unblock() dari Artisan command atau HTTP endpoint internal yang diekspos oleh consuming app.
Middlewares
ValidateMicroserviceToken — sauth.gate
Validasi JWT + permission check opsional via varargs.
Route::middleware('sauth.gate') // token terautentikasi apapun
Route::middleware('sauth.gate:report.view') // satu permission
Route::middleware('sauth.gate:psn,cpy') // psn ATAU cpy — OR logicParameters:
| Name | Type | Default | Description |
|---|---|---|---|
...$permissionNeeded | string | — | Permission yang diperiksa via OR logic; kosong = terima semua token valid |
Token tidak valid → 401. fet === 'wm' (webmaster) → selalu lolos. Tanpa params → lolos semua token valid. Dengan params → OR logic antara entri fet user dan permission yang diminta. Gagal → 403.
RequireWebmaster — sauth.wm
Route::middleware('sauth.wm')Validasi JWT lalu wajibkan $user->permissions === 'wm' secara eksplisit — string, bukan array. fet: ['wm'] (array) → 403. Gunakan sauth.wm (bukan sauth.gate:wm) untuk route yang benar-benar membutuhkan level webmaster.
DualAuthenticate — sauth.dual
Route::middleware('sauth.dual') // session atau JWT tanpa permission check
Route::middleware('sauth.dual:psn,cpy') // jalur JWT: wajib psn atau cpyCek guard web terlebih dahulu — jika session user ada, langsung lolos. Jika tidak, delegasikan ke ValidateMicroserviceToken dengan forwarding permission params. Di jalur session, auth()->user() mengembalikan Eloquent model; di jalur JWT, mengembalikan MicroserviceUser. Controller pada route dual harus menangani keduanya — branch pada auth('web')->check().
CheckServiceScope — sauth.scope
Route::middleware('sauth.scope') // token apapun dengan claim scope
Route::middleware('sauth.scope:srf.read') // scope harus mengandung srf.read
Route::middleware('sauth.scope:srf.read,srf.write') // srf.read ATAU srf.write — OR logicExact string match — tanpa wildcard. OR logic antar params (berbeda dengan sauth.m2m yang AND). Token 1p (tidak punya scope) → 403. Token dengan scope present → lolos jika salah satu scope yang diminta ada.
ValidateM2MToken — sauth.m2m
Route::middleware('sauth.m2m') // token M2M apapun
Route::middleware('sauth.m2m:srf.read') // 1p selalu lolos; 3p wajib srf.read
Route::middleware('sauth.m2m:srf.read,srf.write') // 1p lolos; 3p wajib KEDUA scope (AND)User token (snm ada) → 403. 1p M2M → selalu lolos terlepas dari scope params (internal worker dipercaya di level issuer). 3p M2M / API key JWT → AND logic: semua scope yang disebutkan harus ada.
Exceptions
| Exception | Kondisi | HTTP |
|---|---|---|
UnknownIssuerException | iss tidak ada di trusted issuers | 401 |
TokenExpiredException | JWT exp sudah lewat | 401 |
InvalidTokenException | Signature invalid, JWT malformed, aud tidak cocok, JTI di-blacklist | 401 |
Semua tiga ditangkap middleware dalam single multi-catch dan diabort 401. Detail kegagalan tidak diekspos ke pemanggil.
TIP
Sejak v0.3.2, mismatch format key vs. algoritma melempar RuntimeException (bukan salah satu dari tiga di atas) — menghasilkan HTTP 500, bukan 401. Ini adalah kesalahan konfigurasi operator: PEM key dikonfigurasi untuk EdDSA, atau base64 Ed25519 dikonfigurasi untuk RS256. Pesan error menyertakan perintah bpm:sauth:keygen yang perlu dijalankan di gateway.
ServiceCodes
Namespace: Bpmlib\SauthClient\Constants\ServiceCodes
Konstanta ekosistem yang stabil di semua gateway dan service.
| Konstanta | Nilai | Keterangan |
|---|---|---|
GWA | 'gwa' | Issuer code GWA (claim iss) |
GWC | 'gwc' | Issuer code GWC (claim iss) |
WEBMASTER | 'wm' | Nilai fet untuk webmaster |
ROLE_PERSONAL | 'psn' | Role pelanggan perorangan (GWC) |
ROLE_COMPANY | 'cpy' | Role pelanggan perusahaan (GWC) |
ROLE_MITRA | 'mit' | Role mitra (GWC) |
CUSTOMER_ROLES | ['psn','cpy','mit'] | Semua role pelanggan GWC |
Kode audience service (srf, pnr, pel, dll.) tidak termasuk di sini — definisikan secara lokal di masing-masing consuming app.
Examples
Daftar Isi:
- 1. Setup guard + route dasar
- 2. Permission dan wildcard — sauth.gate
- 3. Route webmaster — sauth.wm
- 4. Session + JWT — sauth.dual
- 5. 3p M2M dan API key JWT — sauth.scope
- 6. M2M-only route — sauth.m2m
- 7. 3p user combined — sauth.gate + sauth.scope
- 8. JTI blacklist — revokasi API key JWT
- 9. Dev bypass — SAUTH_BYPASS dan fake claims
1. Setup guard + route dasar
Konfigurasi minimal untuk memproteksi API route di resource server.
// config/auth.php
'guards' => [
'api' => ['driver' => 'sauth'],
],
'defaults' => ['guard' => 'api'],// routes/api.php
Route::middleware('sauth.gate')->group(function () {
Route::get('/me', fn() => auth()->user());
Route::apiResource('/orders', OrderController::class);
});// app/Http/Controllers/OrderController.php
use Bpmlib\SauthClient\Auth\MicroserviceUser;
class OrderController extends Controller
{
public function index()
{
/** @var MicroserviceUser $user */
$user = auth()->user();
if ($user->isM2M()) {
return Order::all(); // background job / internal service
}
return Order::where('user_id', $user->userId())->get();
}
}SAUTH_AUDIENCE=srf
GWA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
GWC_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"2. Permission dan wildcard — sauth.gate
OR logic antar permission params; wildcard bekerja dari sisi fet user ke requirement.
// routes/api.php
Route::middleware('sauth.gate:report.view')->get('/reports', [ReportController::class, 'index']);
Route::middleware('sauth.gate:psn,cpy')->post('/applications', [ApplicationController::class, 'store']);
Route::middleware('sauth.gate:user.*')->get('/users', [UserController::class, 'index']);// Token dengan fet: ['user.read'] → lolos sauth.gate:user.* ✓ (luas ke sempit)
// Token dengan fet: ['user.*'] → TIDAK lolos sauth.gate:user.read ✗ (sempit ke luas)
// Token dengan fet: 'wm' → selalu lolos (webmaster bypass)Untuk route yang wajib memenuhi DUA permission sekaligus — chain middleware terpisah (bukan satu string dengan koma, karena koma = OR):
// Wajib report.view DAN export.run
Route::middleware(['sauth.gate:report.view', 'sauth.gate:export.run'])->get('/export', ...);3. Route webmaster — sauth.wm
Route operasional yang hanya boleh diakses token dengan fet === 'wm' secara eksplisit.
// routes/api.php
Route::middleware('sauth.wm')->group(function () {
Route::get('/admin/settings', [AdminController::class, 'index']);
Route::post('/admin/maintenance', [AdminController::class, 'toggle']);
});// Benar
Route::middleware('sauth.wm')->...
// Hindari — sauth.gate:wm menggunakan wildcard pattern matching
// dan tidak menjamin semantik 'wm' secara eksplisit
Route::middleware('sauth.gate:wm')->...4. Session + JWT — sauth.dual
Untuk route gateway (GWA/GWC) yang dapat diakses dari Inertia frontend (session) maupun service call (JWT). Resource server murni (SRF, PNR) tidak menggunakan middleware ini.
// routes/web.php (di gateway)
Route::middleware('sauth.dual')->get('/api/dashboard', [DashboardController::class, 'index']);
Route::middleware('sauth.dual:psn,cpy')->post('/api/submissions', [SubmissionController::class, 'store']);// app/Http/Controllers/DashboardController.php
use Bpmlib\SauthClient\Auth\MicroserviceUser;
class DashboardController extends Controller
{
public function index()
{
if (auth('web')->check()) {
// Jalur session — $user adalah Eloquent model
$user = auth('web')->user();
return view('dashboard', ['name' => $user->name]);
}
// Jalur JWT — $user adalah MicroserviceUser
/** @var MicroserviceUser $user */
$user = auth()->user();
return response()->json(['userId' => $user->userId()]);
}
}5. 3p M2M dan API key JWT — sauth.scope
Route untuk external partner — token 3p M2M atau API key JWT (keduanya punya claim scope).
// routes/api.php
Route::prefix('partner')->middleware('sauth.scope:srf.read')->group(function () {
Route::get('/data', [PartnerController::class, 'index']);
Route::get('/export', [PartnerController::class, 'export']);
});
Route::middleware('sauth.scope:srf.write')->post('/partner/submit', [PartnerController::class, 'store']);// app/Http/Controllers/PartnerController.php
use Bpmlib\SauthClient\Auth\MicroserviceUser;
class PartnerController extends Controller
{
public function index()
{
/** @var MicroserviceUser $user */
$user = auth()->user();
// $user->clientId() — client_id external partner
// $user->scope — array scope token
// $user->hasScope('srf.read', 'srf.write') — AND check untuk controller-level guard
return ExamData::forPartner($user->clientId())->paginate();
}
}Params sauth.scope menggunakan OR logic. Untuk AND — chain:
// Wajib punya srf.read DAN srf.write
Route::middleware(['sauth.scope:srf.read', 'sauth.scope:srf.write'])->...6. M2M-only route — sauth.m2m
Route yang menerima worker internal (1p) dan partner eksternal (3p) tapi menolak user token.
// routes/api.php
Route::middleware('sauth.m2m')->post('/internal/sync', [SyncController::class, 'run']);
// 1p lolos tanpa syarat; 3p wajib punya scope (AND)
Route::middleware('sauth.m2m:srf.read')->get('/internal/exam-data', [ExamController::class, 'fetch']);
Route::middleware('sauth.m2m:srf.read,srf.write')->post('/internal/submit', [ExamController::class, 'submit']);// app/Http/Controllers/SyncController.php
use Bpmlib\SauthClient\Auth\MicroserviceUser;
class SyncController extends Controller
{
public function run()
{
/** @var MicroserviceUser $user */
$user = auth()->user();
// $user->isM2M() selalu true di sini (sauth.m2m menolak user token)
// $user->isFirstParty() — true untuk internal worker
// $user->isThirdParty() — true untuk partner eksternal
SyncJob::dispatch($user->clientId());
return response()->json(['queued' => true]);
}
}7. 3p user combined — sauth.gate + sauth.scope
Token 3p user membawa fet (permission user) DAN scope (grant client OAuth). Kedua middleware wajib lolos secara AND. Route ini harus dipisah dari route 1p equivalen.
// routes/api.php
// 1p user token saja
Route::middleware('sauth.gate:user.read')->get('/profile', [ProfileController::class, 'show']);
// 3p user token — wajib fet user.read DAN scope srf.read
Route::middleware(['sauth.gate:user.read', 'sauth.scope:srf.read'])
->get('/partner/profile', [PartnerProfileController::class, 'show']);// Token 1p user → lolos sauth.gate, gagal sauth.scope (tidak ada scope claim) → 403
// Token 3p M2M → lolos sauth.scope, gagal sauth.gate (tidak ada fet) → 403
// Token 3p user dengan user.read + srf.read → lolos keduanya ✓8. JTI blacklist — revokasi API key JWT
API key JWT adalah long-lived token tanpa exp — revokasi dilakukan dengan memblokir jti UUID-nya.
# Setup sekali di consuming app
php artisan bpm:sauth:jti publish-migration
php artisan migrate# CLI — manual revokasi
php artisan bpm:sauth:jti block "550e8400-e29b-41d4-a716-446655440000" --reason="Key dikompromikan"
php artisan bpm:sauth:jti unblock "550e8400-e29b-41d4-a716-446655440000"
php artisan bpm:sauth:jti list// app/Http/Controllers/Internal/JtiBlacklistController.php
// HTTP endpoint untuk integrasi GWA listener (eksposa di route internal)
use Bpmlib\SauthClient\Services\JtiBlacklist;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class JtiBlacklistController extends Controller
{
public function block(Request $request, JtiBlacklist $blacklist): JsonResponse
{
$request->validate(['jti' => 'required|uuid', 'reason' => 'nullable|string']);
$blacklist->block($request->jti, $request->reason);
return response()->json(['blocked' => true]);
}
public function unblock(Request $request, JtiBlacklist $blacklist): JsonResponse
{
$request->validate(['jti' => 'required|uuid']);
$blacklist->unblock($request->jti);
return response()->json(['unblocked' => true]);
}
}// routes/internal.php — proteksi dengan middleware IP allowlist atau internal-only
Route::middleware('internal.only')->group(function () {
Route::post('/jti/block', [JtiBlacklistController::class, 'block']);
Route::post('/jti/unblock', [JtiBlacklistController::class, 'unblock']);
});Library menyediakan JtiBlacklist sebagai injectable service. Endpoint HTTP dan listener GWA adalah tanggung jawab consuming app.
9. Dev bypass — SAUTH_BYPASS dan fake claims
Setup development lokal tanpa gateway.
Pilih level bypass:
# Paling aman — decode token nyata, hanya skip signature
SAUTH_BYPASS=sig
# Skip signature + permission check, tetap butuh token
SAUTH_BYPASS=fet
# Full bypass tanpa token sama sekali (injeksi fake user)
SAUTH_BYPASS=total
SAUTH_FAKE_SID=dev-user-1
SAUTH_FAKE_SNM=Dev User
SAUTH_FAKE_FET=wm # lolos semua sauth.gate permission checkDengan JSON fake claims file (direkomendasikan untuk setup multi-middleware):
php artisan bpm:sauth:fake publish
# → storage/sauth-fake-claims.jsonSAUTH_BYPASS=total
SAUTH_FAKE_CLAIMS_FILE=sauth-fake-claims.jsonEdit storage/sauth-fake-claims.json sesuai kebutuhan test tanpa mengubah .env:
{
"1p_user": {
"sid": "dev-user-1",
"snm": "Dev User",
"fet": ["report.view", "user.read"],
"fp": "1p"
},
"1p_m2m": { "sid": "dev-worker", "fp": "1p" },
"3p_m2m": { "sid": "dev-partner", "scope": ["srf.read"], "fp": "3p" }
}Route kombinasi sauth.gate + sauth.scope (3p user endpoint):
# perm = skip permission + scope check; shape check tetap jalan
SAUTH_BYPASS=perm
SAUTH_FAKE_CLAIMS_FILE=sauth-fake-claims.json
# Pastikan "3p_user" ada di JSON dengan sid, snm, fet, scope, fpAPI key JWT lokal tanpa tabel blacklist:
SAUTH_BYPASS=sig
SAUTH_JTI_BLACKLIST_BYPASS=true # skip DB check untuk token dengan claim jti