sauth-server
OAuth Authorization Server untuk Laravel microservices — wraps Passport dengan standardized JWT claims.
Versi: 0.1.3
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\Concerns\FetchesClientToken;
use Bpmlib\SauthServer\Services\ClientRegistrar;
use Bpmlib\SauthServer\Services\ServiceTokenIssuer;Artisan Commands:
php artisan bpm:sauth:init # Inisialisasi: migrasi, is_first_party column, OAuth keys
php artisan bpm:sauth:client # Buat OAuth client baru untuk ekosistem sauthKonfigurasi:
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 (jika belum ada), publish migration is_first_party, jalankan migrate, dan generate OAuth key pair. Lihat bpm:sauth:init untuk detail.
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 ['fet' => []]; // M2M token — tidak ada user permissions
}
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
$user === nullberarti M2M (client_credentials) flow — return['fet' => []]'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 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 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 |
Environment Variables
SAUTH_ISSUER_CODE=gwa
SAUTH_DEFAULT_AUDIENCE=internal-services
SAUTH_ALGO=RS256
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
# 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=falsebypass
TIP
NEW v0.1.3
Saat true dan app()->isLocal(), FetchesClientToken::clientToken() tidak memanggil GWA. Sebuah JWT palsu dibuat secara lokal menggunakan alg:none (lcobucci forUnsecuredSigner) dengan claim: iss = issuer_code, aud = target audience, sid = client_id, fet = [], expiry = 15 menit. JWT palsu ini di-cache 720 detik.
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.
algorithm
Algoritma signing JWT. Harus cocok dengan konfigurasi di sauth-client pada setiap consuming service — ketidakcocokan menyebabkan signature verification gagal di semua services.
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
Use Case: Ganti ke ES256/ES384 untuk key size lebih kecil dan signing lebih cepat. Pastikan semua services dan kedua gateways menggunakan algoritma yang sama sebelum mengganti.
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 | Kode issuer gateway (gwa atau gwc) |
aud | string | Target service code (srf, pnr, pel, dll.) |
iat / exp | int | Standard timestamps |
sid | string | User ID (user token) atau client_id (M2M token) |
snm | string | Display name aktor — tidak ada di M2M token |
fet | string|array | Permissions — "wm" bypass semua checks; array = daftar atomic permissions |
act | object|null | RFC 8693 actor — {sub: calling_client_id}; hanya ada di delegated token |
Perbedaan claim berdasarkan tipe token:
| Token type | fet | scope | snm |
|---|---|---|---|
| User token (1p atau 3p) | ✓ dari ClaimsProvider | tidak ada | ✓ |
| 1p M2M token | [] array kosong | tidak ada | tidak ada |
| 3p M2M token | tidak ada | ✓ dari registrasi client | tidak ada |
snm yang tidak ada adalah penanda M2M — services menggunakan ini untuk membedakan system actor dari user actor.
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.
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.
Key Management
Setiap gateway generate dan memiliki RSA 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
Artisan Commands
bpm:sauth:init
Inisialisasi sauth-server dalam satu langkah: publish Passport migrations, publish migration is_first_party, jalankan migrate, dan generate OAuth key pair.
Signature:
php artisan bpm:sauth:init [--force]Options:
| Option | Description |
|---|---|
--force | Regenerate OAuth keys meskipun sudah ada |
Command mengecek setiap langkah sebelum dijalankan — aman dijalankan berulang kali. Jika tabel oauth_clients sudah ada, Passport migrations dilewati. Jika kolom is_first_party sudah ada atau migration file-nya sudah di-publish, langkah itu dilewati. Jika OAuth keys sudah ada, generation dilewati kecuali --force diberikan.
php artisan bpm:sauth:init # setup pertama kali
php artisan bpm:sauth:init --force # regenerate OAuth keys (rotasi key)bpm:sauth:client
Buat OAuth client baru untuk ekosistem sauth.
Signature:
php artisan bpm:sauth:client [--first] [--user]Options:
| Option | Description |
|---|---|
--first | Tandai sebagai first-party — skip consent screen, is_first_party=true, fet-based auth |
--user | Gunakan authorization_code + PKCE; tanpa flag ini default ke client_credentials |
Kombinasi flag:
php artisan bpm:sauth:client # 3rd-party M2M — client_credentials, scope-based
php artisan bpm:sauth:client --first # 1st-party M2M — client_credentials, fet-based
php artisan bpm:sauth:client --user # 3rd-party user — authorization_code + PKCE
php artisan bpm:sauth:client --first --user # 1st-party user — frontend GWA/GWCCommand prompt untuk nama client, dan redirect URI jika --user. Setelah selesai menampilkan client_id dan client_secret — secret hanya tersedia sekali, simpan segera.
API Reference
MicroserviceTokenClaimsProviderInterface
interface MicroserviceTokenClaimsProviderInterface
{
public function getClaimsForUser(?Authenticatable $user): array;
}Contract yang diimplementasikan oleh setiap gateway. Dipanggil library selama token issuance untuk semua user token dan 1p M2M token. 3p M2M token 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 — untuk M2M tidak perlu diisi, library set dari client_id |
snm | string|null | Display name; null atau tidak ada untuk M2M |
fet | string|array | Permissions; [] untuk M2M |
Catatan: Saat $user === null, library hanya membaca fet dari return value — sid dan snm di-set langsung dari client credentials. Cukup return ['fet' => []] untuk M2M flow.
FetchesClientToken
TIP
v0.1.3 — $audience param ditambahkan; cache key kini menyertakan audience; bypass mode untuk local dev
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
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 = '',
): 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 |
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,
): ClientUpdate nama, redirect URI, atau flag first-party. 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 |
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
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 audience target service, misalnya 'srf', 'pnr' |
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 jika sauth-server.issuer_code belum dikonfigurasi.
Claims yang di-embed:
| Claim | Nilai |
|---|---|
iss | config('sauth-server.issuer_code') |
aud | $targetService |
iat / nbf / exp | Timestamps standar JWT |
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.
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
NEW v0.1.3
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 ['fet' => []];
}
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 ['fet' => []];
}
return [
'sid' => (string) $user->id,
'snm' => $user->name,
'fet' => [$user->user_type], // 'psn', 'cpy', atau 'mit'
];
}
}3. Registrasi Client via ClientRegistrar
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'),
);
// plainSecret hanya tersedia di instance ini — simpan atau tampilkan sekarang
return response()->json([
'client_id' => $client->getKey(),
'client_secret' => $client->plainSecret,
]);
}
}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
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']);
$result = $this->issuer->issue(
$request->user(),
$request->input('target'),
);
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
TIP
NEW v0.1.3
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=srfLaravel Integration
Service Provider
Auto-registered via package discovery. Mendaftarkan:
- Custom
AccessTokenRepositoryyang menggantikan implementasi default Passport — meng-injectiss,sid,snm,fet,actke setiap token yang diterbitkan ClientRegistrarsebagai singletonServiceTokenIssuersebagai singleton- Artisan commands
bpm:sauth:clientdanbpm:sauth:init TokenExchangeGrantjikatoken_exchange.enabled = true
Published Assets
php artisan vendor:publish --tag=sauth-server-config
# → config/sauth-server.phpRequired Migration
Migration untuk kolom is_first_party di-ship bersama library sebagai stub dan di-publish otomatis oleh bpm:sauth:init ke database/migrations/. Tidak perlu menulis migration secara manual.
Links
- Registry: bpmlib — Private Composer Repository