Sauth Server
OAuth Authorization Server untuk Laravel microservices — wraps Passport dengan standardized JWT claims.
Versi: 0.4.0 Changelog
TL;DR
Install di GWA dan GWC saja — resource server (SRF, PNR, dll.) hanya install sauth-client. Library ini menggantikan copy-paste JWT issuance logic dengan satu implementasi terkontrol yang meng-inject claim standar (iss, fp, sid, snm, fet, scope, jti, act) ke setiap token sesuai tipenya.
Namespace:
use Bpmlib\SauthServer\Contracts\MicroserviceTokenClaimsProviderInterface;
use Bpmlib\SauthServer\Contracts\OidcClaimsProviderInterface; // v0.3.4
use Bpmlib\SauthServer\Concerns\FetchesClientToken;
use Bpmlib\SauthServer\Services\ApiKeyIssuer;
use Bpmlib\SauthServer\Services\ClientRegistrar;
use Bpmlib\SauthServer\Services\IdTokenIssuer; // v0.3.4
use Bpmlib\SauthServer\Services\ServiceTokenIssuer;
use Bpmlib\SauthServer\Support\KeyLoader;Artisan Commands:
php artisan bpm:sauth:init # Setup pertama kali: migrasi + keygen (idempotent)
php artisan bpm:sauth:migration # Publish & jalankan sauth migrations tanpa keygen
php artisan bpm:sauth:keygen # Generate key pair (RS256, ES256, EdDSA, dll.)
php artisan bpm:sauth:client # Buat OAuth client baru
php artisan bpm:sauth:apikey:issue # Terbitkan API key JWT untuk partner
php artisan bpm:sauth:apikey:list # Lihat semua API key yang diterbitkan
php artisan bpm:sauth:apikey:revoke # Cabut API key JWTKonfigurasi:
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:
// bootstrap/providers.php
Bpmlib\SauthServer\SauthServerServiceProvider::class,
Bpmlib\SauthClient\SauthClientServiceProvider::class,Publish Commands
php artisan vendor:publish --tag=sauth-server-config
php artisan bpm:sauth:initbpm:sauth:init menangani Passport migrations, sauth migrations, migrate, dan keygen dalam satu langkah. Untuk konfigurasi lengkap, lihat Configuration.
IMPORTANT
Upgrade ke v0.3.1+: jalankan bpm:sauth:migration lalu populate service_code di semua 1p resource server client sebelum deploy kode. Lihat Deployment Notes.
WARNING
Upgrade ke v0.4.0 (breaking): MicroserviceTokenClaimsProviderInterface::getClaimsForUser() menambahkan parameter kedua string $clientId. Implementasi lama (GwaClaimsProvider, GwcClaimsProvider) yang belum diupdate akan fatal error saat class di-load (Declaration must be compatible). Update signature implementasi sebelum deploy versi ini. Lihat Custom Claims.
Quick Start
Basic Usage
Implement MicroserviceTokenClaimsProviderInterface dan 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, string $clientId): array
{
if ($user === null) {
return ['sid' => '', 'snm' => null]; // interface compliance saja
}
return [
'sid' => (string) $user->id,
'snm' => $user->name,
'fet' => $user->is_webmaster ? 'wm' : $user->getFeatures(),
];
}
}// AppServiceProvider::register()
$this->app->bind(MicroserviceTokenClaimsProviderInterface::class, GwaClaimsProvider::class);Key Points:
getClaimsForUser(null, ...)tidak dipanggil untuk 1p M2M token — library setsid = client_idlangsung; return value untuknulldiabaikan$clientId(v0.4.0+) — client OAuth yang meminta token; gunakan untuk scoping custom claim ke client tertentu — lihat Custom Claims'wm'padafetmemberikan bypass penuh ke semua routesauth.gate- Bind di
register()bukanboot()— service provider ini resolve pada saat container bootstrap
Configuration
Configuration File
php artisan vendor:publish --tag=sauth-server-configAvailable Options
| Option | Type | Default | Description |
|---|---|---|---|
issuer_code | string | - | Kode gateway (gwa/gwc) — di-embed sebagai iss di 1p token |
default_audience | string | 'internal-services' | Fallback aud jika tidak ada target service |
algorithm | string | 'RS256' | Algoritma signing shared dengan sauth-client Lihat selengkapnya |
sign_algorithm | string|null | null | Override algo signing sauth-server Lihat selengkapnya |
apikey_sign_algorithm | string|null | null | Override algo khusus API key JWT Lihat selengkapnya |
apikey_private_key_file | string|null | null | Override key file khusus API key JWT Lihat selengkapnya |
private_key_file | string | 'oauth-private.key' | Path private key relatif ke storage_path() |
token_exchange.enabled | bool | false | Aktifkan token exchange grant (RFC 8693) — GWA only |
token_ttl.access | int | 900 | TTL access token dalam detik |
token_ttl.refresh | int | 604800 | TTL refresh token dalam detik |
client_credentials.token_url | string | - | URL /oauth/token 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: skip GWA call di local env Lihat selengkapnya |
oidc.enabled | bool | false | Aktifkan OIDC — endpoints + id_token issuance Lihat selengkapnya |
bypass
Saat true dan app()->isLocal(), FetchesClientToken::clientToken() tidak memanggil GWA — JWT palsu dibuat lokal (alg:none) dengan claim: iss = issuer_code, aud = target, sid = client_id, fp = '1p', expiry 15 menit. Di-cache 720 detik. Shape identik dengan 1p M2M nyata: tidak ada fet, snm, atau scope.
Kondisi app()->isLocal() di-evaluate saat runtime — nilai true di production tidak mengaktifkan bypass selama APP_ENV != local.
oidc.enabled
TIP
NEW v0.3.4
Saat true, service provider mendaftarkan tiga endpoint OIDC secara otomatis:
| Endpoint | Keterangan |
|---|---|
GET /.well-known/openid-configuration | Discovery document |
GET /.well-known/jwks.json | Public key dalam format JWK Set |
GET /userinfo | sub + name + extra claims via OidcClaimsProviderInterface |
Mengaktifkan id_token issuance untuk client is_oidc = true. Kolom is_oidc harus ada sebelum flag ini diaktifkan — jalankan bpm:sauth:migration terlebih dulu. Boot-time guard aktif: jika kolom belum ada, RuntimeException dilempar.
sign_algorithm
Override algoritma signing khusus sauth-server. Mengalahkan SAUTH_ALGO untuk token signing dan key generation. SAUTH_ALGO tetap dibaca resource server.
Urutan resolusi token signing: SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256
Gunakan saat gateway ingin ganti algoritma tanpa mengubah SAUTH_ALGO yang dibaca resource server:
SAUTH_SIGN_ALGO=ES256 # gateway sign dengan ES256
SAUTH_ALGO=RS256 # resource server masih RS256apikey_sign_algorithm
TIP
NEW v0.3.3
Override algoritma signing khusus API key JWT — tidak memengaruhi token Passport reguler.
Urutan resolusi: SAUTH_APIKEY_SIGN_ALGO → SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256
apikey_private_key_file
TIP
NEW v0.3.3
Override path private key khusus API key JWT. Path relatif ke storage_path().
Urutan resolusi: SAUTH_APIKEY_PRIVATE_KEY_FILE → SAUTH_PRIVATE_KEY_FILE → oauth-private.key
Gunakan bersama apikey_sign_algorithm untuk key pair yang sepenuhnya terpisah dari key utama gateway.
algorithm
Algoritma signing JWT shared antara sauth-server dan sauth-client. Harus cocok di setiap consuming service — mismatch menyebabkan signature verification gagal.
Values: RS256 (default), RS384, RS512, ES256, ES384, ES512, EdDSA.
EdDSA: key format base64 single-line, bukan PEM. KeyLoader mendeteksi mismatch format/algo dan melempar RuntimeException dengan pesan yang menyertakan command perbaikan.
Untuk ganti algoritma gateway secara independen, gunakan sign_algorithm — lihat sign_algorithm.
Environment Variables
SAUTH_ISSUER_CODE=gwa
SAUTH_DEFAULT_AUDIENCE=internal-services
SAUTH_ALGO=RS256
SAUTH_SIGN_ALGO= # override SAUTH_ALGO untuk signing di sauth-server
SAUTH_APIKEY_SIGN_ALGO= # override algo khusus API key JWT
SAUTH_TOKEN_EXCHANGE_ENABLED=false
SAUTH_ACCESS_TOKEN_TTL=900
SAUTH_REFRESH_TOKEN_TTL=604800
SAUTH_PRIVATE_KEY_FILE=oauth-private.key
SAUTH_APIKEY_PRIVATE_KEY_FILE= # override key file khusus API key JWT
# Untuk FetchesClientToken (workers)
SAUTH_TOKEN_URL=https://gwa.example.com/oauth/token
SAUTH_CLIENT_ID=your-client-id
SAUTH_CLIENT_SECRET=your-client-secret
SAUTH_BYPASS=false
SAUTH_OIDC_ENABLED=falseTabel
Publish Migration
php artisan bpm:sauth:init # atau
php artisan bpm:sauth:migrationMigration di-publish otomatis ke database/migrations/ — tidak perlu menulis manual.
Tables
SauthJtiToken
Table:
sauth_jti_tokensNamespace:Bpmlib\SauthServer\Models\SauthJtiToken
Audit table untuk setiap API key JWT yang diterbitkan. Primary key UUID (id = jti di JWT).
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | No | - | Primary key — sama dengan jti di JWT |
app | varchar(255) | No | - | Nama integrasi (sid di JWT) |
aud | varchar(255) | No | - | Target service code |
issuer | varchar(255) | No | - | Siapa yang menerbitkan — user SID atau 'cli' |
encrypted_scopes | text | Yes | null | Scopes dienkripsi via encrypt() |
encrypted_token | text | No | - | Plaintext JWT dienkripsi via encrypt() |
revoke_reason | varchar(255) | Yes | null | Alasan pencabutan |
revoked_at | timestamp | Yes | null | Timestamp pencabutan; null = aktif |
created_at | timestamp | Yes | null | - |
updated_at | timestamp | Yes | null | - |
Core Concepts
Token Claim Structure
Nama claim adalah shared contract antara sauth-server, sauth-client, dan sauth-frontend — jangan ubah tanpa memperbarui ketiganya.
| Claim | Type | Description |
|---|---|---|
iss | string | 1p token: kode gateway (gwa/gwc); 3p + API key JWT: APP_URL |
aud | string | Target service code |
iat | int | Issued-at timestamp |
exp | int | Expiry — tidak ada di API key JWT (revocation via jti blacklist) |
fp | string | Party marker — '1p' atau '3p' |
sid | string | User ID (user token), client_id (M2M), atau app name (API key JWT) |
snm | string | Display name user — tidak ada di M2M dan API key JWT |
fet | string|array | Permissions user — "wm" bypass semua; array = atomic permissions; tidak pernah ada di M2M |
scope | string | Space-separated scopes — 3p user, 3p M2M, API key JWT; tidak ada di 1p |
jti | string | UUID — hanya ada di API key JWT; digunakan sebagai blacklist key |
act | object | RFC 8693 — {sub: calling_client_id}; hanya ada di delegated token |
| (host-defined) | mixed | NEW v0.4.0 — claim root-level tambahan yang dikembalikan getClaimsForUser() di luar sid/snm/fet; hanya di user token. Nama claim ditentukan host — lihat Custom Claims |
Perbedaan per token type:
| Token type | fp | sid | snm | fet | scope | jti | exp |
|---|---|---|---|---|---|---|---|
| 1p user token | 1p | user ID | ✓ | ✓ | — | — | ✓ |
| 3p user token | 3p | user ID | ✓ | ✓ | ✓ | — | ✓ |
| 1p M2M token | 1p | client_id | — | — | — | — | ✓ |
| 3p M2M token | 3p | client_id | — | — | ✓ | — | ✓ |
| API key JWT | 3p | app name | — | — | ✓ | ✓ | — |
1st-Party vs 3rd-Party Clients
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. Kolom is_first_party adalah kolom custom di oauth_clients — ditambahkan oleh library, bukan bagian dari Passport native.
Third-party — developer/partner eksternal. Melalui OAuth2 consent screen dan scope-based authorization.
service_code — kolom nullable unique di oauth_clients. ServiceTokenIssuer memvalidasi target ticketbooth terhadap kolom ini sebelum menerbitkan token. Lihat Deployment Notes.
Middleware mapping:
| Token type | Middleware |
|---|---|
| 1p user | sauth.gate |
| 1p M2M | sauth.gate (no params) atau sauth.m2m |
| 3p user | sauth.gate + sauth.scope (keduanya wajib) |
| 3p M2M | sauth.scope |
| API key JWT | sauth.scope |
Custom Claims
TIP
NEW v0.4.0
Host apps dapat menyisipkan claim tambahan yang tidak ada di ecosystem contract — misalnya gender — untuk dibaca client tertentu. Key apa pun yang dikembalikan getClaimsForUser() di luar sid/snm/fet otomatis menjadi claim root-level (flat, tanpa wrapper ext: {...}).
class GwaClaimsProvider implements MicroserviceTokenClaimsProviderInterface
{
private const GENDER_CLAIM_CLIENTS = ['9f3a1c20-hr-portal-client-id'];
public function getClaimsForUser(?Authenticatable $user, string $clientId): array
{
if ($user === null) {
return ['sid' => '', 'snm' => null];
}
$claims = [
'sid' => (string) $user->id,
'snm' => $user->name,
'fet' => $user->is_webmaster ? 'wm' : $user->getFeatures(),
];
if (in_array($clientId, self::GENDER_CLAIM_CLIENTS, true)) {
$claims['gender'] = $user->gender; // hanya untuk satu client
}
$claims['locale'] = $user->preferred_locale ?? 'en'; // semua client dapat
return $claims;
}
}Poin penting:
- Default: terlihat oleh semua client. Tidak ada config allow/deny di level library — scoping ke client tertentu adalah tanggung jawab kode host via parameter
$clientId. - Key reserved akan throw. Key yang collide dengan claim ter-reservasi (
iss,aud,iat,exp,nbf,sub,jti,sid,snm,fet,scope,act,fp,scopes— lihatBpmlib\SauthServer\Support\ReservedClaims::LIST) melemparBpmlib\SauthServer\Exceptions\ReservedClaimExceptionsaat issuance — bug developer/config, tidak di-catch internal. - Berlaku identik di kedua flow issuance — OAuth/Passport flow (
AccessTokenRepository→AccessToken::setExtraClaims()) dan ticketbooth flow (ServiceTokenIssuer, JWT dibangun manual) — keduanya validasi lewatReservedClaims::assertNoCollision()yang sama, tanpa duplikasi logic. - Tidak ikut di token exchange.
TokenExchangeGranthanya copysid/snm/fet/fpdari subject token — custom claim otomatis ter-strip. sauth-clienttidak berubah — consumer decode payload JWT sendiri setelah middleware verifikasi signature:php$payload = json_decode(base64_decode(strtr(explode('.', $token)[1], '-_', '+/')), true); $gender = $payload['gender'] ?? null;
Grant Types
| Grant | Use Case |
|---|---|
authorization_code + PKCE | Login user via GWA atau GWC |
client_credentials | M2M — service-to-service |
token_exchange (RFC 8693) | Service chaining — pertahankan context user asli |
client_credentials + parameter resource: 1p M2M caller dapat menyertakan resource untuk menentukan aud di JWT. Tanpa ini, aud = default_audience.
Token exchange dinonaktifkan secara default (SAUTH_TOKEN_EXCHANGE_ENABLED=false). Hanya relevan di GWA. Parameter resource wajib (RFC 9700 — satu token, satu resource server).
Key Management
Setiap gateway generate dan memiliki key pair sendiri — private key GWA dan GWC sepenuhnya terpisah. Claim iss pada token memberitahu sauth-client public key mana yang dipakai untuk verifikasi.
php artisan bpm:sauth:keygen # algo dari SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256
php artisan bpm:sauth:keygen ES256 --path=keys # storage/keys/oauth-{private,public}.key
php artisan bpm:sauth:keygen EdDSA --file-prefix=gwa| Algorithm | Format key |
|---|---|
| RS256/384/512 | PEM (-----BEGIN RSA PRIVATE KEY-----) |
| ES256/384/512 | PEM (-----BEGIN EC PRIVATE KEY-----) |
| EdDSA | Base64 single-line (bukan PEM) |
KeyLoader mendeteksi mismatch format/algo sebelum operasi kriptografi dan melempar RuntimeException dengan command perbaikan.
NOTE
Laravel hanya mengcover /storage/*.key di .gitignore. Key di subdirektori (--path=keys) butuh entry manual: /storage/keys/*.key. bpm:sauth:keygen mencetak warning otomatis saat --path non-root.
Artisan Commands
- bpm:sauth:init
- bpm:sauth:migration
- bpm:sauth:keygen
- bpm:sauth:client
- bpm:sauth:apikey:issue
- bpm:sauth:apikey:list
- bpm:sauth:apikey:revoke
bpm:sauth:init
Setup sauth-server dalam satu langkah: Passport migrations, sauth migrations, migrate, dan keygen. Idempotent — aman dijalankan berulang.
Signature:
php artisan bpm:sauth:init [--force] [--algo=] [--path=] [--file-prefix=]Flags:
| Flag | Description |
|---|---|
--force | Regenerate key pair meskipun sudah ada (dengan konfirmasi prompt) |
Options:
| Option | Default | Description |
|---|---|---|
--algo=<string> | SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 | Algoritma key generation |
--path=<string> | storage root | Direktori output key relatif ke storage_path() |
--file-prefix=<string> | oauth | Prefix nama file key |
Contoh:
php artisan bpm:sauth:init
php artisan bpm:sauth:init --force
php artisan bpm:sauth:init --algo=ES256
php artisan bpm:sauth:init --algo=EdDSA --path=keys --file-prefix=gwabpm:sauth:migration
Publish dan jalankan sauth migrations secara idempotent — tanpa keygen. Gunakan saat upgrade rolling.
Signature:
php artisan bpm:sauth:migrationLangkah-langkah (masing-masing dengan cek skip):
is_first_party+service_codecolumns — fresh install: satu stub; upgrade dari ≤v0.3.0: dua stub terpisahis_oidccolumn dioauth_clientssauth_jti_tokenstable- Jalankan
migrate --forcejika ada yang dipublish
bpm:sauth:keygen
Generate key pair (private + public) untuk algoritma yang dipilih.
Signature:
php artisan bpm:sauth:keygen [algo] [--path=] [--file-prefix=] [--force]Arguments:
| Argument | Required | Description |
|---|---|---|
algo | No | RS256, RS384, RS512, ES256, ES384, ES512, EdDSA. Default: SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 |
Flags:
| Flag | Description |
|---|---|
--force | Overwrite file yang sudah ada (dengan konfirmasi prompt) |
Options:
| Option | Default | Description |
|---|---|---|
--path=<string> | storage root | Direktori output relatif ke storage_path() |
--file-prefix=<string> | oauth | Prefix — menghasilkan {prefix}-private.key dan {prefix}-public.key |
Contoh:
php artisan bpm:sauth:keygen # storage/oauth-{private,public}.key
php artisan bpm:sauth:keygen ES256 --path=keys # storage/keys/oauth-{private,public}.key
php artisan bpm:sauth:keygen EdDSA --file-prefix=gwa # storage/gwa-{private,public}.key
php artisan bpm:sauth:keygen RS256 --force # overwrite dengan konfirmasibpm:sauth:client
Buat OAuth client baru. Secret hanya tersedia sekali di output — simpan segera.
Signature:
php artisan bpm:sauth:client [--first] [--user] [--oidc]Flags:
| Flag | Description |
|---|---|
--first | First-party (is_first_party=true); prompt service code (wajib diisi) |
--user | Grant authorization_code + PKCE; default client_credentials tanpa flag ini |
--oidc | OIDC client (is_oidc=true); wajib bersama --user; error jika --first |
Kombinasi:
| Kombinasi | Perilaku |
|---|---|
| (tanpa flag) | 3p M2M — client_credentials |
--first | 1p M2M — prompt service code |
--user | 3p user — authorization_code + PKCE |
--first --user | 1p user (GWA/GWC frontend) — prompt service code |
--user --oidc | 3p OIDC user — id_token diaktifkan |
bpm:sauth:apikey:issue
Terbitkan API key JWT baru untuk partner machine integration.
Signature:
php artisan bpm:sauth:apikey:issue {app} {aud} [--scopes=] [--issuer=]Arguments:
| Argument | Required | Description |
|---|---|---|
app | Yes | Nama integrasi — menjadi sid di JWT (contoh: acme-erp) |
aud | Yes | Target service code (contoh: srf) |
Options:
| Option | Default | Description |
|---|---|---|
--scopes=<string> | '' | Space-separated scopes (contoh: "srf.read srf.write") |
--issuer=<string> | 'cli' | Audit trail — siapa yang menerbitkan |
JWT ditampilkan sekali ke stdout. Tidak bisa diambil kembali kecuali melalui webmaster decrypt endpoint di GWA.
bpm:sauth:apikey:list
Tampilkan semua API key JWT yang pernah diterbitkan.
Signature:
php artisan bpm:sauth:apikey:listKolom output: jti, app, aud, issuer, created_at, revoked_at.
bpm:sauth:apikey:revoke
Cabut API key JWT.
Signature:
php artisan bpm:sauth:apikey:revoke {jti} [--reason=]Arguments:
| Argument | Required | Description |
|---|---|---|
jti | Yes | UUID jti dari API key yang akan dicabut |
Options:
| Option | Default | Description |
|---|---|---|
--reason=<string> | null | Alasan pencabutan untuk audit trail |
Menandai revoked_at di sauth_jti_tokens dan fire JtiTokenRevoked event. Tidak memblokir otomatis di resource server — jalankan di setiap resource server yang menerima key tersebut:
php artisan bpm:sauth:jti block {jti} --reason="..."API Reference
Daftar Isi:
- Contracts: MicroserviceTokenClaimsProviderInterface, OidcClaimsProviderInterface
- Traits: FetchesClientToken
- Services: ClientRegistrar, ServiceTokenIssuer, ApiKeyIssuer, IdTokenIssuer
- Models: SauthJtiToken
- Support: KeyLoader
- Events: JtiTokenRevoked
Contracts
MicroserviceTokenClaimsProviderInterface
Namespace: Bpmlib\SauthServer\Contracts\MicroserviceTokenClaimsProviderInterface
Contract yang diimplementasikan setiap gateway. Dipanggil hanya untuk user token (1p dan 3p) — 1p M2M, 3p M2M, dan API key JWT tidak memanggil interface ini.
getClaimsForUser()
public function getClaimsForUser(?Authenticatable $user, string $clientId): arrayParameters
| Name | Type | Default | Description |
|---|---|---|---|
$user | Authenticatable|null | - | User yang login; null saat M2M flow |
$clientId | string | - | NEW v0.4.0 — identifier client OAuth yang meminta token; tersedia dari kedua call site (OAuth flow via AccessTokenRepository, ticketbooth flow via ServiceTokenIssuer) |
Returns: array{sid: string, snm: string|null, fet: string|list<string>, ...<string, mixed>}
| Key | Type | Description |
|---|---|---|
sid | string | User ID |
snm | string|null | Display name |
fet | string|array | Permissions — "wm" = bypass penuh; array = atomic permissions |
| (key lain) | mixed | NEW v0.4.0 — key tambahan apa pun menjadi custom claim root-level di JWT. Lihat Custom Claims |
Untuk $user === null: return value diabaikan — tulis implementasi minimal untuk interface compliance.
WARNING
Breaking (v0.4.0): parameter $clientId ditambahkan sebagai parameter wajib kedua. Implementasi lama dengan satu parameter akan fatal error (Declaration must be compatible) saat class di-load — update signature sebelum upgrade.
OidcClaimsProviderInterface
TIP
NEW v0.3.4
Namespace: Bpmlib\SauthServer\Contracts\OidcClaimsProviderInterface
Contract opsional untuk extra OIDC claims di /userinfo dan id_token. Jika tidak di-bind, /userinfo tetap berfungsi — mengembalikan sub + name saja.
getOidcClaims()
public function getOidcClaims(Authenticatable $user): arrayReturns: array<string, mixed> — extra OIDC claims. Jangan sertakan sub atau name.
Contoh:
return ['email' => $user->email, 'email_verified' => $user->email_verified_at !== null];Traits
FetchesClientToken
Namespace: Bpmlib\SauthServer\Concerns\FetchesClientToken
Trait untuk worker/job yang memanggil service lain menggunakan M2M token. Menangani fetch → cache → bypass otomatis.
Cache key: sauth_client_token_{client_id}_{audience}. TTL = 80% dari expires_in server (real mode) atau 720 detik (bypass mode). Konfigurasi dibaca dari sauth-server.client_credentials.
clientToken()
protected function clientToken(string $audience = ''): stringKembalikan M2M token valid untuk audience yang dituju.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$audience | string | '' | Target service code. Default ke sauth-server.default_audience jika kosong |
Returns: string — Bearer token siap pakai
clientToken('srf') dan clientToken('pnr') di-cache secara independen.
forgetClientToken()
protected function forgetClientToken(string $audience = ''): voidHapus token dari cache. Panggil setelah 401 dari downstream service agar token baru di-fetch pada request berikutnya.
Services
ClientRegistrar
Namespace: Bpmlib\SauthServer\Services\ClientRegistrar
Manajemen OAuth client lifecycle — digunakan oleh bpm:sauth:client dan gateway admin controller. Auto-bound sebagai singleton.
create()
public function create(
string $name,
bool $isFirstParty = false,
bool $isUserClient = false,
string $redirectUri = '',
?string $serviceCode = null,
bool $isOidc = false,
): ClientParameters
| Name | Type | Default | Description |
|---|---|---|---|
$name | string | - | Nama client |
$isFirstParty | bool | false | is_first_party = true |
$isUserClient | bool | false | true = authorization_code; false = client_credentials |
$redirectUri | string | '' | Wajib jika $isUserClient = true |
$serviceCode | string|null | null | Service code 1p resource server; null/'' = tidak diset |
$isOidc | bool | false | is_oidc = true — hanya bermakna untuk 3p authorization_code client |
Returns: Client — plainSecret tersedia di instance ini. Tidak bisa diambil lagi.
update()
public function update(
Client $client,
string $name,
?string $redirectUri = null,
?bool $isFirstParty = null,
?string $serviceCode = null,
?bool $isOidc = null,
): ClientParameters
| Name | Type | Default | Description |
|---|---|---|---|
$client | Client | - | Client yang diupdate |
$name | string | - | Nama baru |
$redirectUri | string|null | null | null = tidak diubah |
$isFirstParty | bool|null | null | null = tidak diubah |
$serviceCode | string|null | null | null = tidak diubah; '' = hapus; non-empty = set |
$isOidc | bool|null | null | null = tidak diubah |
Returns: Client — fresh instance dari database
delete()
public function delete(Client $client): voidRevoke semua token aktif dan hapus record oauth_clients.
rotateSecret()
public function rotateSecret(Client $client): ClientGenerate client secret baru. Caller bertanggung jawab untuk audit logging.
Returns: Client — plainSecret tersedia. Tidak bisa diambil lagi.
list()
public function list(?bool $firstParty = null): \Illuminate\Database\Eloquent\CollectionParameters
| Name | Type | Default | Description |
|---|---|---|---|
$firstParty | bool|null | null | true = 1p saja; false = 3p saja; null = semua |
Returns: Collection<int, Client>
ServiceTokenIssuer
Namespace: Bpmlib\SauthServer\Services\ServiceTokenIssuer
Ticketbooth issuance — tidak melalui Passport grant flow, tidak ada DB write, tidak ada refresh token. Auto-bound sebagai singleton.
issue()
public function issue(Authenticatable $user, string $targetService): arrayParameters
| Name | Type | Default | Description |
|---|---|---|---|
$user | Authenticatable | - | User dari $request->user() |
$targetService | string | - | Service code atau client ID yang terdaftar di oauth_clients |
Returns
| Key | Type | Description |
|---|---|---|
access | string | Signed JWT siap pakai |
expires_in | int | TTL dalam detik (token_ttl.access) |
Throws:
RuntimeException—issuer_codebelum dikonfigurasi\InvalidArgumentException—$targetServicetidak ditemukan sebagaiservice_codedi 1p client aktif
aud di JWT selalu service_code dari record yang ditemukan — tidak pernah raw client ID. Gateway controller harus catch InvalidArgumentException dan return 422.
Custom claims (v0.4.0+): JWT dibangun manual di sini (bukan lewat AccessToken entity), jadi extra claim di-validasi dan di-emit inline — tapi lewat helper ReservedClaims::assertNoCollision() yang sama dengan AccessToken, jadi hasil akhirnya identik dengan token dari OAuth flow untuk user yang sama. Lihat Custom Claims.
ApiKeyIssuer
TIP
Updated v0.3.3: algo dan key terpisah via SAUTH_APIKEY_SIGN_ALGO / SAUTH_APIKEY_PRIVATE_KEY_FILE
Namespace: Bpmlib\SauthServer\Services\ApiKeyIssuer
Menerbitkan API key JWT (long-lived, tanpa expiry). Auto-bound sebagai singleton.
issue()
public function issue(string $app, string $aud, string $issuer, array $scopes): stringParameters
| Name | Type | Default | Description |
|---|---|---|---|
$app | string | - | Nama integrasi — menjadi sid di JWT |
$aud | string | - | Target service code |
$issuer | string | - | Audit trail — user SID atau 'cli' |
$scopes | array | - | Array scope (['srf.read', 'srf.write']) |
Returns: string — plaintext JWT, hanya tersedia saat ini. Disimpan encrypted di sauth_jti_tokens.
Claims yang di-embed: iss = APP_URL, aud, fp = '3p', sid = $app, jti (UUID baru), scope, iat. Tidak ada exp.
IdTokenIssuer
TIP
NEW v0.3.4 | Updated v0.3.5: nonce otomatis diteruskan dari authorization request
Namespace: Bpmlib\SauthServer\Services\IdTokenIssuer
Build dan tandatangani OIDC id_token. Dipanggil otomatis oleh OidcBearerTokenResponse — tidak perlu diinject manual. Auto-bound sebagai singleton.
issue()
public function issue(AccessToken $token): stringReturns: string — signed id_token JWT
Claims: iss = APP_URL, sub = sid, aud = client_id, iat/exp, name (jika ada), nonce (v0.3.5: otomatis dari authorization request).
Models
SauthJtiToken
Namespace: Bpmlib\SauthServer\Models\SauthJtiToken
Eloquent model untuk sauth_jti_tokens. Primary key UUID ($incrementing = false, $keyType = 'string'). Skema tabel lihat Tabel.
revoke()
public function revoke(?string $reason = null): voidUpdate revoked_at = now() dan revoke_reason, lalu fire JtiTokenRevoked event.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$reason | string|null | null | Alasan pencabutan |
Support
KeyLoader
Namespace: Bpmlib\SauthServer\Support\KeyLoader
Helper untuk memuat lcobucci/jwt key dengan format-aware factory dan deteksi mismatch key/algo sebelum operasi kriptografi.
final class KeyLoader
{
public static function privateKey(string $path, string $algo): InMemory;
public static function publicKey(string $path, string $algo): InMemory;
}Parameters (kedua method):
| Name | Type | Description |
|---|---|---|
$path | string | Path relatif ke storage_path() |
$algo | string | 'RS256', 'ES256', 'EdDSA', dll. |
Returns: Lcobucci\JWT\Signer\Key\InMemory — EdDSA: base64Encoded(); RS*/ES*: file().
Throws: RuntimeException — file tidak ditemukan, atau format key tidak cocok dengan algo.
Events
JtiTokenRevoked
Namespace: Bpmlib\SauthServer\Events\JtiTokenRevoked
Difire oleh SauthJtiToken::revoke(). GWA dapat mendaftarkan listener di EventServiceProvider untuk propagasi revocation otomatis ke resource server via M2M token.
class JtiTokenRevoked
{
public function __construct(public readonly SauthJtiToken $token) {}
}Examples
Contains:
- 1. ClaimsProvider — GWA dan GWC
- 2. FetchesClientToken di Job
- 3. Ticketbooth Controller dengan ServiceTokenIssuer
- 4. Token Exchange
- 5. API Key JWT — Issue, Revoke, Block
- 6. ClientRegistrar — Buat, Update, List
- 7. OIDC Client Setup
- 8. Key Management dengan bpm:sauth:keygen
1. ClaimsProvider — GWA dan GWC
GWA menggunakan atomic permissions dari getFeatures(), GWC menggunakan role code tunggal.
// GWA — internal users
class GwaClaimsProvider implements MicroserviceTokenClaimsProviderInterface
{
public function getClaimsForUser(?Authenticatable $user, string $clientId): array
{
if ($user === null) {
return ['sid' => '', 'snm' => null];
}
return [
'sid' => (string) $user->id,
'snm' => $user->name,
'fet' => $user->is_webmaster ? 'wm' : $user->getFeatures(),
];
}
}
// GWC — external customers
class GwcClaimsProvider implements MicroserviceTokenClaimsProviderInterface
{
public function getClaimsForUser(?Authenticatable $user, string $clientId): array
{
if ($user === null) {
return ['sid' => '', 'snm' => null];
}
return [
'sid' => (string) $user->id,
'snm' => $user->name,
'fet' => [$user->user_type], // 'psn', 'cpy', atau 'mit'
];
}
}2. FetchesClientToken di Job
Job yang memanggil dua service berbeda — token masing-masing di-cache independen.
<?php
namespace App\Jobs;
use Bpmlib\SauthServer\Concerns\FetchesClientToken;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Http;
class SyncJob implements ShouldQueue
{
use Queueable, FetchesClientToken;
public function handle(): void
{
$srfRes = Http::withToken($this->clientToken('srf'))
->get('https://srf.internal/api/data');
if ($srfRes->status() === 401) {
$this->forgetClientToken('srf');
$this->release(5);
return;
}
$pnrRes = Http::withToken($this->clientToken('pnr'))
->post('https://pnr.internal/api/sync', $srfRes->json());
if ($pnrRes->status() === 401) {
$this->forgetClientToken('pnr');
$this->release(5);
}
}
}Di local dev, set SAUTH_BYPASS=true — tidak ada GWA yang dibutuhkan.
3. Ticketbooth Controller dengan ServiceTokenIssuer
Controller thin di gateway. ServiceTokenIssuer diinject via constructor DI.
<?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']);
try {
return response()->json(
$this->issuer->issue($request->user(), $request->input('target'))
);
} catch (\InvalidArgumentException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
}
}// routes/api.php — wajib auth:sanctum
Route::middleware('auth:sanctum')
->post('/api/sauth/token', [ServiceTokenController::class, 'issue']);Response: { "access": "<JWT>", "expires_in": 900 }. Tidak ada refresh token — session gateway adalah long-lived credential.
4. Token Exchange
SRF menukar user token (aud=srf) ke token baru untuk memanggil PNR. Token hasil: 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,
'subject_token_type' => 'urn:ietf:params:oauth:token-type:access_token',
'resource' => 'pnr', // wajib (RFC 9700)
]);
$delegatedToken = $response->json('access_token');Aktifkan di GWA: SAUTH_TOKEN_EXCHANGE_ENABLED=true.
5. API Key JWT — Issue, Revoke, Block
Via Artisan:
php artisan bpm:sauth:apikey:issue acme-erp srf --scopes="srf.read srf.write" --issuer=admin
php artisan bpm:sauth:apikey:list
php artisan bpm:sauth:apikey:revoke 550e8400-e29b-41d4-a716-446655440000 --reason="Compromised"
# Di setiap resource server:
php artisan bpm:sauth:jti block 550e8400-e29b-41d4-a716-446655440000Via controller (programmatic):
use Bpmlib\SauthServer\Services\ApiKeyIssuer;
class ApiKeyController extends Controller
{
public function __construct(private ApiKeyIssuer $issuer) {}
public function store(Request $request): JsonResponse
{
$request->validate(['app' => 'required|string', 'aud' => 'required|string', 'scopes' => 'required|array']);
$token = $this->issuer->issue(
app: $request->string('app'),
aud: $request->string('aud'),
issuer: (string) $request->user()->sid,
scopes: $request->array('scopes'),
);
return response()->json(['token' => $token]);
}
}JWT yang diterbitkan tidak memiliki exp — aktif sampai dicabut via jti blacklist di resource server.
6. ClientRegistrar — Buat, Update, List
use Bpmlib\SauthServer\Services\ClientRegistrar;
// Buat 1p resource server client dengan service_code
$client = $registrar->create(name: 'SRF Service', isFirstParty: true, serviceCode: 'srf');
// $client->plainSecret — simpan sekarang, tidak bisa diambil lagi
// Update service_code
$registrar->update($client, 'SRF Service', serviceCode: 'srf');
// Hapus service_code (tidak lagi jadi target ticketbooth)
$registrar->update($client, 'SRF Service', serviceCode: '');
// Rotasi secret
$client = $registrar->rotateSecret($client);
$newSecret = $client->plainSecret;
// List clients
$all = $registrar->list();
$firstParty = $registrar->list(firstParty: true);
$thirdParty = $registrar->list(firstParty: false);7. OIDC Client Setup
TIP
NEW v0.3.4
# 1. Aktifkan dan jalankan migrasi
# .env: SAUTH_OIDC_ENABLED=true
php artisan bpm:sauth:migration
# 2. Buat OIDC client
php artisan bpm:sauth:client --user --oidc// 3. Provider extra OIDC claims (opsional)
class GwaOidcClaimsProvider implements OidcClaimsProviderInterface
{
public function getOidcClaims(Authenticatable $user): array
{
return ['email' => $user->email, 'email_verified' => $user->email_verified_at !== null];
}
}
// AppServiceProvider::register()
$this->app->bind(OidcClaimsProviderInterface::class, GwaOidcClaimsProvider::class);Flow authorization_code + openid scope mengembalikan access_token + id_token. Claims id_token: sub, name, email, iss = APP_URL, aud = client_id.
8. Key Management dengan bpm:sauth:keygen
# RSA key pair — default
php artisan bpm:sauth:keygen RS256
# → storage/oauth-private.key + oauth-public.key
# EC key pair di subdirektori
php artisan bpm:sauth:keygen ES256 --path=keys
# → storage/keys/oauth-{private,public}.key
# Tambahkan ke .gitignore: /storage/keys/*.key
# Key pair terpisah untuk API key JWT
php artisan bpm:sauth:keygen ES256 --path=keys --file-prefix=apikey
# → storage/keys/apikey-{private,public}.key# Setup dasar
SAUTH_PRIVATE_KEY_FILE=oauth-private.key
SAUTH_ALGO=RS256
# Gateway pakai ES256, resource server masih RS256
SAUTH_SIGN_ALGO=ES256
SAUTH_PRIVATE_KEY_FILE=keys/oauth-private.key
# Key pair terpisah untuk API key JWT
SAUTH_APIKEY_SIGN_ALGO=ES256
SAUTH_APIKEY_PRIVATE_KEY_FILE=keys/apikey-private.keyLaravel Integration
Service Provider
Auto-registered via package discovery. Mendaftarkan:
- Custom
AccessTokenRepository— injectiss,fp,sid,snm,fet,scope,actsesuai token type ClientRegistrar,ServiceTokenIssuer,ApiKeyIssuer,IdTokenIssuersebagai singleton- Artisan commands:
bpm:sauth:init,bpm:sauth:migration,bpm:sauth:keygen,bpm:sauth:client,bpm:sauth:apikey:issue,bpm:sauth:apikey:list,bpm:sauth:apikey:revoke TokenExchangeGrantjikatoken_exchange.enabled = true- Saat
oidc.enabled = true: endpoint discovery, JWKS, userinfo;OidcBearerTokenResponse;OidcAuthCodeRepository(nonce passthrough v0.3.5); boot-time schema guard untuk kolomis_oidc
Published Assets
php artisan vendor:publish --tag=sauth-server-config
# → config/sauth-server.phpRequired Migrations
Di-publish otomatis oleh bpm:sauth:init atau bpm:sauth:migration — tidak perlu menulis manual:
is_first_party+service_code— fresh install: satu stub; upgrade dari ≤v0.3.0: dua stub terpisahis_oidc— kolom boolean dioauth_clientsuntuk OIDC clientsauth_jti_tokens— audit table API key JWT
Deployment Notes
IMPORTANT
Deploy kode setelah populate service_code. Deploy kode sebelum populate menyebabkan semua ticketbooth request gagal dengan 422.
# Langkah 1: tambahkan kolom
php artisan bpm:sauth:migration
# Langkah 2: populate service_code pada 1p resource server client
php artisan tinker
>>> \Laravel\Passport\Client::where('is_first_party', true)->whereNull('service_code')->get()
>>> \Laravel\Passport\Client::find($id)->update(['service_code' => 'srf'])
# Langkah 3: deploy kode v0.3.1+
# ServiceTokenIssuer validasi aktif setelah iniLinks
- Registry: bpmlib — Private Composer Repository