Skip to content

Sauth Server

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

Versi: 0.3.7 Changelog

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

bash
php artisan bpm:sauth:init               # Inisialisasi: migrasi, is_oidc column, sauth_jti_tokens, OAuth keys
php artisan bpm:sauth:migration          # Publish dan jalankan sauth migrations saja (tanpa keygen)
php artisan bpm:sauth:keygen             # Generate key pair untuk algoritma tertentu (RS256, ES256, EdDSA, dll.)
php artisan bpm:sauth:client             # Buat OAuth client baru — --oidc untuk OIDC client (v0.3.4)
php artisan bpm:sauth:apikey issue       # Terbitkan API key JWT untuk partner
php artisan bpm:sauth:apikey list        # Lihat semua API key JWT yang diterbitkan
php artisan bpm:sauth:apikey revoke      # Cabut API key JWT

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, publish sauth migrations (is_first_party, service_code, sauth_jti_tokens), jalankan migrate, dan generate OAuth key pair. Lihat bpm:sauth:init untuk detail.

IMPORTANT

Sebelum deploy v0.3.1: jalankan bpm:sauth:migration terlebih dulu, lalu populate service_code di semua 1p resource server client. ServiceTokenIssuer akan menolak target yang belum terdaftar dengan InvalidArgumentException. Lihat Deployment Order.


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 ['sid' => '', 'snm' => null]; // M2M — tidak dipanggil untuk 1p M2M di v0.2.0+
        }

        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 untuk user token
  • Di v0.2.0+, getClaimsForUser(null) tidak dipanggil untuk 1p M2M (client_credentials) — library set sid = client_id langsung; tidak ada fet atau snm di token M2M
  • Null case tetap diperlukan untuk interface compliance; nilai return-nya diabaikan
  • '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 shared dengan sauth-client Lihat selengkapnya
sign_algorithmstring|nullnullOverride algoritma signing sauth-server — mengalahkan SAUTH_ALGO untuk token signing dan key generation. Null = fallback ke algorithm. Lihat selengkapnya
apikey_sign_algorithmstring|nullnullOverride algo signing khusus API key JWT — fallback: SAUTH_APIKEY_SIGN_ALGOSAUTH_SIGN_ALGOSAUTH_ALGORS256. Null = ikuti chain reguler. Lihat selengkapnya
apikey_private_key_filestring|nullnullOverride path private key khusus API key JWT — fallback: SAUTH_APIKEY_PRIVATE_KEY_FILESAUTH_PRIVATE_KEY_FILEoauth-private.key. 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 dan bpm:sauth:init 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
oidc.enabledboolfalseAktifkan OIDC support — mendaftarkan endpoint /.well-known/openid-configuration, /.well-known/jwks.json, /userinfo, dan mengaktifkan id_token issuance untuk client is_oidc=true. Lihat selengkapnya

Environment Variables

dotenv
SAUTH_ISSUER_CODE=gwa
SAUTH_DEFAULT_AUDIENCE=internal-services
SAUTH_ALGO=RS256
SAUTH_SIGN_ALGO=               # opsional — override SAUTH_ALGO untuk signing di sauth-server saja
SAUTH_APIKEY_SIGN_ALGO=        # opsional — override algo signing khusus API key JWT
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
SAUTH_APIKEY_PRIVATE_KEY_FILE= # opsional — override key file khusus API key JWT

# 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

# OIDC support — aktifkan discovery, JWKS, userinfo, dan id_token issuance
# Jalankan bpm:sauth:migration terlebih dulu untuk menambah kolom is_oidc
SAUTH_OIDC_ENABLED=false
bypass

Saat true dan app()->isLocal(), FetchesClientToken::clientToken() tidak memanggil GWA. Sebuah JWT palsu dibuat secara lokal menggunakan HMAC-SHA256 dengan throwaway key (signature tidak diverifikasi di sisi penerima saat bypass aktif) dengan claim: iss = issuer_code, aud = target audience, sid = client_id, fp = '1p', expiry = 15 menit. JWT palsu ini di-cache 720 detik. Tidak ada fet, snm, atau scope — sesuai dengan shape token 1p M2M nyata.

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.


oidc.enabled

TIP

NEW v0.3.4

Saat true, SauthServerServiceProvider mendaftarkan tiga endpoint OIDC secara otomatis — tidak perlu perubahan di routes/api.php consuming app:

EndpointKeterangan
GET /.well-known/openid-configurationDiscovery document — issuer, endpoints, algoritma, scopes
GET /.well-known/jwks.jsonPublic key dalam format JWK Set
GET /userinfoKembalikan sub + name; claim tambahan via OidcClaimsProviderInterface

Juga mengaktifkan id_token issuance untuk client yang ditandai is_oidc = true dalam authorization_code flow + openid scope. Kolom is_oidc harus ada di oauth_clients sebelum flag ini diaktifkan — jalankan bpm:sauth:migration terlebih dulu. Boot-time guard aktif: jika kolom belum ada, RuntimeException dilempar dengan pesan actionable.

dotenv
SAUTH_OIDC_ENABLED=true
bash
php artisan bpm:sauth:migration   # tambah kolom is_oidc ke oauth_clients

sign_algorithm

TIP

NEW v0.3.2

Override algoritma signing khusus sauth-server. Ketika di-set, mengalahkan SAUTH_ALGO untuk token signing (SignerResolver) dan key generation (bpm:sauth:keygen, bpm:sauth:init). SAUTH_ALGO tetap menjadi shared fallback yang dibaca kedua library.

Urutan resolusi:

KonteksUrutan
Token signingSAUTH_SIGN_ALGOSAUTH_ALGORS256
bpm:sauth:keygen (tanpa argumen)SAUTH_SIGN_ALGOSAUTH_ALGORS256
bpm:sauth:init (tanpa --algo)SAUTH_SIGN_ALGOSAUTH_ALGORS256

Kapan digunakan: saat gateway ingin beralih algoritma signing (misalnya ke ES256) tanpa mengubah SAUTH_ALGO yang juga dibaca resource server — resource server terus membaca SAUTH_ALGO sampai siap diperbarui secara independen.

dotenv
SAUTH_SIGN_ALGO=ES256   # gateway sign dengan ES256
SAUTH_ALGO=RS256        # resource server masih RS256 sampai diperbarui

Setelah semua resource server siap: pindahkan ke SAUTH_ALGO=ES256 dan hapus SAUTH_SIGN_ALGO.


apikey_sign_algorithm

TIP

NEW v0.3.3

Override algoritma signing khusus untuk API key JWT yang diterbitkan ApiKeyIssuer. Tidak memengaruhi token Passport reguler (user token, M2M token, token exchange).

Urutan resolusi:

KonteksUrutan
API key JWT signingSAUTH_APIKEY_SIGN_ALGOSAUTH_SIGN_ALGOSAUTH_ALGORS256
Token Passport regulerSAUTH_SIGN_ALGOSAUTH_ALGORS256 (tidak berubah)

Ketika tidak di-set (null), fallback ke chain reguler — perilaku identik dengan v0.3.2. Berguna ketika partner perlu memverifikasi API key JWT secara langsung (tanpa sauth-client) dengan algoritma atau key rotation yang independen dari key utama gateway.

dotenv
SAUTH_APIKEY_SIGN_ALGO=ES256   # API key JWT sign dengan ES256
SAUTH_SIGN_ALGO=RS256          # token Passport reguler tetap RS256

Gunakan bersama SAUTH_APIKEY_PRIVATE_KEY_FILE untuk key pair yang sepenuhnya terpisah.


apikey_private_key_file

TIP

NEW v0.3.3

Override path private key khusus untuk API key JWT yang diterbitkan ApiKeyIssuer. Path bersifat relatif terhadap storage_path().

Urutan resolusi:

KonteksUrutan
API key JWT keySAUTH_APIKEY_PRIVATE_KEY_FILESAUTH_PRIVATE_KEY_FILEoauth-private.key
Token Passport regulerSAUTH_PRIVATE_KEY_FILEoauth-private.key (tidak berubah)
dotenv
SAUTH_APIKEY_PRIVATE_KEY_FILE=keys/apikey-private.key
bash
# Generate key pair terpisah untuk API key JWT
php artisan bpm:sauth:keygen ES256 --path=keys --file-prefix=apikey
# → storage/keys/apikey-private.key + storage/keys/apikey-public.key

Distribusikan apikey-public.key ke partner sebagai kunci verifikasi — terpisah dari public key utama gateway. Deteksi mismatch key/algo via KeyLoader aktif untuk key ini juga.


algorithm

TIP

EdDSA ditambahkan di v0.3.1 | SAUTH_SIGN_ALGO override ditambahkan di v0.3.2

Algoritma signing JWT shared antara sauth-server dan sauth-client. Harus cocok dengan konfigurasi di sauth-client pada setiap consuming service — ketidakcocokan menyebabkan signature verification gagal di semua services.

Untuk mengubah algoritma gateway secara independen tanpa memperbarui resource server sekaligus, gunakan sign_algorithm (SAUTH_SIGN_ALGO) — lihat sign_algorithm.

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
  • 'EdDSA' — Ed25519 (key format base64, bukan PEM)

Use Case: Ganti ke ES256/ES384 untuk key size lebih kecil dan signing lebih cepat. EdDSA untuk key terkecil dengan performa terbaik. Pastikan semua services dan kedua gateways menggunakan algoritma yang sama sebelum mengganti.

Catatan EdDSA: key format berbeda dari RSA/EC — base64 single-line, bukan PEM. KeyLoader mendeteksi format otomatis dan melempar RuntimeException jika ada mismatch antara format file dan SAUTH_ALGO. Gunakan bpm:sauth:keygen EdDSA untuk generate key yang benar.


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
issstringIssuer — 1p token: kode gateway (gwa atau gwc); 3p token + API key JWT: full URL gateway (APP_URL) sesuai RFC 9068
audstringTarget service code (srf, pnr, pel, dll.)
iatintIssued-at timestamp — ada di semua token
expintExpiry timestamp — ada di semua token kecuali API key JWT; API key JWT tidak ada expiry, revocation via jti blacklist
fpstringParty marker — '1p' untuk first-party token, '3p' untuk third-party token; ada di semua token
sidstringUser ID (user token), client_id (M2M token), atau app name (API key JWT)
snmstringDisplay name user — tidak ada di M2M token dan API key JWT
fetstring|arrayPermissions user — "wm" bypass semua checks; array = daftar atomic permissions; tidak pernah ada di M2M token
scopestringSpace-separated scopes — ada di 3p user token, 3p M2M token, dan API key JWT; tidak ada di 1p token
jtistringUUID — hanya ada di API key JWT; digunakan sebagai blacklist key di setiap resource server
actobject|nullRFC 8693 actor — {sub: calling_client_id}; hanya ada di delegated token

Perbedaan claim berdasarkan tipe token:

Token typeissfpsidsnmfetscopejtiexp
1p user tokenissuer_code1puser IDtidak adatidak ada
3p user tokenAPP_URL3puser IDtidak ada
1p M2M tokenissuer_code1pclient_idtidak adatidak adatidak adatidak ada
3p M2M tokenAPP_URL3pclient_idtidak adatidak adatidak ada
API key JWTAPP_URL3papp nametidak adatidak adatidak ada

fp adalah penanda eksplisit party type — '1p' untuk first-party, '3p' untuk third-party. snm tetap membedakan user actor dari system actor (M2M). jti + tidak ada exp adalah penanda API key JWT.

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.

service_code — kolom baru di v0.3.1. Kolom nullable unique di oauth_clients yang mengidentifikasi service code 1p resource server. ServiceTokenIssuer memvalidasi target permintaan ticketbooth terhadap kolom ini sebelum menerbitkan token — target tanpa service_code terdaftar akan ditolak. Lihat Deployment Order v0.3.1.

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.

client_credentials — parameter resource (v0.3.7)

1p M2M caller dapat menyertakan parameter resource untuk menentukan aud di JWT yang diterbitkan. Tanpa parameter ini, aud jatuh ke sauth-server.default_audience.

POST /oauth/token
grant_type=client_credentials
&client_id=<client-id>
&client_secret=<client-secret>
&resource=pnr

Berlaku hanya untuk 1p M2M token (is_first_party = true). Penamaan resource konsisten dengan TokenExchangeGrant. Caller yang tidak mengirim resource tidak terpengaruh.

Key Management

TIP

bpm:sauth:keygen dan KeyLoader ditambahkan di v0.3.1

Setiap gateway generate dan memiliki 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

Generate key pair dengan bpm:sauth:keygen:

bash
php artisan bpm:sauth:keygen                                 # algo dari SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 (v0.3.2)
php artisan bpm:sauth:keygen RS256                           # storage/oauth-private.key + oauth-public.key
php artisan bpm:sauth:keygen ES256 --path=keys               # storage/keys/oauth-private.key + ...
php artisan bpm:sauth:keygen EdDSA --file-prefix=gwa         # storage/gwa-private.key + gwa-public.key
php artisan bpm:sauth:keygen RS256 --force                   # overwrite dengan konfirmasi prompt
AlgorithmFormat key
RS256/384/512PEM (-----BEGIN RSA PRIVATE KEY-----)
ES256/384/512PEM (-----BEGIN EC PRIVATE KEY-----)
EdDSABase64 single-line (bukan PEM)

Deteksi mismatch key/algo: KeyLoader mendeteksi ketidakcocokan format file dengan SAUTH_ALGO sebelum operasi kriptografi apapun dan melempar RuntimeException dengan pesan actionable:

Key at oauth-private.key is PEM but SAUTH_ALGO=EdDSA expects base64-encoded Ed25519.
Run: php artisan bpm:sauth:keygen EdDSA

Catatan .gitignore: Laravel hanya mengcover /storage/*.key by default. Jika key disimpan di subdirektori (--path=keys), tambahkan /storage/keys/*.key ke .gitignore. bpm:sauth:keygen mencetak warning otomatis saat --path non-root.


Artisan Commands

bpm:sauth:init

TIP

Updated v0.3.1: flag --algo, --path, --file-prefix ditambahkan; tidak lagi memanggil passport:keys Updated v0.3.2: resolusi algo tanpa --algo kini: SAUTH_SIGN_ALGOSAUTH_ALGORS256

Inisialisasi sauth-server dalam satu langkah: publish Passport migrations, publish sauth migrations (is_first_party, service_code, sauth_jti_tokens), jalankan migrate, dan generate OAuth key pair.

Signature:

bash
php artisan bpm:sauth:init [--force] [--algo=] [--path=] [--file-prefix=]

Options:

OptionDescription
--forceRegenerate OAuth keys meskipun sudah ada
--algo=Algoritma key generation — default: SAUTH_SIGN_ALGOSAUTH_ALGORS256
--path=Direktori output key relatif ke storage_path() — default: storage root
--file-prefix=Prefix nama file key — default: oauth

Command mengecek setiap langkah sebelum dijalankan — aman dijalankan berulang kali (idempotent). Langkah yang dijalankan secara berurutan:

  1. Publish Passport migrations → migrate (dilewati jika tabel oauth_clients sudah ada)
  2. Delegasi ke bpm:sauth:migration — menangani is_first_party, service_code, dan sauth_jti_tokens migrations (masing-masing dilewati jika sudah ada)
  3. Generate key pair via bpm:sauth:keygen (dilewati jika file sudah ada kecuali --force diberikan)
bash
php artisan bpm:sauth:init                              # setup pertama kali — algo dari config
php artisan bpm:sauth:init --force                      # regenerate OAuth keys (rotasi key)
php artisan bpm:sauth:init --algo=ES256                 # generate EC keys
php artisan bpm:sauth:init --algo=EdDSA --path=keys --file-prefix=gwa

bpm:sauth:migration

TIP

NEW v0.3.1

Publish dan jalankan tiga sauth migrations secara idempotent — tanpa keygen. Gunakan command ini saat upgrade rolling (tidak perlu generate ulang key).

Signature:

bash
php artisan bpm:sauth:migration

Langkah-langkah (masing-masing dengan cek skip):

  1. is_first_party column — fresh install: publish stub yang menambahkan keduanya (is_first_party + service_code) sekaligus. Dilewati jika kolom sudah ada.
  2. service_code column — upgrade path: publish stub yang menambahkan service_code saja jika is_first_party sudah ada tapi service_code belum. Dilewati otomatis pada fresh install (sudah ditangani langkah 1).
  3. is_oidc column — publish stub add_is_oidc_to_oauth_clients. Dilewati jika kolom sudah ada.
  4. sauth_jti_tokens table — publish JTI migration. Dilewati jika tabel sudah ada.
  5. Jalankan migrate --force sekali jika ada yang baru dipublish.
bash
# Gunakan saat upgrade tanpa keygen
php artisan bpm:sauth:migration

bpm:sauth:keygen

TIP

NEW v0.3.1 | Updated v0.3.2: argumen algo kini opsional — resolusi: arg → SAUTH_SIGN_ALGOSAUTH_ALGORS256

Generate key pair (private + public) untuk algoritma yang dipilih.

Signature:

bash
php artisan bpm:sauth:keygen [algo] [--path=] [--file-prefix=] [--force]

Arguments:

ArgumentDescription
algo(Opsional) Algoritma — RS256, RS384, RS512, ES256, ES384, ES512, EdDSA. Jika tidak diisi, resolusi: SAUTH_SIGN_ALGOSAUTH_ALGORS256

Options:

OptionDefaultDescription
--path=storage rootDirektori output relatif ke storage_path()
--file-prefix=oauthPrefix nama file — menghasilkan {prefix}-private.key dan {prefix}-public.key
--force-Overwrite file yang sudah ada (dengan konfirmasi prompt)

Output defaults:

  • storage/oauth-private.key
  • storage/oauth-public.key
bash
php artisan bpm:sauth:keygen RS256                           # RSA 4096-bit
php artisan bpm:sauth:keygen ES256 --path=keys               # EC prime256v1, ke storage/keys/
php artisan bpm:sauth:keygen EdDSA --file-prefix=gwa         # Ed25519, ke storage/gwa-private.key
php artisan bpm:sauth:keygen RS256 --force                   # overwrite dengan konfirmasi

Setelah generate, command mengingatkan untuk mengupdate SAUTH_PRIVATE_KEY_FILE di .env. Jika --path non-root, mencetak warning .gitignore.


bpm:sauth:client

TIP

Updated v0.3.4: flag --oidc ditambahkan; validasi kombinasi flag; prompt interaktif untuk --user

Buat OAuth client baru untuk ekosistem sauth.

Signature:

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

Options:

OptionDescription
--firstTandai sebagai first-party — skip consent screen, is_first_party=true, fet-based auth; prompt service code
--userGunakan authorization_code + PKCE; tanpa flag ini default ke client_credentials
--oidcTandai sebagai OIDC client (is_oidc=true) — menerbitkan id_token; wajib dipakai bersama --user

Kombinasi flag:

KombinasiPerilaku
(tanpa flag)3p M2M — client_credentials, scope-based
--first1p M2M — client_credentials, fet-based; prompt service code
--user3p user — authorization_code + PKCE; prompt "Issue OIDC id_token?"
--first --user1p user — frontend GWA/GWC; prompt service code
--user --oidc3p OIDC user — authorization_code + PKCE + id_token; skip prompt
--oidc --firstHard fail — OIDC dan first-party kontradiksi
--oidc (tanpa --user)Hard failid_token hanya ada di authorization_code flow

Saat is_oidc=true dan SAUTH_OIDC_ENABLED=false, client tetap dibuat dengan peringatan. Command prompt untuk nama client, redirect URI jika --user, dan service code jika --first (wajib). Secret hanya tersedia sekali, simpan segera.


bpm:sauth:apikey

Kelola API key JWT — long-lived signed JWT untuk partner machine integration yang stabil. Tidak ada OAuth flow, tidak ada expiry, revocation via jti blacklist.

bpm:sauth:apikey issue

bash
php artisan bpm:sauth:apikey issue {app} {aud} [--scopes=] [--issuer=]
Argument/OptionDescription
{app}Nama integrasi (menjadi sid di JWT) — contoh: acme-erp
{aud}Kode target service — contoh: srf
--scopesSpace-separated scopes — contoh: "srf.read srf.write"
--issuerAudit trail — siapa yang menerbitkan; default cli

Mendelegasikan ke ApiKeyIssuer::issue(). JWT ditampilkan sekali ke stdout dengan peringatan untuk menyimpannya segera — tidak bisa diambil kembali tanpa webmaster decrypt endpoint.

bash
php artisan bpm:sauth:apikey issue acme-erp srf --scopes="srf.read srf.write" --issuer=admin

bpm:sauth:apikey list

bash
php artisan bpm:sauth:apikey list

Menampilkan tabel dengan kolom: jti, app, aud, issuer, created_at, revoked_at. Berguna untuk audit dan menemukan jti sebelum melakukan revoke.

bpm:sauth:apikey revoke

bash
php artisan bpm:sauth:apikey revoke {jti} [--reason=]
Argument/OptionDescription
{jti}UUID jti dari API key yang akan dicabut
--reasonAlasan pencabutan untuk audit trail

Menandai revoked_at di sauth_jti_tokens dan memfire JtiTokenRevoked event. Tidak memblokir secara otomatis di resource server — setelah revoke, jalankan di setiap resource server yang menerima key tersebut:

bash
# Jalankan di setiap resource server (sauth-client command)
php artisan bpm:sauth:jti block {jti} --reason="Compromised"

API Reference

MicroserviceTokenClaimsProviderInterface

php
interface MicroserviceTokenClaimsProviderInterface
{
    public function getClaimsForUser(?Authenticatable $user): array;
}

Contract yang diimplementasikan oleh setiap gateway. Di v0.2.0+, dipanggil library hanya untuk user token (1p dan 3p). 1p M2M token tidak memanggil interface ini — library set sid = client_id langsung dan tidak menyertakan fet atau snm. 3p M2M token dan API key JWT juga 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 — hanya dibaca untuk user token; untuk M2M (jika $user === null) nilai ini diabaikan
snmstring|nullDisplay name user
fetstring|arrayPermissions user — "wm" bypass semua checks; array = daftar atomic permissions

Catatan (v0.2.0+): Untuk 1p M2M (client_credentials), getClaimsForUser(null) tidak dipanggil sama sekali. Return value untuk $user === null hanya diperlukan agar interface tetap terpenuhi — tulis implementasi paling minimal yang valid, misalnya return ['sid' => '', 'snm' => null].


FetchesClientToken

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

TIP

Updated v0.3.4: isOidc parameter ditambahkan ke create() dan update()

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 = '',
    ?string $serviceCode = null,
    bool $isOidc = false,
): 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
$serviceCodestring|nullnullService code untuk 1p resource server ('srf', 'pnr', dll.); null atau '' = tidak diset
$isOidcboolfalseTandai sebagai OIDC client (is_oidc=true); hanya bermakna untuk 3p authorization_code client

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,
    ?string $serviceCode = null,
    ?bool $isOidc = null,
): Client

Update nama, redirect URI, flag first-party, atau service code. 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
$serviceCodestring|nullnullnull = tidak diubah; '' = set null di DB (hapus); non-empty = set nilai baru
$isOidcbool|nullnullnull = tidak diubah; true/false = set nilai baru

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

TIP

Updated v0.3.1: validasi target terhadap service_code sebelum issuance

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 service target atau client ID — harus terdaftar sebagai service_code di oauth_clients

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
  • \InvalidArgumentException — jika $targetService tidak ditemukan di oauth_clients sebagai service_code atau id dari 1p client yang aktif

Validasi target (v0.3.1+): sebelum membangun JWT, query ke oauth_clients untuk mencari 1p client dengan service_code = $targetService atau id = $targetService. Jika tidak ditemukan → InvalidArgumentException. Gateway controller harus catch dan return 422. aud di JWT selalu service_code dari record yang ditemukan — tidak pernah client ID raw.

Claims yang di-embed:

ClaimNilai
issconfig('sauth-server.issuer_code') — kode gateway (selalu 1p)
audservice_code dari client yang ditemukan di DB
iat / nbf / expTimestamps standar JWT
fp'1p' — ticketbooth selalu first-party
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.


KeyLoader

TIP

NEW v0.3.1

Namespace: Bpmlib\SauthServer\Support\KeyLoader

Helper untuk memuat key pair lcobucci/jwt dengan format-aware factory dan deteksi mismatch key/algo sebelum operasi kriptografi apapun.

php
final class KeyLoader
{
    public static function privateKey(string $path, string $algo): InMemory;
    public static function publicKey(string $path, string $algo): InMemory;
}

privateKey() / publicKey()

php
public static function privateKey(string $path, string $algo): InMemory
public static function publicKey(string $path, string $algo): InMemory

Baca key dari file dan kembalikan instance lcobucci/jwt InMemory yang sesuai.

Parameters

NameTypeDescription
$pathstringPath relatif ke storage_path() — contoh: 'oauth-private.key' atau 'keys/gwa-private.key'
$algostringAlgoritma — 'RS256', 'ES256', 'EdDSA', dll.

Returns: Lcobucci\JWT\Signer\Key\InMemory

  • EdDSA ($algo = 'EDDSA') → InMemory::base64Encoded($contents)
  • RS*/ES* → InMemory::file(storage_path($path))

Throws: RuntimeException untuk dua kondisi:

  • File tidak ditemukan
  • Mismatch format: PEM file + SAUTH_ALGO=EdDSA, atau base64 file + SAUTH_ALGO=RS256/ES256/dll.

Pesan error menyertakan command bpm:sauth:keygen yang harus dijalankan untuk memperbaiki.


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

Service untuk menerbitkan API key JWT — long-lived JWT bertanda tangan untuk partner machine integration. Logic terpusat di sini, digunakan oleh bpm:sauth:apikey issue command dan admin controller GWA. Auto-bound sebagai singleton oleh service provider. Inject via constructor DI.

issue()

php
public function issue(
    string $app,
    string $aud,
    string $issuer,
    array $scopes,
): string

Terbitkan API key JWT baru dan simpan audit row ke sauth_jti_tokens.

Parameters

NameTypeDescription
$appstringNama integrasi — menjadi sid di JWT (contoh: 'acme-erp')
$audstringKode target service (contoh: 'srf', 'pnr')
$issuerstringAudit trail — user SID atau 'cli'
$scopesarrayArray scope string (contoh: ['srf.read', 'srf.write'])

Returns: string — plaintext JWT, hanya tersedia saat ini. Disimpan encrypted di database; dapat diambil kembali hanya melalui webmaster decrypt endpoint di GWA.

Claims yang di-embed:

ClaimNilai
issconfig('app.url') — full URL gateway; API key JWT selalu 3p (RFC 9068)
aud$aud
fp'3p' — API key JWT selalu third-party
sid$app
jtiUUID baru — digunakan sebagai blacklist key di resource server
scopeimplode(' ', $scopes) — space-separated
iatTimestamp saat issuance
expTidak ada — API key JWT tidak punya expiry

Algoritma signing (v0.3.3+): Fallback chain SAUTH_APIKEY_SIGN_ALGOSAUTH_SIGN_ALGOSAUTH_ALGORS256. Gunakan SAUTH_APIKEY_SIGN_ALGO dan SAUTH_APIKEY_PRIVATE_KEY_FILE untuk key pair yang independen dari token Passport reguler. Lihat apikey_sign_algorithm dan apikey_private_key_file.

Throws: RuntimeException jika app.url belum dikonfigurasi, atau jika format key file tidak cocok dengan algoritma yang digunakan.


SauthJtiToken

Namespace: Bpmlib\SauthServer\Models\SauthJtiToken

Eloquent model untuk tabel sauth_jti_tokens — audit record setiap API key JWT yang diterbitkan. Primary key UUID (id = jti di JWT).

Fillable: id, app, aud, issuer, encrypted_scopes, encrypted_token, revoke_reason, revoked_at

Cast: revoked_atdatetime

revoke()

php
public function revoke(?string $reason = null): void

Tandai API key sebagai dicabut dan fire JtiTokenRevoked event.

ParameterTypeDescription
$reasonstring|nullAlasan pencabutan untuk audit trail

Mengupdate revoked_at = now() dan revoke_reason. Setelah memanggil ini, daftarkan listener untuk JtiTokenRevoked di GWA untuk propagasi otomatis ke resource server, atau jalankan bpm:sauth:jti block {jti} secara manual di setiap resource server.


OidcClaimsProviderInterface

TIP

NEW v0.3.4

Namespace: Bpmlib\SauthServer\Contracts\OidcClaimsProviderInterface

Contract opsional yang diimplementasikan consuming app untuk menyediakan extra OIDC claims di /userinfo dan id_token. Jika tidak di-bind, /userinfo tetap berfungsi dan mengembalikan sub + name saja.

Bind di AppServiceProvider::register():

php
$this->app->bind(OidcClaimsProviderInterface::class, GwaOidcClaimsProvider::class);

getOidcClaims()

php
public function getOidcClaims(Authenticatable $user): array

Kembalikan extra standard OIDC claims. Jangan sertakan sub atau name — keduanya sudah di-set dari token.

Returns: array<string, mixed> — contoh: ['email' => '...', 'email_verified' => true, 'picture' => '...']


IdTokenIssuer

TIP

NEW v0.3.4 | Updated v0.3.5: nonce otomatis diteruskan dari authorization request

Namespace: Bpmlib\SauthServer\Services\IdTokenIssuer

Service yang membangun dan menandatangani OIDC id_token. Dipanggil otomatis oleh OidcBearerTokenResponse — tidak perlu diinject secara manual kecuali ada kebutuhan khusus. Auto-bound sebagai singleton.

issue()

php
public function issue(AccessToken $token): string

Bangun dan tandatangani id_token dari access token entity yang diberikan. Menggunakan key dan algoritma yang sama dengan access token (SAUTH_SIGN_ALGOSAUTH_ALGO).

Claims yang di-embed:

ClaimNilai
issAPP_URL — selalu URL untuk OIDC
subsid dari access token (user ID)
audclient_id OAuth client yang meminta
iat / expTimestamps; exp = sekarang + token_ttl.access
namesnm jika ada; dihilangkan untuk M2M
nonceDari parameter nonce authorization request — otomatis diteruskan ke id_token (v0.3.5); consuming app harus simpan ke session['oidc_nonce'] untuk manual-approve path (lihat docs/oidc-gwa-req.md)

Throws: RuntimeException jika APP_URL belum dikonfigurasi atau sid kosong.


JtiTokenRevoked

Namespace: Bpmlib\SauthServer\Events\JtiTokenRevoked

Event yang difire oleh SauthJtiToken::revoke(). Memungkinkan GWA mendaftarkan listener untuk propagasi revocation otomatis ke resource server.

php
class JtiTokenRevoked
{
    public function __construct(public readonly SauthJtiToken $token) {}
}

GWA dapat mendaftarkan listener di EventServiceProvider yang POST ke internal JTI block endpoint setiap resource server menggunakan M2M token. Library menyediakan event-nya; topologi (service mana yang perlu dinotifikasi) ada di codebase GWA, bukan di library ini.


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 ['sid' => '', 'snm' => null]; // tidak dipanggil untuk 1p M2M di v0.2.0+
        }

        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 ['sid' => '', 'snm' => null]; // tidak dipanggil untuk 1p M2M di v0.2.0+
        }

        return [
            'sid' => (string) $user->id,
            'snm' => $user->name,
            'fet' => [$user->user_type], // 'psn', 'cpy', atau 'mit'
        ];
    }
}

3. Registrasi Client via ClientRegistrar

TIP

Updated v0.3.1: parameter serviceCode ditambahkan

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'),
            serviceCode: $request->string('service_code') ?: null,
        );

        // plainSecret hanya tersedia di instance ini — simpan atau tampilkan sekarang
        return response()->json([
            'client_id'     => $client->getKey(),
            'client_secret' => $client->plainSecret,
            'service_code'  => $client->service_code,
        ]);
    }
}

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

TIP

Updated v0.3.1: catch InvalidArgumentException untuk target tidak terdaftar

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

        try {
            $result = $this->issuer->issue(
                $request->user(),
                $request->input('target'),
            );
        } catch (\InvalidArgumentException $e) {
            return response()->json(['message' => $e->getMessage()], 422);
        }

        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

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

10. API Key JWT — Issue, List, Revoke

Workflow lengkap penerbitan, audit, dan pencabutan API key JWT untuk partner machine integration.

Menerbitkan via Artisan:

bash
# Terbitkan API key untuk acme-erp mengakses SRF
php artisan bpm:sauth:apikey issue acme-erp srf --scopes="srf.read srf.write" --issuer=admin

# Output:
# API key JWT issued successfully.
# JTI: 550e8400-e29b-41d4-a716-446655440000
#
# Token (shown once — store immediately):
# eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...

Menerbitkan via controller (programmatic):

php
<?php

namespace App\Http\Controllers\Admin;

use Bpmlib\SauthServer\Services\ApiKeyIssuer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

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'),
        );

        // Token hanya tersedia sekali — kembalikan ke caller untuk disimpan
        return response()->json(['token' => $token]);
    }
}

List semua API key:

bash
php artisan bpm:sauth:apikey list

# +--------------------------------------+-----------+-----+--------+---------------------+------------+
# | jti                                  | app       | aud | issuer | created_at          | revoked_at |
# +--------------------------------------+-----------+-----+--------+---------------------+------------+
# | 550e8400-e29b-41d4-a716-446655440000 | acme-erp  | srf | admin  | 2026-01-15 10:00:00 |            |
# +--------------------------------------+-----------+-----+--------+---------------------+------------+

Mencabut API key:

bash
# 1. Tandai sebagai dicabut di GWA
php artisan bpm:sauth:apikey revoke 550e8400-e29b-41d4-a716-446655440000 --reason="Compromised"

# 2. Blokir di setiap resource server yang menerima key ini
php artisan bpm:sauth:jti block 550e8400-e29b-41d4-a716-446655440000 --reason="Compromised"

JWT yang diterbitkan memiliki struktur:

json
{
  "iss": "https://admin.example.com",
  "aud": "srf",
  "fp": "3p",
  "sid": "acme-erp",
  "jti": "550e8400-e29b-41d4-a716-446655440000",
  "scope": "srf.read srf.write",
  "iat": 1736935200
}

Tidak ada exp — API key JWT bersifat permanen sampai dicabut. Resource server menggunakan jti blacklist (via sauth-client's JtiBlacklist) untuk revocation.


11. Generate Key Pair dengan bpm:sauth:keygen

TIP

NEW v0.3.1 | Updated v0.3.2: argumen algo opsional

Generate key pair untuk berbagai algoritma dan konfigurasi path.

bash
# Tanpa argumen — algo dari SAUTH_SIGN_ALGO → SAUTH_ALGO → RS256 (v0.3.2)
php artisan bpm:sauth:keygen

# Default eksplisit — RSA 4096-bit, storage/oauth-private.key + oauth-public.key
php artisan bpm:sauth:keygen RS256

# EC key ke subdirektori
php artisan bpm:sauth:keygen ES256 --path=keys
# Output: storage/keys/oauth-private.key + storage/keys/oauth-public.key
# Warning: tambahkan /storage/keys/*.key ke .gitignore

# EdDSA dengan prefix custom
php artisan bpm:sauth:keygen EdDSA --file-prefix=gwa
# Output: storage/gwa-private.key (base64) + storage/gwa-public.key (base64)

# Overwrite dengan konfirmasi
php artisan bpm:sauth:keygen RS256 --force

Setelah generate, update .env:

dotenv
# Untuk RS256/ES256 default
SAUTH_PRIVATE_KEY_FILE=oauth-private.key
SAUTH_ALGO=RS256

# Untuk ES256 --path=keys
SAUTH_PRIVATE_KEY_FILE=keys/oauth-private.key
SAUTH_ALGO=ES256

# Untuk EdDSA --file-prefix=gwa
SAUTH_PRIVATE_KEY_FILE=gwa-private.key
SAUTH_ALGO=EdDSA

# Override signing gateway tanpa mengubah SAUTH_ALGO (v0.3.2)
SAUTH_SIGN_ALGO=ES256
SAUTH_ALGO=RS256

12. Registrasi 1p Resource Server Client dengan service_code

TIP

NEW v0.3.1

Setiap 1p resource server yang menjadi target ticketbooth harus didaftarkan dengan service_code. Tanpa ini, ServiceTokenIssuer akan menolak semua request ke service tersebut.

Via Artisan:

bash
# Buat client M2M first-party untuk SRF — prompt untuk service code
php artisan bpm:sauth:client --first
# Client name: SRF Service
# Service code: srf
#
# Output:
#   Client ID    : 5
#   Client Secret: xxxx
#   Service Code : srf

Via Tinker (populate existing client):

php
// Populate service_code pada client yang sudah ada
\Laravel\Passport\Client::find(5)->update(['service_code' => 'srf']);

Via ClientRegistrar (programmatic):

php
// Buat client baru dengan service_code
$client = $registrar->create(
    name: 'SRF Service',
    isFirstParty: true,
    serviceCode: 'srf',
);

// Update client yang sudah ada
$registrar->update($client, 'SRF Service', serviceCode: 'srf');

// Hapus service_code (client tidak lagi menjadi target ticketbooth)
$registrar->update($client, 'SRF Service', serviceCode: '');

13. Setup OIDC Client

TIP

NEW v0.3.4

Setup lengkap OIDC: aktifkan fitur, daftarkan client, dan implementasikan OidcClaimsProviderInterface untuk extra claims.

1. Aktifkan OIDC dan jalankan migrasi:

bash
# .env
SAUTH_OIDC_ENABLED=true

php artisan bpm:sauth:migration

2. Buat OIDC client:

bash
php artisan bpm:sauth:client --user --oidc
# Client name: Acme Dashboard
# Redirect URI: https://acme.example.com/callback
#
#   Client ID     : 7
#   Client Secret : xxxx
#   OIDC          : yes

3. Implementasikan OidcClaimsProviderInterface (opsional):

php
<?php

namespace App\Auth;

use Bpmlib\SauthServer\Contracts\OidcClaimsProviderInterface;
use Illuminate\Contracts\Auth\Authenticatable;

class GwaOidcClaimsProvider implements OidcClaimsProviderInterface
{
    public function getOidcClaims(Authenticatable $user): array
    {
        return [
            'email'          => $user->email,
            'email_verified' => $user->email_verified_at !== null,
        ];
    }
}
php
// AppServiceProvider::register()
$this->app->bind(OidcClaimsProviderInterface::class, GwaOidcClaimsProvider::class);

4. Flow authorization_code dengan openid scope:

GET /oauth/authorize?client_id=7&scope=openid+profile&response_type=code&...
→ user login + consent
→ POST /oauth/token { code, ... }
→ { access_token: "...", id_token: "eyJ...", expires_in: 900 }

id_token membawa: sub (user ID), name, email (dari provider), iss = APP_URL, aud = client_id.


Laravel Integration

Service Provider

Auto-registered via package discovery. Mendaftarkan:

  • Custom AccessTokenRepository — meng-inject iss, fp, sid, snm, fet, scope, act ke setiap token sesuai tipe
  • ClientRegistrar, ServiceTokenIssuer, ApiKeyIssuer, IdTokenIssuer sebagai singleton
  • Artisan commands: bpm:sauth:init, bpm:sauth:migration, bpm:sauth:keygen, bpm:sauth:client, bpm:sauth:apikey issue/list/revoke
  • TokenExchangeGrant jika token_exchange.enabled = true
  • Saat oidc.enabled = true: endpoint /.well-known/openid-configuration, /.well-known/jwks.json, /userinfo; OidcBearerTokenResponse untuk id_token issuance; OidcAuthCodeRepository untuk nonce passthrough (v0.3.5); boot-time schema guard untuk kolom is_oidc

Published Assets

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

Required Migrations

Migration di-ship bersama library sebagai stub dan di-publish otomatis oleh bpm:sauth:init atau bpm:sauth:migration ke database/migrations/. Tidak perlu menulis migration secara manual.

  1. is_first_party + service_code columns — fresh install: satu stub menambahkan keduanya ke oauth_clients. Upgrade dari ≤v0.3.0: dua stub terpisah (is_first_party sudah ada → hanya service_code yang ditambahkan).
  2. sauth_jti_tokens table — audit table untuk API key JWT; kolom: id (UUID = jti), app, aud, issuer, encrypted_scopes, encrypted_token, revoke_reason, revoked_at
  3. is_oidc column — kolom boolean di oauth_clients untuk menandai OIDC client; ditambahkan oleh bpm:sauth:migration secara idempotent

Deployment Order v0.3.1

IMPORTANT

Ikuti urutan ini — deploy kode sebelum populate service_code menyebabkan semua ticketbooth request return 422.

bash
# Langkah 1 — tambahkan kolom ke database
php artisan bpm:sauth:migration

# Langkah 2 — populate service_code pada semua 1p resource server client
# (via Tinker, migration data, atau bpm:sauth:client --first)
php artisan tinker
>>> \Laravel\Passport\Client::where('is_first_party', true)->whereNull('service_code')->get()
# Identify masing-masing client, lalu:
>>> \Laravel\Passport\Client::find($id)->update(['service_code' => 'srf'])

# Langkah 3 — deploy kode v0.3.1
# ServiceTokenIssuer validasi aktif setelah ini

Tidak ada perubahan wajib untuk:

  • Resource server — tidak ada perubahan di sauth-client
  • Gateway ClaimsProvider — interface tidak berubah
  • Worker yang menggunakan FetchesClientToken — tidak ada perubahan
  • Token yang sudah diterbitkan — tetap valid