Skip to content

Sauth Server

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

Versi: 0.4.0 Changelog

PHPLaravel


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:

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

php
// bootstrap/providers.php
Bpmlib\SauthServer\SauthServerServiceProvider::class,
Bpmlib\SauthClient\SauthClientServiceProvider::class,

Publish Commands

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

bpm: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
<?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(),
        ];
    }
}
php
// AppServiceProvider::register()
$this->app->bind(MicroserviceTokenClaimsProviderInterface::class, GwaClaimsProvider::class);

Key Points:

  • getClaimsForUser(null, ...) tidak dipanggil untuk 1p M2M token — library set sid = client_id langsung; return value untuk null diabaikan
  • $clientId (v0.4.0+) — client OAuth yang meminta token; gunakan untuk scoping custom claim ke client tertentu — lihat Custom Claims
  • 'wm' pada fet memberikan bypass penuh ke semua route sauth.gate
  • Bind di register() bukan boot() — service provider ini resolve pada saat container bootstrap

Configuration

Configuration File

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

Available Options

OptionTypeDefaultDescription
issuer_codestring-Kode gateway (gwa/gwc) — di-embed sebagai iss di 1p token
default_audiencestring'internal-services'Fallback aud jika tidak ada target service
algorithmstring'RS256'Algoritma signing shared dengan sauth-client Lihat selengkapnya
sign_algorithmstring|nullnullOverride algo signing sauth-server Lihat selengkapnya
apikey_sign_algorithmstring|nullnullOverride algo khusus API key JWT Lihat selengkapnya
apikey_private_key_filestring|nullnullOverride key file khusus API key JWT Lihat selengkapnya
private_key_filestring'oauth-private.key'Path private key relatif ke storage_path()
token_exchange.enabledboolfalseAktifkan token exchange grant (RFC 8693) — GWA only
token_ttl.accessint900TTL access token dalam detik
token_ttl.refreshint604800TTL refresh token dalam detik
client_credentials.token_urlstring-URL /oauth/token GWA — untuk FetchesClientToken
client_credentials.client_idstring-Client ID — untuk FetchesClientToken
client_credentials.client_secretstring-Client secret — untuk FetchesClientToken
bypassboolfalseDev bypass: skip GWA call di local env Lihat selengkapnya
oidc.enabledboolfalseAktifkan 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:

EndpointKeterangan
GET /.well-known/openid-configurationDiscovery document
GET /.well-known/jwks.jsonPublic key dalam format JWK Set
GET /userinfosub + 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_ALGOSAUTH_ALGORS256

Gunakan saat gateway ingin ganti algoritma tanpa mengubah SAUTH_ALGO yang dibaca resource server:

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

apikey_sign_algorithm

TIP

NEW v0.3.3

Override algoritma signing khusus API key JWT — tidak memengaruhi token Passport reguler.

Urutan resolusi: SAUTH_APIKEY_SIGN_ALGOSAUTH_SIGN_ALGOSAUTH_ALGORS256


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_FILESAUTH_PRIVATE_KEY_FILEoauth-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

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

Tabel

Publish Migration

bash
php artisan bpm:sauth:init      # atau
php artisan bpm:sauth:migration

Migration di-publish otomatis ke database/migrations/ — tidak perlu menulis manual.


Tables

SauthJtiToken

Table: sauth_jti_tokens Namespace: Bpmlib\SauthServer\Models\SauthJtiToken

Audit table untuk setiap API key JWT yang diterbitkan. Primary key UUID (id = jti di JWT).

ColumnTypeNullableDefaultDescription
iduuidNo-Primary key — sama dengan jti di JWT
appvarchar(255)No-Nama integrasi (sid di JWT)
audvarchar(255)No-Target service code
issuervarchar(255)No-Siapa yang menerbitkan — user SID atau 'cli'
encrypted_scopestextYesnullScopes dienkripsi via encrypt()
encrypted_tokentextNo-Plaintext JWT dienkripsi via encrypt()
revoke_reasonvarchar(255)YesnullAlasan pencabutan
revoked_attimestampYesnullTimestamp pencabutan; null = aktif
created_attimestampYesnull-
updated_attimestampYesnull-

Core Concepts

Token Claim Structure

Nama claim adalah shared contract antara sauth-server, sauth-client, dan sauth-frontend — jangan ubah tanpa memperbarui ketiganya.

ClaimTypeDescription
issstring1p token: kode gateway (gwa/gwc); 3p + API key JWT: APP_URL
audstringTarget service code
iatintIssued-at timestamp
expintExpiry — tidak ada di API key JWT (revocation via jti blacklist)
fpstringParty marker — '1p' atau '3p'
sidstringUser ID (user token), client_id (M2M), atau app name (API key JWT)
snmstringDisplay name user — tidak ada di M2M dan API key JWT
fetstring|arrayPermissions user — "wm" bypass semua; array = atomic permissions; tidak pernah ada di M2M
scopestringSpace-separated scopes — 3p user, 3p M2M, API key JWT; tidak ada di 1p
jtistringUUID — hanya ada di API key JWT; digunakan sebagai blacklist key
actobjectRFC 8693 — {sub: calling_client_id}; hanya ada di delegated token
(host-defined)mixedNEW 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 typefpsidsnmfetscopejtiexp
1p user token1puser ID
3p user token3puser ID
1p M2M token1pclient_id
3p M2M token3pclient_id
API key JWT3papp 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 typeMiddleware
1p usersauth.gate
1p M2Msauth.gate (no params) atau sauth.m2m
3p usersauth.gate + sauth.scope (keduanya wajib)
3p M2Msauth.scope
API key JWTsauth.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: {...}).

php
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 — lihat Bpmlib\SauthServer\Support\ReservedClaims::LIST) melempar Bpmlib\SauthServer\Exceptions\ReservedClaimException saat issuance — bug developer/config, tidak di-catch internal.
  • Berlaku identik di kedua flow issuance — OAuth/Passport flow (AccessTokenRepositoryAccessToken::setExtraClaims()) dan ticketbooth flow (ServiceTokenIssuer, JWT dibangun manual) — keduanya validasi lewat ReservedClaims::assertNoCollision() yang sama, tanpa duplikasi logic.
  • Tidak ikut di token exchange. TokenExchangeGrant hanya copy sid/snm/fet/fp dari subject token — custom claim otomatis ter-strip.
  • sauth-client tidak 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

GrantUse Case
authorization_code + PKCELogin user via GWA atau GWC
client_credentialsM2M — 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.

bash
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
AlgorithmFormat key
RS256/384/512PEM (-----BEGIN RSA PRIVATE KEY-----)
ES256/384/512PEM (-----BEGIN EC PRIVATE KEY-----)
EdDSABase64 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

Setup sauth-server dalam satu langkah: Passport migrations, sauth migrations, migrate, dan keygen. Idempotent — aman dijalankan berulang.

Signature:

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

Flags:

FlagDescription
--forceRegenerate key pair meskipun sudah ada (dengan konfirmasi prompt)

Options:

OptionDefaultDescription
--algo=<string>SAUTH_SIGN_ALGOSAUTH_ALGORS256Algoritma key generation
--path=<string>storage rootDirektori output key relatif ke storage_path()
--file-prefix=<string>oauthPrefix nama file key

Contoh:

bash
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=gwa

bpm:sauth:migration

Publish dan jalankan sauth migrations secara idempotent — tanpa keygen. Gunakan saat upgrade rolling.

Signature:

bash
php artisan bpm:sauth:migration

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

  1. is_first_party + service_code columns — fresh install: satu stub; upgrade dari ≤v0.3.0: dua stub terpisah
  2. is_oidc column di oauth_clients
  3. sauth_jti_tokens table
  4. Jalankan migrate --force jika ada yang dipublish

bpm:sauth:keygen

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

Signature:

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

Arguments:

ArgumentRequiredDescription
algoNoRS256, RS384, RS512, ES256, ES384, ES512, EdDSA. Default: SAUTH_SIGN_ALGOSAUTH_ALGORS256

Flags:

FlagDescription
--forceOverwrite file yang sudah ada (dengan konfirmasi prompt)

Options:

OptionDefaultDescription
--path=<string>storage rootDirektori output relatif ke storage_path()
--file-prefix=<string>oauthPrefix — menghasilkan {prefix}-private.key dan {prefix}-public.key

Contoh:

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

bpm:sauth:client

Buat OAuth client baru. Secret hanya tersedia sekali di output — simpan segera.

Signature:

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

Flags:

FlagDescription
--firstFirst-party (is_first_party=true); prompt service code (wajib diisi)
--userGrant authorization_code + PKCE; default client_credentials tanpa flag ini
--oidcOIDC client (is_oidc=true); wajib bersama --user; error jika --first

Kombinasi:

KombinasiPerilaku
(tanpa flag)3p M2M — client_credentials
--first1p M2M — prompt service code
--user3p user — authorization_code + PKCE
--first --user1p user (GWA/GWC frontend) — prompt service code
--user --oidc3p OIDC user — id_token diaktifkan

bpm:sauth:apikey:issue

Terbitkan API key JWT baru untuk partner machine integration.

Signature:

bash
php artisan bpm:sauth:apikey:issue {app} {aud} [--scopes=] [--issuer=]

Arguments:

ArgumentRequiredDescription
appYesNama integrasi — menjadi sid di JWT (contoh: acme-erp)
audYesTarget service code (contoh: srf)

Options:

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

bash
php artisan bpm:sauth:apikey:list

Kolom output: jti, app, aud, issuer, created_at, revoked_at.


bpm:sauth:apikey:revoke

Cabut API key JWT.

Signature:

bash
php artisan bpm:sauth:apikey:revoke {jti} [--reason=]

Arguments:

ArgumentRequiredDescription
jtiYesUUID jti dari API key yang akan dicabut

Options:

OptionDefaultDescription
--reason=<string>nullAlasan 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:

bash
php artisan bpm:sauth:jti block {jti} --reason="..."

API Reference

Daftar Isi:

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()
php
public function getClaimsForUser(?Authenticatable $user, string $clientId): array

Parameters

NameTypeDefaultDescription
$userAuthenticatable|null-User yang login; null saat M2M flow
$clientIdstring-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>}

KeyTypeDescription
sidstringUser ID
snmstring|nullDisplay name
fetstring|arrayPermissions — "wm" = bypass penuh; array = atomic permissions
(key lain)mixedNEW 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()
php
public function getOidcClaims(Authenticatable $user): array

Returns: array<string, mixed> — extra OIDC claims. Jangan sertakan sub atau name.

Contoh:

php
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()
php
protected function clientToken(string $audience = ''): string

Kembalikan M2M token valid untuk audience yang dituju.

Parameters

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

Hapus 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()
php
public function create(
    string $name,
    bool $isFirstParty = false,
    bool $isUserClient = false,
    string $redirectUri = '',
    ?string $serviceCode = null,
    bool $isOidc = false,
): Client

Parameters

NameTypeDefaultDescription
$namestring-Nama client
$isFirstPartyboolfalseis_first_party = true
$isUserClientboolfalsetrue = authorization_code; false = client_credentials
$redirectUristring''Wajib jika $isUserClient = true
$serviceCodestring|nullnullService code 1p resource server; null/'' = tidak diset
$isOidcboolfalseis_oidc = true — hanya bermakna untuk 3p authorization_code client

Returns: ClientplainSecret tersedia di instance ini. Tidak bisa diambil lagi.


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

Parameters

NameTypeDefaultDescription
$clientClient-Client yang diupdate
$namestring-Nama baru
$redirectUristring|nullnullnull = tidak diubah
$isFirstPartybool|nullnullnull = tidak diubah
$serviceCodestring|nullnullnull = tidak diubah; '' = hapus; non-empty = set
$isOidcbool|nullnullnull = tidak diubah

Returns: Client — fresh instance dari database


delete()
php
public function delete(Client $client): void

Revoke semua token aktif dan hapus record oauth_clients.


rotateSecret()
php
public function rotateSecret(Client $client): Client

Generate client secret baru. Caller bertanggung jawab untuk audit logging.

Returns: ClientplainSecret tersedia. Tidak bisa diambil lagi.


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

Parameters

NameTypeDefaultDescription
$firstPartybool|nullnulltrue = 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()
php
public function issue(Authenticatable $user, string $targetService): array

Parameters

NameTypeDefaultDescription
$userAuthenticatable-User dari $request->user()
$targetServicestring-Service code atau client ID yang terdaftar di oauth_clients

Returns

KeyTypeDescription
accessstringSigned JWT siap pakai
expires_inintTTL dalam detik (token_ttl.access)

Throws:

  • RuntimeExceptionissuer_code belum dikonfigurasi
  • \InvalidArgumentException$targetService tidak ditemukan sebagai service_code di 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()
php
public function issue(string $app, string $aud, string $issuer, array $scopes): string

Parameters

NameTypeDefaultDescription
$appstring-Nama integrasi — menjadi sid di JWT
$audstring-Target service code
$issuerstring-Audit trail — user SID atau 'cli'
$scopesarray-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()
php
public function issue(AccessToken $token): string

Returns: 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()
php
public function revoke(?string $reason = null): void

Update revoked_at = now() dan revoke_reason, lalu fire JtiTokenRevoked event.

Parameters

NameTypeDefaultDescription
$reasonstring|nullnullAlasan 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.

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

Parameters (kedua method):

NameTypeDescription
$pathstringPath relatif ke storage_path()
$algostring'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.

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

Examples

Contains:

1. ClaimsProvider — GWA dan GWC

GWA menggunakan atomic permissions dari getFeatures(), GWC menggunakan role code tunggal.

php
// 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
<?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
<?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);
        }
    }
}
php
// 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.

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,
    '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:

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

Via controller (programmatic):

php
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

php
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

bash
# 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
php
// 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

bash
# 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
dotenv
# 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.key

Laravel Integration

Service Provider

Auto-registered via package discovery. Mendaftarkan:

  • Custom AccessTokenRepository — inject iss, fp, sid, snm, fet, scope, act sesuai token type
  • ClientRegistrar, ServiceTokenIssuer, ApiKeyIssuer, IdTokenIssuer sebagai 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
  • TokenExchangeGrant jika token_exchange.enabled = true
  • Saat oidc.enabled = true: endpoint discovery, JWKS, userinfo; OidcBearerTokenResponse; OidcAuthCodeRepository (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

Di-publish otomatis oleh bpm:sauth:init atau bpm:sauth:migration — tidak perlu menulis manual:

  1. is_first_party + service_code — fresh install: satu stub; upgrade dari ≤v0.3.0: dua stub terpisah
  2. is_oidc — kolom boolean di oauth_clients untuk OIDC client
  3. sauth_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.

bash
# 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 ini