sauth-client
Middleware dan guard Laravel untuk verifikasi JWT di microservice — tanpa Passport, tanpa database.
Versi: 0.1.3 (latest)
TL;DR
Library ini memvalidasi JWT yang diterbitkan oleh gateway (GWA atau GWC) di setiap request masuk ke microservice. Guard sauth me-resolve token dari header Authorization: Bearer, memverifikasi signature dengan public key issuer, lalu mengisi auth()->user() dengan objek MicroserviceUser — tidak ada session, tidak ada database.
Namespace:
use Bpmlib\SauthClient\Auth\MicroserviceUser;
use Bpmlib\SauthClient\Auth\MicroserviceTokenGuard;
use Bpmlib\SauthClient\Contracts\IssuerResolverInterface;
use Bpmlib\SauthClient\Resolvers\ConfigDrivenIssuerResolver;
use Bpmlib\SauthClient\Exceptions\InvalidTokenException;
use Bpmlib\SauthClient\Exceptions\TokenExpiredException;
use Bpmlib\SauthClient\Exceptions\UnknownIssuerException;
use Bpmlib\SauthClient\Constants\ServiceCodes;Konfigurasi:
config/sauth-client.php— Lihat lengkap
Installation & Setup
Requirements
PHP: 8.4+
Composer Dependencies:
firebase/php-jwt: ^7.0illuminate/auth: ^12.0|^13.0illuminate/support: ^12.0|^13.0illuminate/http: ^12.0|^13.0
Framework: Laravel 12.0+
Composer Install
composer require bpmlib/sauth-clientAuto-Discovery
Service provider otomatis terdaftar via Laravel package discovery.
Publish Commands
php artisan vendor:publish --tag=sauth-client-configUntuk konfigurasi lengkap, lihat Configuration.
Quick Start
Basic Usage
Daftarkan guard sauth di config/auth.php, lalu pakai middleware sauth.gate di route.
// config/auth.php
'guards' => [
'api' => ['driver' => 'sauth'],
],// routes/api.php
use Illuminate\Support\Facades\Route;
Route::middleware('sauth.gate')->group(function () {
Route::get('/profile', fn() => auth()->user()->name);
});// Di controller
$user = auth()->user(); // MicroserviceUser
$user->name; // nama aktor
$user->permissions; // 'wm' atau array permissionComprehensive Example
Controller yang menangani user token dan M2M token sekaligus.
<?php
namespace App\Http\Controllers;
use Bpmlib\SauthClient\Auth\MicroserviceUser;
use Illuminate\Http\JsonResponse;
class ReportController extends Controller
{
public function store(): JsonResponse
{
/** @var MicroserviceUser $user */
$user = auth()->user();
if ($user->isM2M()) {
// Request dari service lain (client_credentials token)
$callerId = $user->clientId();
return response()->json([
'created_by_service' => $callerId,
]);
}
// Request dari user manusia
$userId = $user->userId();
return response()->json([
'created_by' => $userId,
'name' => $user->name,
]);
}
}Configuration
Config File
php artisan vendor:publish --tag=sauth-client-configMenghasilkan config/sauth-client.php.
Available Options
| Option | Type | Default | Description |
|---|---|---|---|
trusted_issuers | array | — | Map issuer code → inline PEM string. Digunakan sebagai fallback jika trusted_issuer_files untuk issuer tersebut tidak diisi. |
trusted_issuers.gwa | string | env('GWA_PUBLIC_KEY') | Inline public key GWA (PEM) |
trusted_issuers.gwc | string | env('GWC_PUBLIC_KEY') | Inline public key GWC (PEM) |
trusted_issuer_files | array | — | Map issuer code → path file relatif terhadap storage_path(). Prioritas lebih tinggi dari trusted_issuers untuk issuer yang sama. Direkomendasikan untuk production dan rotasi key. |
trusted_issuer_files.gwa | string|null | env('GWA_PUBLIC_KEY_FILE') | Path file public key GWA, mis. keys/gwa_public.pem |
trusted_issuer_files.gwc | string|null | env('GWC_PUBLIC_KEY_FILE') | Path file public key GWC, mis. keys/gwc_public.pem |
audience | string | env('SAUTH_AUDIENCE') | Kode audience service ini — harus cocok dengan claim aud pada token masuk |
algorithms | array | [env('SAUTH_ALGO', 'RS256')] | Algoritma JWT yang diterima — harus sinkron dengan SAUTH_ALGO di gateway. |
bypass | bool | false | Bypass lokal: lewati verifikasi signature/expiry/audience tapi tetap decode token nyata ke MicroserviceUser. Authorization header tetap wajib kecuali bypass_fake_user.sid diisi. Hanya aktif saat app()->isLocal(). |
bypass_fake_user | array | — | Fake user yang diinjeksikan saat bypass=true dan tidak ada Authorization header. Diabaikan saat bypass=false. |
bypass_fake_user.sid | string|null | env('SAUTH_FAKE_SID', null) | ID fake user — null berarti tetap wajib token nyata (meski tidak diverifikasi) |
bypass_fake_user.snm | string|null | env('SAUTH_FAKE_SNM', 'Dev User') | Nama fake user — set null/kosong untuk simulasi M2M token |
bypass_fake_user.fet | string|array | env('SAUTH_FAKE_FET', 'wm') | Permission fake user — default 'wm' agar lolos semua sauth.gate permission check |
bypass_fake_m2m | array | — | Fake 1p M2M user untuk route sauth.m2m saat bypass=true dan tidak ada Authorization header. |
bypass_fake_m2m.sid | string|null | env('SAUTH_FAKE_M2M_SID', null) | client_id fake caller — null berarti tetap wajib token nyata |
bypass_fake_m2m.fet | array | [] | Shape 1p M2M — array kosong, tidak ada snm |
Environment Variables
# Pilihan 1 — file path (direkomendasikan untuk production)
# Resolver membaca dari storage_path($path); menang atas GWA_PUBLIC_KEY jika diisi.
GWA_PUBLIC_KEY_FILE=keys/gwa_public.pem
GWC_PUBLIC_KEY_FILE=keys/gwc_public.pem
# Pilihan 2 — inline PEM (fallback, atau untuk dev lokal)
GWA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
GWC_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
SAUTH_AUDIENCE=srf
SAUTH_ALGO=RS256
SAUTH_BYPASS=false
SAUTH_FAKE_SID=local-dev-user
SAUTH_FAKE_SNM="Dev User"
SAUTH_FAKE_FET=wm
SAUTH_FAKE_M2M_SID=local-worker-clientSetiap gateway memiliki key pair sendiri. Public key GWA dan GWC adalah key yang berbeda — diperoleh dari bpm:sauth:init masing-masing gateway. Setelah rotasi key (bpm:sauth:init --force), cukup ganti file di storage/keys/ — tidak perlu mengubah .env.
Core Concepts
Token Claim Structure
Semua token yang diterbitkan gateway mengikuti struktur claim berikut:
| Claim | Type | Ada di |
|---|---|---|
iss | string | Semua token — kode issuer (gwa atau gwc) |
aud | string | Semua token — kode target service (srf, pnr, dll.) |
iat / exp | int | Semua token — timestamp standard JWT |
sid | string | Semua token — user ID (user token) atau client_id (M2M) |
snm | string | User token saja — nama aktor; tidak ada di semua M2M token |
fet | string|array | User token + 1p M2M — "wm" untuk webmaster; array permission/role code; 1p M2M mendapat [] |
scope | string | 3p M2M saja — OAuth scope space-separated; tidak ada di user token dan 1p M2M |
act | object|null | Token exchange saja — {sub: calling_client_id}; null untuk direct token |
Identifikasi tipe token: snm tidak ada = M2M. scope ada = 3p M2M. fet ada = user token atau 1p M2M.
sauth.gate membaca fet. sauth.scope membaca scope. sauth.m2m menerima keduanya (1p + 3p) sambil menolak user token. Pisahkan route sesuai tipe token yang diterima.
User Tokens vs M2M Tokens
Perbedaan utama: claim snm (subject name).
| User Token | M2M Token | |
|---|---|---|
| Grant type | authorization_code | client_credentials |
snm | Ada (nama user) | Tidak ada |
sid | User ID | Client ID |
isM2M() | false | true |
userId() | ✅ Mengembalikan sid | ❌ Throws RuntimeException |
clientId() | ❌ Throws RuntimeException | ✅ Mengembalikan sid |
Gunakan isM2M() sebelum memanggil userId() atau clientId(). Keduanya sengaja throw exception jika dipanggil pada tipe token yang salah — gagal cepat lebih baik daripada audit log yang korup.
Permission Model
Klaim fet menentukan akses:
Nilai fet | Tipe | Behaviour di sauth.gate |
|---|---|---|
"wm" | string | Webmaster bypass — selalu lolos, tanpa cek permission |
["report.read", "report.write"] | array | Permission GWA — dicek dengan OR logic |
["psn"] | array | Role code GWC — dicek dengan OR logic |
OR logic: sauth.gate:psn,cpy lolos jika fet mengandung psn atau cpy.
Wildcard: permission user module.* cocok dengan module.read, module.write, dll. Matching menggunakan regex wildcard.
Per-Gateway Public Keys
Setiap gateway menggunakan key pair RSA sendiri. Claim iss pada token menentukan public key mana yang dipakai untuk verifikasi. Artinya kompromi pada satu gateway tidak mempengaruhi token dari gateway lain, dan rotasi key bisa dilakukan per-gateway secara independen.
API Reference
MicroserviceUser
Objek Authenticatable stateless — tidak ada Eloquent, tidak ada DB. Dibuat dari claim JWT yang sudah divalidasi.
Namespace: Bpmlib\SauthClient\Auth\MicroserviceUser
Properties
| Property | Type | Description |
|---|---|---|
$id | string | Nilai sid — user ID atau client ID tergantung tipe token |
$name | string|null | Nilai snm — nama aktor; null untuk M2M token |
$permissions | string|array<int, string>|null | Nilai fet — 'wm' atau array permission/role code; null untuk 3p M2M token (tidak punya fet) |
$actor | string|null | Nilai act.sub — client ID pemanggil pada delegated token; null untuk direct token |
$scope | array<int, string>|null | Nilai claim scope yang sudah diparsing — hanya ada di 3p M2M token; null untuk user/1p M2M token. |
Semua property readonly.
Methods
isM2M()
public function isM2M(): boolMengembalikan true jika $name adalah null (token diterbitkan via client_credentials).
userId()
public function userId(): stringMengembalikan $id untuk user token. Throws RuntimeException jika dipanggil pada M2M token.
clientId()
public function clientId(): stringMengembalikan $id untuk M2M token. Throws RuntimeException jika dipanggil pada user token.
isFirstParty()
TIP
NEW v0.1.3
public function isFirstParty(): boolMengembalikan true jika token adalah 1p M2M — claim fet ada (bahkan []) dan tidak ada scope. Selalu false untuk user token dan 3p M2M.
isThirdParty()
TIP
NEW v0.1.3
public function isThirdParty(): boolMengembalikan true jika token adalah 3p M2M — claim scope ada dan tidak ada fet. Selalu false untuk user token dan 1p M2M.
hasScope()
TIP
NEW v0.1.3
public function hasScope(string ...$scopes): boolParameters
| Name | Type | Default | Description |
|---|---|---|---|
$scopes | string variadic | — | Satu atau lebih scope yang dicek (AND logic) |
Returns: bool — true hanya jika semua scope yang disebutkan ada di claim scope token. Selalu false untuk 1p M2M (tidak punya claim scope).
AND logic: hasScope('srf.read', 'srf.write') mengembalikan false jika token hanya punya srf.read.
MicroserviceTokenGuard
Guard stateless yang memvalidasi Bearer JWT di setiap request dan mengembalikan MicroserviceUser.
Namespace: Bpmlib\SauthClient\Auth\MicroserviceTokenGuard
Implements: Illuminate\Contracts\Auth\Guard
Uses: Illuminate\Auth\GuardHelpers
Guard ini dikonfigurasi, bukan dipanggil langsung. Daftarkan di config/auth.php dengan driver 'sauth'.
Methods
user()
public function user(): ?MicroserviceUserMengekstrak token dari header Authorization: Bearer, memverifikasi signature, dan mengembalikan MicroserviceUser. Mengembalikan null jika tidak ada token. Throws InvalidTokenException, TokenExpiredException, atau UnknownIssuerException jika token invalid.
validate()
public function validate(array $credentials = []): boolSelalu mengembalikan false — stub interface, tidak digunakan.
IssuerResolverInterface
Kontrak untuk me-resolve public key RSA dari kode issuer.
Namespace: Bpmlib\SauthClient\Contracts\IssuerResolverInterface
resolvePublicKey()
public function resolvePublicKey(string $issuer): stringParameters
| Name | Type | Default | Description |
|---|---|---|---|
$issuer | string | — | Kode issuer dari claim iss |
Returns: string — RSA public key dalam format PEM
Throws: UnknownIssuerException jika issuer tidak dikenal
Implementasikan interface ini untuk mengambil public key secara dinamis (misalnya dari JWKS endpoint). Lihat selengkapnya.
ConfigDrivenIssuerResolver
Implementasi default IssuerResolverInterface — me-resolve public key dari config/sauth-client.php dengan prioritas dua tahap:
- File path (prioritas) — baca
trusted_issuer_files.{issuer}; jika diisi, baca file daristorage_path($path). - Inline PEM (fallback) — baca
trusted_issuers.{issuer}sebagai string PEM langsung.
Melempar UnknownIssuerException jika keduanya kosong, atau jika path diisi tapi file tidak ditemukan/kosong.
Namespace: Bpmlib\SauthClient\Resolvers\ConfigDrivenIssuerResolver
Terdaftar otomatis sebagai singleton. Tidak perlu diinstansiasi manual kecuali ingin di-override.
Middlewares
ValidateMicroserviceToken
Alias: sauth.gate
Memvalidasi JWT dan secara opsional memeriksa permission.
Signature:
public function handle(Request $request, Closure $next, string ...$permissionNeeded): mixedParameters
| Name | Type | Default | Description |
|---|---|---|---|
$permissionNeeded | string variadic | [] | Satu atau lebih permission yang diperlukan (OR logic) |
Behaviour:
- Tanpa params → lolos jika token valid
fet === 'wm'→ selalu lolos (webmaster bypass)- Dengan params → lolos jika
fetmengandung salah satu dari params (OR logic, wildcard support)
Responses:
401— tidak ada token, token invalid/expired, atau issuer tidak dikenal403— token valid tapi permission tidak terpenuhi
RequireWebmaster
Alias: sauth.wm
Memvalidasi JWT dan memastikan fet === 'wm'. Menolak token dengan permission array, meskipun signature valid.
Signature:
public function handle(Request $request, Closure $next): mixedTidak menerima parameter permission. Gunakan middleware ini — bukan sauth.gate:wm — untuk route webmaster.
Responses:
401— token tidak ada atau invalid403— token valid tapi bukan webmaster
DualAuthenticate
Alias: sauth.dual
Cek guard web (session) lebih dulu; jika tidak ada session aktif, delegasi ke ValidateMicroserviceToken.
Signature:
public function handle(Request $request, Closure $next, string ...$permissionNeeded): mixedParameters
| Name | Type | Default | Description |
|---|---|---|---|
$permissionNeeded | string variadic | [] | Diteruskan ke ValidateMicroserviceToken jika path JWT yang diambil |
Catatan: Pada path session, auth()->user() mengembalikan Eloquent model dari guard web. Pada path JWT, auth('api')->user() mengembalikan MicroserviceUser. Controller pada route dual harus menangani keduanya.
CheckServiceScope
Alias: sauth.scope
Memvalidasi JWT dan memeriksa claim scope (space-separated). Khusus untuk third-party M2M token dari external OAuth client yang mendapat akses via client_credentials grant di GWA. Gunakan sauth.gate untuk user/1p M2M — gunakan sauth.scope untuk 3p M2M.
Signature:
public function handle(Request $request, Closure $next, string ...$scopeNeeded): mixedParameters
| Name | Type | Default | Description |
|---|---|---|---|
$scopeNeeded | string variadic | [] | Satu atau lebih scope yang diperlukan (OR logic, exact match) |
Behaviour:
- Tanpa params → lolos jika token valid
- Dengan params → lolos jika claim
scopemengandung salah satu dari params (OR logic)
Responses:
401— token tidak ada atau invalid403— token valid tapi scope tidak terpenuhi
ValidateM2MToken
TIP
NEW v0.1.3
Alias: sauth.m2m
Memvalidasi JWT dan memastikan token adalah M2M — baik 1p internal worker maupun 3p external partner. User token selalu ditolak (403). Scope params hanya berlaku untuk 3p M2M (AND logic).
Signature:
public function handle(Request $request, Closure $next, string ...$requiredScopes): ResponseParameters
| Name | Type | Default | Description |
|---|---|---|---|
$requiredScopes | string variadic | [] | Scope yang diperlukan — hanya dicek untuk token 3p M2M (AND logic) |
Behaviour:
- User token (
snmada) →403 - 1p M2M (
isFirstParty()) → selalu lolos, scope params diabaikan - 3p M2M (
isThirdParty()) tanpa params → lolos - 3p M2M dengan params → lolos hanya jika semua scope ada (AND logic)
- Token malformed (tidak ada
fetmaupunscope) →403
Responses:
401— token tidak ada atau invalid403— user token, atau 3p M2M dengan scope tidak cukup
Exceptions
InvalidTokenException
new InvalidTokenException(string $reason = 'Invalid token')Dilempar saat token malformed, signature tidak valid, atau claim aud tidak cocok.
TokenExpiredException
new TokenExpiredException()
// Message: 'Token has expired'Dilempar saat JWT sudah melewati waktu exp.
UnknownIssuerException
new UnknownIssuerException(string $issuer)
// Message: "Unknown token issuer: {$issuer}"Dilempar saat kode iss pada token tidak ada di trusted_issuers.
ServiceCodes
TIP
NEW v0.1.3 — nilai GWA/GWC diubah dari '01-gwa'/'02-gwc' ke 'gwa'/'gwc'
Konstanta ekosistem-wide untuk kode issuer gateway, permission, dan role GWC.
Namespace: Bpmlib\SauthClient\Constants\ServiceCodes
Issuer Codes (claim iss) — hanya gateway issuer ekosistem:
ServiceCodes::GWA // 'gwa'
ServiceCodes::GWC // 'gwc'Permission:
ServiceCodes::WEBMASTER // 'wm'GWC Customer Role Codes (nilai dalam claim fet):
ServiceCodes::ROLE_PERSONAL // 'psn'
ServiceCodes::ROLE_COMPANY // 'cpy'
ServiceCodes::ROLE_MITRA // 'mit'
ServiceCodes::CUSTOMER_ROLES // ['psn', 'cpy', 'mit']Konstanta yang dihapus di v0.1.2:
SRF_ISSUER, SRF, PNR, PEL — ini adalah nilai app-level, bukan kontrak ekosistem. Setiap service sudah tahu audience code-nya sendiri dari SAUTH_AUDIENCE di .env. Definisikan secara lokal di codebase masing-masing jika dibutuhkan:
// Di consuming app — bukan di library
const SRF_ISSUER = 'srf';
const MY_AUDIENCE = 'srf';Examples
Daftar Isi:
- 1. Proteksi route — token apapun yang valid
- 2. Akses berbasis permission (OR logic)
- 3. Wildcard permission
- 4. Route khusus webmaster
- 5. Dual authentication (session OR JWT)
- 6. Controller: branching user vs M2M
- 7. Custom IssuerResolver
- 8. External partner route (3p M2M)
- 9. Artisan: bpm:sauth:lookup
- 10. sauth.m2m — internal worker dan external partner
NEW v0.1.3
1. Proteksi route — token apapun yang valid
Semua route di grup ini memerlukan token JWT yang valid, tanpa syarat permission tertentu.
Route::middleware('sauth.gate')->group(function () {
Route::get('/data', [DataController::class, 'index']);
Route::post('/submit', [DataController::class, 'store']);
});2. Akses berbasis permission (OR logic)
Route ini bisa diakses oleh user dengan permission report.view atau report.export.
Route::get('/reports', [ReportController::class, 'index'])
->middleware('sauth.gate:report.view,report.export');
// Bisa juga untuk GWC role codes (psn ATAU cpy)
Route::get('/customer-data', [CustomerController::class, 'index'])
->middleware('sauth.gate:psn,cpy');3. Wildcard permission
Permission assessment.* pada token user cocok dengan semua permission di modul assessment.
// Route ini dilindungi dengan permission spesifik
Route::post('/assessments', [AssessmentController::class, 'store'])
->middleware('sauth.gate:assessment.create');
// Token user dengan fet: ['assessment.*'] akan lolos
// Token user dengan fet: ['assessment.create'] juga lolos
// Token user dengan fet: ['report.view'] akan ditolak (403)4. Route khusus webmaster
Gunakan sauth.wm, bukan sauth.gate:wm.
Route::middleware('sauth.wm')->group(function () {
Route::get('/admin/config', [AdminController::class, 'config']);
Route::post('/admin/reset', [AdminController::class, 'reset']);
});sauth.gate:wm tidak akan berfungsi karena 'wm' sebagai string permission tidak akan pernah cocok dengan array fet. sauth.wm secara eksplisit memeriksa fet === 'wm'.
5. Dual authentication (session OR JWT)
Untuk route yang bisa diakses dari Inertia frontend (session) maupun dari service lain (JWT).
// Tanpa syarat permission
Route::middleware('sauth.dual')->get('/shared', [SharedController::class, 'index']);
// Dengan syarat permission (hanya berlaku pada path JWT)
Route::middleware('sauth.dual:data.read')->get('/data', [SharedController::class, 'data']);// Di controller
public function index(): JsonResponse
{
if (auth('web')->check()) {
// Path session — auth()->user() adalah Eloquent model
$user = auth('web')->user();
return response()->json(['source' => 'session', 'name' => $user->name]);
}
// Path JWT — auth('api')->user() adalah MicroserviceUser
$user = auth('api')->user();
return response()->json(['source' => 'jwt', 'name' => $user->name]);
}6. Controller: branching user vs M2M
Pola standar untuk endpoint yang bisa dipanggil oleh user maupun service.
<?php
namespace App\Http\Controllers;
use Bpmlib\SauthClient\Auth\MicroserviceUser;
use Illuminate\Http\JsonResponse;
class SubmissionController extends Controller
{
public function store(): JsonResponse
{
/** @var MicroserviceUser $actor */
$actor = auth()->user();
$record = [
'data' => request('payload'),
'created_at' => now(),
];
if ($actor->isM2M()) {
// Panggilan antar-service
$record['created_by_service'] = $actor->clientId();
$record['delegated_user'] = $actor->actor; // RFC 8693 act.sub, bisa null
} else {
// Request dari user manusia
$record['created_by'] = $actor->userId();
$record['actor_name'] = $actor->name;
}
// Simpan $record ...
return response()->json($record, 201);
}
}7. Custom IssuerResolver
Ganti ConfigDrivenIssuerResolver dengan implementasi sendiri — misalnya untuk mengambil public key dari JWKS endpoint atau secret manager.
<?php
namespace App\Auth;
use Bpmlib\SauthClient\Contracts\IssuerResolverInterface;
use Bpmlib\SauthClient\Exceptions\UnknownIssuerException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class RemoteJwksResolver implements IssuerResolverInterface
{
public function resolvePublicKey(string $issuer): string
{
return Cache::remember("jwks_{$issuer}", 3600, function () use ($issuer) {
$response = Http::get("https://keys.internal/jwks/{$issuer}");
if ($response->failed()) {
throw new UnknownIssuerException($issuer);
}
return $response->json('public_key')
?? throw new UnknownIssuerException($issuer);
});
}
}Daftarkan di AppServiceProvider:
use Bpmlib\SauthClient\Contracts\IssuerResolverInterface;
use App\Auth\RemoteJwksResolver;
public function register(): void
{
$this->app->bind(IssuerResolverInterface::class, RemoteJwksResolver::class);
}8. External partner route (3p M2M)
Pisahkan route internal (user + 1p M2M) dari route eksternal (3p M2M partner). sauth.gate membaca claim fet; sauth.scope membaca claim scope — keduanya tidak boleh dicampur.
Route::prefix('/api')->group(function () {
// Internal — user token dan 1p M2M worker
Route::middleware('sauth.gate')->group(function () {
Route::get('/submissions', [SubmissionController::class, 'index']);
Route::post('/submissions', [SubmissionController::class, 'store']);
});
// Eksternal — 3p M2M partner dengan OAuth scope
Route::middleware('sauth.scope:partner.read')->group(function () {
Route::get('/partner/submissions', [PartnerController::class, 'index']);
});
Route::middleware('sauth.scope:partner.read,partner.write')->group(function () {
Route::post('/partner/submissions', [PartnerController::class, 'store']);
});
});// Di controller — token 3p M2M selalu M2M, clientId() aman dipanggil langsung
public function index(): JsonResponse
{
$partner = auth()->user(); // MicroserviceUser
return response()->json([
'partner_id' => $partner->clientId(),
'scope' => $partner->scope, // ['partner.read', 'partner.write']
]);
}9. Artisan: bpm:sauth:lookup
TIP
NEW v0.1.2
Reverse-index semua route berdasarkan middleware sauth yang dipakai. Berguna untuk audit permission, onboarding, atau CI script.
# Lihat semua route beserta permission yang dibutuhkan (fet)
php artisan bpm:sauth:lookup
# Lihat route berdasarkan OAuth scope (3p M2M)
php artisan bpm:sauth:lookup --type=scope
# Lihat semua route yang dilindungi sauth.wm
php artisan bpm:sauth:lookup --type=wm
# Lihat route sauth.m2m beserta scope yang diperlukan
php artisan bpm:sauth:lookup --type=m2m
# Output JSON — pipeable ke jq
php artisan bpm:sauth:lookup --json
php artisan bpm:sauth:lookup --type=m2m --json | jq '.[0].routes'Contoh output --type=fet (default):
Permission Routes Route Names
------------ ------ ----------------------------------------
(none) 1 api.health
cpy 1 api.applications.store
psn 3 api.profile.show, api.profile.update, api.applications.store
report.view 3 api.reports.index, api.reports.show, api.reports.exportRoute dengan sauth.gate:psn,cpy muncul di baris psn dan cpy. (none) berarti middleware ada tapi tanpa syarat permission — token valid apapun diterima.
10. sauth.m2m — internal worker dan external partner
TIP
NEW v0.1.3
Route yang bisa dipanggil oleh internal worker (1p M2M) maupun external partner (3p M2M), tapi menolak user token. Scope params hanya berlaku untuk 3p.
Route::prefix('/api')->group(function () {
// Hanya M2M — internal worker ATAU external partner
Route::middleware('sauth.m2m')->group(function () {
Route::get('/internal/status', [StatusController::class, 'index']);
});
// M2M dengan scope — 1p selalu lolos; 3p harus punya srf.read
Route::middleware('sauth.m2m:srf.read')->group(function () {
Route::get('/partner/exam-data', [ExamController::class, 'index']);
});
// AND logic — 3p harus punya KEDUA scope
Route::middleware('sauth.m2m:srf.read,srf.write')->group(function () {
Route::post('/partner/exam-submit', [ExamController::class, 'store']);
});
});// Di controller — branch berdasarkan subtipe M2M
public function index(): JsonResponse
{
/** @var MicroserviceUser $caller */
$caller = auth()->user();
if ($caller->isFirstParty()) {
// Internal service worker — trusted sepenuhnya
return response()->json([
'caller' => $caller->clientId(),
'internal' => true,
]);
}
// 3p external partner
return response()->json([
'partner_id' => $caller->clientId(),
'scope' => $caller->scope,
]);
}Laravel Integration
Service Provider
SauthClientServiceProvider terdaftar otomatis via package discovery. Provider ini:
- Merge
config/sauth-client.php - Bind
IssuerResolverInterface→ConfigDrivenIssuerResolver(singleton) - Daftarkan guard driver
'sauth' - Daftarkan lima alias middleware
- Daftarkan Artisan command
bpm:sauth:lookup
Guard Registration
Tambahkan di config/auth.php:
'guards' => [
'api' => [
'driver' => 'sauth',
],
],Setelah ini, seluruh auth API Laravel berfungsi tanpa database:
auth()->user() // MicroserviceUser | null
auth()->check() // true jika token valid
auth()->user()->isM2M() // true jika client_credentials token
auth()->user()->userId() // human user ID
auth()->user()->clientId() // service client IDMiddleware Aliases
| Alias | Class | Keterangan |
|---|---|---|
sauth.gate | ValidateMicroserviceToken | Validasi token + permission opsional |
sauth.wm | RequireWebmaster | Validasi token + wajib webmaster |
sauth.dual | DualAuthenticate | Session fallback → JWT |
sauth.scope | CheckServiceScope | Validasi token + scope 3p M2M (OR) |
sauth.m2m | ValidateM2MToken | M2M-only; 1p selalu lolos; 3p scope AND |
Artisan Commands
| Command | Keterangan |
|---|---|
bpm:sauth:lookup | Reverse-index route berdasarkan sauth middleware key (--type=fet|scope|wm|m2m, --json) |