Skip to content

sauth-server

OAuth Authorization Server untuk Laravel microservices — wraps Passport dengan standardized JWT claims.

Versi: 0.1.3

PHPLaravel


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:

php
use Bpmlib\SauthServer\Contracts\MicroserviceTokenClaimsProviderInterface;
use Bpmlib\SauthServer\Concerns\FetchesClientToken;
use Bpmlib\SauthServer\Services\ClientRegistrar;
use Bpmlib\SauthServer\Services\ServiceTokenIssuer;

Artisan Commands:

bash
php artisan bpm:sauth:init        # Inisialisasi: migrasi, is_first_party column, OAuth keys
php artisan bpm:sauth:client      # Buat OAuth client baru untuk ekosistem sauth

Konfigurasi:


Installation & Setup

Requirements

PHP:

  • Minimum: 8.4

Composer Dependencies:

bash
composer require bpmlib/sauth-client

Framework Requirements:

  • Laravel 12.0+ atau 13.0+
  • Laravel Passport 12.0+ atau 13.0+

Composer Install

bash
composer require bpmlib/sauth-server

Auto-Discovery

Service provider auto-registered via package discovery. Jika tidak menggunakan auto-discovery:

php
// config/app.php
'providers' => [
    Bpmlib\SauthServer\SauthServerServiceProvider::class,
    Bpmlib\SauthClient\SauthClientServiceProvider::class,
],

Publish Commands

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

Untuk konfigurasi lengkap, lihat Configuration.

Jalankan perintah init untuk setup database dan generate OAuth keys dalam satu langkah:

bash
php artisan bpm:sauth:init

Command 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
<?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(),
        ];
    }
}
php
// 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 === null berarti M2M (client_credentials) flow — return ['fet' => []]
  • 'wm' pada fet memberikan bypass penuh ke semua route yang dijaga sauth.gate

Configuration

Configuration File

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

Available Options

OptionTypeDefaultDescription
issuer_codestring-Kode issuer gateway — di-embed sebagai iss di setiap JWT
default_audiencestring'internal-services'Fallback aud claim jika tidak ada target service
algorithmstring'RS256'Algoritma signing JWT Lihat selengkapnya
token_exchange.enabledboolfalseAktifkan token exchange grant (RFC 8693)
token_ttl.accessint900TTL access token dalam detik (default 15 menit)
token_ttl.refreshint604800TTL refresh token dalam detik (default 7 hari)
private_key_filestring'oauth-private.key'Path private key relatif ke storage_path() — digunakan ServiceTokenIssuer untuk signing JWT ticketbooth
client_credentials.token_urlstring-URL /oauth/token di GWA — untuk FetchesClientToken
client_credentials.client_idstring-Client ID — untuk FetchesClientToken
client_credentials.client_secretstring-Client secret — untuk FetchesClientToken
bypassboolfalseDev bypass: saat true + app()->isLocal(), FetchesClientToken tidak memanggil GWA — JWT palsu dibuat lokal (alg:none). Tidak aktif di production. Lihat selengkapnya

Environment Variables

dotenv
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=false
bypass

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.

ClaimTypeDescription
issstringKode issuer gateway (gwa atau gwc)
audstringTarget service code (srf, pnr, pel, dll.)
iat / expintStandard timestamps
sidstringUser ID (user token) atau client_id (M2M token)
snmstringDisplay name aktor — tidak ada di M2M token
fetstring|arrayPermissions — "wm" bypass semua checks; array = daftar atomic permissions
actobject|nullRFC 8693 actor — {sub: calling_client_id}; hanya ada di delegated token

Perbedaan claim berdasarkan tipe token:

Token typefetscopesnm
User token (1p atau 3p)✓ dari ClaimsProvidertidak ada
1p M2M token[] array kosongtidak adatidak ada
3p M2M tokentidak ada✓ dari registrasi clienttidak 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

GrantUse Case
authorization_code + PKCELogin user via GWA atau GWC
client_credentialsM2M — 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.php di bawah trusted_issuers
  • Claim iss pada token masuk memberitahu sauth-client public 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:

bash
php artisan bpm:sauth:init [--force]

Options:

OptionDescription
--forceRegenerate 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.

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

bash
php artisan bpm:sauth:client [--first] [--user]

Options:

OptionDescription
--firstTandai sebagai first-party — skip consent screen, is_first_party=true, fet-based auth
--userGunakan authorization_code + PKCE; tanpa flag ini default ke client_credentials

Kombinasi flag:

bash
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/GWC

Command prompt untuk nama client, dan redirect URI jika --user. Setelah selesai menampilkan client_id dan client_secretsecret hanya tersedia sekali, simpan segera.


API Reference

MicroserviceTokenClaimsProviderInterface

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

php
public function getClaimsForUser(?Authenticatable $user): array

Kembalikan claims yang akan di-embed ke access token.

Parameters

NameTypeDefaultDescription
$userAuthenticatable|null-User yang login; null saat M2M (client_credentials) flow

Returns: array{sid: string, snm: string|null, fet: string|list<string>}

KeyTypeDescription
sidstringUser ID — untuk M2M tidak perlu diisi, library set dari client_id
snmstring|nullDisplay name; null atau tidak ada untuk M2M
fetstring|arrayPermissions; [] 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()
php
protected function clientToken(string $audience = ''): string

Kembalikan 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

NameTypeDefaultDescription
$audiencestring''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()
php
protected function forgetClientToken(string $audience = ''): void

Hapus token dari cache. Panggil setelah menerima 401 dari downstream service agar token baru di-fetch pada request berikutnya.

Parameters

NameTypeDefaultDescription
$audiencestring''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()

php
public function create(
    string $name,
    bool $isFirstParty = false,
    bool $isUserClient = false,
    string $redirectUri = '',
): Client

Buat OAuth client baru.

Parameters

NameTypeDefaultDescription
$namestring-Nama client
$isFirstPartyboolfalseTandai sebagai first-party (is_first_party = true)
$isUserClientboolfalsetrue = authorization_code + PKCE; false = client_credentials
$redirectUristring''Redirect URI — wajib jika $isUserClient = true

Returns: Laravel\Passport\ClientplainSecret tersedia di instance ini. Secret tidak bisa diambil lagi setelah ini.

update()

php
public function update(
    Client $client,
    string $name,
    ?string $redirectUri = null,
    ?bool $isFirstParty = null,
): Client

Update nama, redirect URI, atau flag first-party. Tidak mengubah secret.

Parameters

NameTypeDefaultDescription
$clientClient-Client yang akan diupdate
$namestring-Nama baru
$redirectUristring|nullnullRedirect URI baru; null = tidak diubah
$isFirstPartybool|nullnullFlag first-party baru; null = tidak diubah

Returns: Laravel\Passport\Client — fresh instance dari database

delete()

php
public function delete(Client $client): void

Revoke semua token aktif dan hapus record oauth_clients.

Parameters

NameTypeDefaultDescription
$clientClient-Client yang akan dihapus

rotateSecret()

php
public function rotateSecret(Client $client): Client

Generate client secret baru. Caller (controller) bertanggung jawab untuk audit logging.

Parameters

NameTypeDefaultDescription
$clientClient-Client yang secret-nya akan dirotasi

Returns: Laravel\Passport\ClientplainSecret tersedia di instance ini. Secret tidak bisa diambil lagi setelah ini.

list()

php
public function list(?bool $firstParty = null): \Illuminate\Database\Eloquent\Collection

Kembalikan semua client aktif (tidak ter-soft-delete). Soft-deleted clients dikecualikan secara otomatis karena Passport Client model menggunakan SoftDeletes.

Parameters

NameTypeDefaultDescription
$firstPartybool|nullnulltrue = 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()

php
public function issue(Authenticatable $user, string $targetService): array

Terbitkan JWT bertarget service tertentu untuk session user yang sedang login.

Parameters

NameTypeDefaultDescription
$userAuthenticatableUser yang sedang login (dari $request->user())
$targetServicestringKode audience target service, misalnya 'srf', 'pnr'

Returns: array{access: string, expires_in: int}

KeyTypeDescription
accessstringSigned JWT siap digunakan sebagai Bearer token
expires_inintTTL 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:

ClaimNilai
issconfig('sauth-server.issuer_code')
aud$targetService
iat / nbf / expTimestamps standar JWT
sidDari ClaimsProvider::getClaimsForUser($user)['sid']
snmDari ClaimsProvider — tidak ada di token jika null
fetDari 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

ClaimsProvider untuk internal users. is_webmaster menghasilkan 'wm' yang bypass semua permission checks di sauth.gate.

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

php
$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
<?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
<?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
<?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.

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

php
$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 PNR

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

php
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
<?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.phpwajib dijaga auth:sanctum:

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

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

dotenv
# 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=srf

Laravel Integration

Service Provider

Auto-registered via package discovery. Mendaftarkan:

  • Custom AccessTokenRepository yang menggantikan implementasi default Passport — meng-inject iss, sid, snm, fet, act ke setiap token yang diterbitkan
  • ClientRegistrar sebagai singleton
  • ServiceTokenIssuer sebagai singleton
  • Artisan commands bpm:sauth:client dan bpm:sauth:init
  • TokenExchangeGrant jika token_exchange.enabled = true

Published Assets

bash
php artisan vendor:publish --tag=sauth-server-config
# → config/sauth-server.php

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