Skip to content

Sauth Client

Stateless JWT verification middleware dan guard untuk Laravel microservice.

Versi: 0.3.4 Changelog

PHPLaravel


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:

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

bash
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 JSON

Konfigurasi:


Installation & Setup

Requirements

PHP: 8.4+

Composer Dependencies:

bash
# Ditarik otomatis oleh bpmlib/sauth-client
firebase/php-jwt ^7.0

Framework: Laravel 12.0+ atau 13.0+

Composer Install

bash
composer require bpmlib/sauth-client

Auto-Discovery

SauthClientServiceProvider auto-registered via Laravel package discovery. Tidak perlu mendaftarkan secara manual.

Publish Commands

bash
php artisan vendor:publish --tag=sauth-client-config

Membuat config/sauth-client.php. Untuk konfigurasi lengkap, lihat Configuration.


Quick Start

php
// config/auth.php
'guards' => [
    'api' => ['driver' => 'sauth'],
],
'defaults' => [
    'guard' => 'api',
],
php
// 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);
});
php
// 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:

dotenv
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

bash
php artisan vendor:publish --tag=sauth-client-config

Issuer 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.

OptionEnv VarDefaultDescription
trusted_issuers.gwaGWA_PUBLIC_KEYnullInline PEM public key untuk GWA
trusted_issuers.gwcGWC_PUBLIC_KEYnullInline PEM public key untuk GWC
trusted_issuer_files.gwaGWA_PUBLIC_KEY_FILEnullPath file PEM relatif ke storage/; prioritas atas inline
trusted_issuer_files.gwcGWC_PUBLIC_KEY_FILEnullPath file PEM relatif ke storage/; prioritas atas inline
trusted_issuer_url_map.gwaGWA_APP_URLnullURL gateway GWA untuk token 3p dengan iss berbasis URL
trusted_issuer_url_map.gwcGWC_APP_URLnullURL 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.

OptionEnv VarDefaultDescription
audienceSAUTH_AUDIENCEnullAudience code service ini (mis. srf, pnr) — harus cocok dengan claim aud token
algorithms[0]SAUTH_ALGORS256Algoritma JWT global; fallback jika tidak ada per-issuer override
issuer_algorithms.gwaSAUTH_GWA_ALGOnullOverride algoritma khusus GWA; null = gunakan SAUTH_ALGO
issuer_algorithms.gwcSAUTH_GWC_ALGOnullOverride 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.

OptionEnv VarDefaultDescription
apikey_issuer_filesAPIKEY_PUBLIC_KEY_FILEnullPath file public key khusus API key JWT; fallback ke trusted_issuer_files['gwa']
apikey_issuer_algorithmsSAUTH_APIKEY_ALGOnullAlgoritma 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_BYPASSSignature / ExpPermission (fet)ScopeShape check
false / noneEnforcedEnforcedEnforcedEnforced
sigSkippedEnforcedEnforcedEnforced
fetSkippedSkippedEnforcedEnforced
scopeSkippedEnforcedSkippedEnforced
permSkippedSkippedSkippedEnforced
true / totalSkippedSkippedSkippedSkipped

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.

OptionEnv VarDefaultDescription
bypassSAUTH_BYPASSfalseBypass lokal — lihat tabel di atas
scope_bypass (deprecated)SAUTH_SCOPE_BYPASSfalseStack di atas bypass via OR logic untuk sauth.scope saja
jti_bypassSAUTH_JTI_BLACKLIST_BYPASSfalseSkip 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

OptionEnv VarDefaultDescription
fake_claims_fileSAUTH_FAKE_CLAIMS_FILEnullPath JSON relatif ke storage/; prioritas atas flat env vars
bypass_fake_user.sidSAUTH_FAKE_SIDnullFake user ID untuk sauth.gate; null = tetap butuh token
bypass_fake_user.snmSAUTH_FAKE_SNMDev UserFake display name untuk sauth.gate
bypass_fake_user.fetSAUTH_FAKE_FETwmFake permission untuk sauth.gate; wm = lolos semua permission check
bypass_fake_m2m.sidSAUTH_FAKE_M2M_SIDnullFake client ID untuk sauth.m2m; shape 1p M2M
bypass_fake_scope.sidSAUTH_FAKE_SCOPE_SIDnullFake client ID untuk pure sauth.scope route
bypass_fake_scope.scopeSAUTH_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 TokenfpsnmscopejtiexpMiddleware
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:

  1. Baca Bearer token dari header Authorization
  2. peekIssuer() — decode payload JWT tanpa verifikasi signature untuk membaca nilai raw iss
  3. issuerResolver->resolvePublicKey($iss) — resolusi public key; throws UnknownIssuerException jika tidak dikenal. Token 3p dengan iss berbasis URL di-map ke kode pendek via trusted_issuer_url_map sebelum lookup key
  4. Resolusi algoritma per issuer: issuer_algorithms[$code]SAUTH_ALGORS256

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.

  1. JWT::decode() via firebase/php-jwt — verifikasi signature + expiry (skip exp jika tidak ada)
  2. Cek claim aud cocok dengan sauth-client.audiencedilewati untuk token 3p (fp=3p) karena token 3p dibatasi oleh scope via sauth.scope, bukan aud; throws InvalidTokenException untuk token 1p/pra-v0.3 jika tidak cocok. aud berupa array (valid per RFC 7519) diterima dengan benar
  3. Jika jti ada: cek tabel sauth_blacklisted_jti_tokens (cached 5 menit) — throws InvalidTokenException jika blacklisted
  4. Build dan return MicroserviceUser dari claims yang telah tervalidasi

Hasil dicache di $resolvedUser — tidak ada decode ulang pada request yang sama.

Tabel Keputusan Middleware

Middleware1p User3p User1p M2M3p M2M / API key JWT
sauth.gateLolos jika fet cocok (atau wm)Lolos jika fet cocok (atau wm)Lolos jika tanpa params403 (tidak ada fet)
sauth.wmLolos jika fet === 'wm'Lolos jika fet === 'wm'403403
sauth.dualLolos (session atau JWT)Lolos (session atau JWT)Lolos jika tanpa params403
sauth.scope403 (tidak ada scope)Lolos jika scope cocok (OR)403 (tidak ada scope)Lolos jika scope cocok (OR)
sauth.m2m403 (snm ada)403 (snm ada)Selalu lolosLolos 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:

bash
php artisan bpm:sauth:lookup [--type=fet] [--json]

Options:

OptionDefaultDescription
--typefetfet — permission gate; scope — OAuth scope; wm — route webmaster; m2m — M2M scope
--jsonEmit 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:

bash
php artisan bpm:sauth:jti {subcommand} [jti] [--reason=]

Subcommands:

SubcommandDescription
publish-migrationCopy 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
listTampilkan 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:

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

dotenv
SAUTH_FAKE_CLAIMS_FILE=sauth-fake-claims.json

Template JSON yang dihasilkan:

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

Mapping profil per middleware:

MiddlewareKunci JSON
sauth.gate, sauth.dual1p_user
sauth.m2m1p_m2m
sauth.scope3p_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:

php
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()
php
public function isM2M(): bool

true jika name === null — token diterbitkan via client_credentials. Gunakan ini (bukan isFirstParty()) untuk membedakan user vs. mesin.

userId()
php
public function userId(): string

Returns id untuk user token. Throws RuntimeException jika dipanggil pada M2M token.

clientId()
php
public function clientId(): string

Returns id untuk M2M token. Throws RuntimeException jika dipanggil pada user token.

isFirstParty() / isThirdParty()
php
public function isFirstParty(): bool
public function isThirdParty(): bool

TIP

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()
php
public function hasScope(string ...$required): bool

AND 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

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

php
$this->app->singleton(IssuerResolverInterface::class, MyJwksIssuerResolver::class);

ConfigDrivenIssuerResolver

Namespace: Bpmlib\SauthClient\Resolvers\ConfigDrivenIssuerResolver

Implementasi default IssuerResolverInterface. Resolusi 3 langkah per issuer:

  1. File pathtrusted_issuer_files.{$issuer} (prioritas; path relatif ke storage/)
  2. Inline valuetrusted_issuers.{$issuer} (fallback)
  3. 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.

php
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 menit

MicroserviceTokenGuard 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.

php
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 logic

Parameters:

NameTypeDefaultDescription
...$permissionNeededstringPermission 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

php
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

php
Route::middleware('sauth.dual')           // session atau JWT tanpa permission check
Route::middleware('sauth.dual:psn,cpy')   // jalur JWT: wajib psn atau cpy

Cek 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

php
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 logic

Exact 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

php
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

ExceptionKondisiHTTP
UnknownIssuerExceptioniss tidak ada di trusted issuers401
TokenExpiredExceptionJWT exp sudah lewat401
InvalidTokenExceptionSignature invalid, JWT malformed, aud tidak cocok, JTI di-blacklist401

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.

KonstantaNilaiKeterangan
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

Konfigurasi minimal untuk memproteksi API route di resource server.

php
// config/auth.php
'guards' => [
    'api' => ['driver' => 'sauth'],
],
'defaults' => ['guard' => 'api'],
php
// routes/api.php
Route::middleware('sauth.gate')->group(function () {
    Route::get('/me', fn() => auth()->user());
    Route::apiResource('/orders', OrderController::class);
});
php
// 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();
    }
}
dotenv
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.

php
// 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']);
php
// 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):

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

php
// routes/api.php
Route::middleware('sauth.wm')->group(function () {
    Route::get('/admin/settings', [AdminController::class, 'index']);
    Route::post('/admin/maintenance', [AdminController::class, 'toggle']);
});
php
// 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.

php
// 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']);
php
// 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).

php
// 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']);
php
// 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:

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

php
// 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']);
php
// 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.

php
// 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']);
php
// 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.

bash
# Setup sekali di consuming app
php artisan bpm:sauth:jti publish-migration
php artisan migrate
bash
# 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
php
// 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]);
    }
}
php
// 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:

dotenv
# 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 check

Dengan JSON fake claims file (direkomendasikan untuk setup multi-middleware):

bash
php artisan bpm:sauth:fake publish
# → storage/sauth-fake-claims.json
dotenv
SAUTH_BYPASS=total
SAUTH_FAKE_CLAIMS_FILE=sauth-fake-claims.json

Edit storage/sauth-fake-claims.json sesuai kebutuhan test tanpa mengubah .env:

json
{
    "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):

dotenv
# 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, fp

API key JWT lokal tanpa tabel blacklist:

dotenv
SAUTH_BYPASS=sig
SAUTH_JTI_BLACKLIST_BYPASS=true   # skip DB check untuk token dengan claim jti