ums-laravel maintained by kodepik
UMS Laravel SDK
Laravel package for SSO authentication & authorization via UMS (User Management System).
Package ini menyediakan:
- SSO Login/Logout (OAuth2 redirect flow via UMS → Keycloak)
- JWT token validation via JWKS public key (RS256)
- Middleware untuk protect routes berdasarkan permission, role, dan module
- Helper functions untuk cek akses di mana saja (controller, service, blade)
- API authentication dengan Bearer token
Daftar Isi
- Requirements
- Installation
- Configuration
- Prasyarat di UMS Admin
- Quick Start — SSO Login
- Quick Start — API Bearer Token
- Middleware Reference
- Helper Functions
- UmsClaims Object
- Facade
- Implementasi Lengkap (Step by Step)
- Blade Template
- Advanced: Custom Routes
- Advanced: Token Refresh
- Troubleshooting
- Security Notes
Requirements
- PHP 8.1+
- Laravel 10.x atau 11.x
- UMS server yang sudah running
Installation
1. Install package
composer require kodepik/ums-laravel
Package menggunakan Laravel auto-discovery — ServiceProvider dan Facade otomatis terdaftar.
2. Publish config
php artisan vendor:publish --tag=ums-config
File config/ums.php akan dibuat.
Configuration
Tambahkan ke file .env:
# URL server UMS
UMS_BASE_URL=https://ums.yourserver.com
# App ID yang didaftarkan di UMS Admin
UMS_APP_ID=your-app-id
# Callback URL (harus didaftarkan juga di UMS Admin)
UMS_CALLBACK_URL=https://yourapp.com/ums/callback
# SSL verification (set false jika development dengan self-signed cert)
UMS_VERIFY_SSL=true
# Optional: disable auto-registered routes
UMS_ROUTES_ENABLED=true
# Optional: ganti prefix routes (default: 'ums')
UMS_ROUTES_PREFIX=ums
# Optional: JWKS cache duration (default: 3600 seconds / 1 jam)
UMS_JWKS_CACHE_TTL=3600
Prasyarat di UMS Admin
Sebelum integrasi, pastikan di UMS Admin:
- Application sudah dibuat — catat
app_id(contoh:APP-xxxxx) - Callback URL didaftarkan —
https://yourapp.com/ums/callback - Logout redirect URL didaftarkan —
https://yourapp.com/login - User di-assign ke application — user yang bisa login
- Module, Role, Permission — sudah di-assign ke user sesuai kebutuhan
Quick Start — SSO Login
Setelah install dan config, SSO langsung bisa dipakai tanpa coding tambahan.
Auto-registered routes:
| Route | Method | Name | Fungsi |
|---|---|---|---|
/ums/login |
GET | ums.login |
Redirect ke UMS → Keycloak |
/ums/callback |
GET | ums.callback |
Handle callback setelah login |
/ums/logout |
GET | ums.logout |
Logout + redirect ke UMS logout |
/ums/refresh |
POST | ums.refresh |
Refresh token yang expired |
Di halaman login kamu:
<a href="{{ route('ums.login') }}">Login with SSO</a>
Setelah login berhasil:
User di-redirect ke /dashboard (configurable di UmsAuthController). Data tersimpan di session:
session('ums_token'); // JWT access token
session('ums_refresh_token'); // Refresh token
session('ums_user'); // Decoded claims (array)
Logout:
<a href="{{ route('ums.logout') }}">Logout</a>
Quick Start — API Bearer Token
Untuk API endpoint yang di-consume frontend/mobile, gunakan Bearer token:
// routes/api.php
Route::middleware(['ums.auth'])->group(function () {
Route::get('/profile', function () {
return response()->json(ums_user()->toArray());
});
});
Client mengirim request dengan header:
Authorization: Bearer <jwt_token_dari_ums>
Middleware Reference
ums.auth — Validasi Token
Memastikan request memiliki Bearer token yang valid.
Route::middleware(['ums.auth'])->group(function () {
// Semua route di sini butuh token valid
});
Response jika gagal:
{"success": false, "message": "Missing authorization token"} // 401
{"success": false, "message": "Invalid or expired token"} // 401
ums.permission:module,permission — Cek Permission
Memastikan user punya permission tertentu di module tertentu.
// User harus punya permission 'read' di module 'inventory'
Route::middleware(['ums.auth', 'ums.permission:inventory,read'])->group(function () {
Route::get('/items', [ItemController::class, 'index']);
});
// User harus punya permission 'write' di module 'inventory'
Route::middleware(['ums.auth', 'ums.permission:inventory,write'])->group(function () {
Route::post('/items', [ItemController::class, 'store']);
});
Response jika gagal:
{"success": false, "message": "Permission denied: inventory.write"} // 403
ums.role:module,role — Cek Role
Memastikan user punya role tertentu di module tertentu.
// User harus punya role 'admin' di module 'dashboard'
Route::middleware(['ums.auth', 'ums.role:dashboard,admin'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});
Response jika gagal:
{"success": false, "message": "Role denied: dashboard.admin"} // 403
ums.module:module — Cek Akses Module
Memastikan user punya akses ke module tertentu (tanpa cek permission/role spesifik).
// User harus punya akses ke module 'reports'
Route::middleware(['ums.auth', 'ums.module:reports'])->group(function () {
Route::get('/reports', [ReportController::class, 'index']);
});
Response jika gagal:
{"success": false, "message": "Module access denied: reports"} // 403
Kombinasi Middleware
Middleware bisa di-chain:
// Harus login + punya module inventory + permission delete
Route::middleware(['ums.auth', 'ums.module:inventory', 'ums.permission:inventory,delete'])
->delete('/items/{id}', [ItemController::class, 'destroy']);
Helper Functions
Tersedia global helper yang bisa dipanggil dari mana saja:
// Get current user (dari request attribute atau session)
$user = ums_user(); // UmsClaims|null
// Cek permission
ums_can('inventory', 'read'); // bool
ums_can('inventory', 'delete'); // bool
// Cek role
ums_has_role('dashboard', 'admin'); // bool
ums_has_role('dashboard', 'viewer'); // bool
// Cek module access
ums_has_module('inventory'); // bool
ums_has_module('reports'); // bool
// Get raw JWT token dari session
ums_token(); // string|null
UmsClaims Object
Object yang dikembalikan oleh ums_user():
$user = ums_user();
// Properties
$user->userId; // "d4c8546e-3166-4afe-a79f-70c073a7c1f7"
$user->email; // "user@example.com"
$user->appId; // "APP-10cd66bc"
$user->modules; // array of module claims
$user->exp; // 1782881536 (token expiry unix timestamp)
$user->iat; // 1782877936 (token issued at)
$user->jti; // "e2a4b879-ffd9-4a1a-9536-f0edcb1a9ebb"
// Methods
$user->hasPermission('inventory', 'read'); // bool
$user->hasRole('inventory', 'admin'); // bool
$user->hasModule('inventory'); // bool
$user->getModuleRoles('inventory'); // ['admin', 'editor']
$user->getModulePermissions('inventory'); // ['read', 'write', 'delete']
$user->getModuleNames(); // ['dashboard', 'inventory']
$user->isExpired(); // bool
$user->toArray(); // array
Struktur modules:
$user->modules = [
[
'module' => 'dashboard',
'parent_module' => null,
'roles' => ['admin'],
'permissions' => ['create', 'delete', 'update', 'view'],
],
[
'module' => 'reports',
'parent_module' => 'dashboard',
'roles' => ['viewer'],
'permissions' => ['read'],
],
];
Facade
use Kodepik\UMS\Facades\UMS;
// Validate token secara manual
$claims = UMS::validateToken($jwtToken);
// Validate via UMS server (server-side, lebih aman)
$claims = UMS::validateTokenRemote($jwtToken);
// Refresh token
$result = UMS::refreshToken($refreshToken);
// $result = ['token' => '...', 'refresh_token' => '...']
// Get URLs
$loginUrl = UMS::getLoginUrl();
$logoutUrl = UMS::getLogoutUrl('https://myapp.com/login');
// Clear JWKS cache (setelah key rotation di UMS)
UMS::invalidateJwksCache();
Implementasi Lengkap (Step by Step)
Step 1: Install & Config
composer require kodepik/ums-laravel
php artisan vendor:publish --tag=ums-config
Tambahkan ke .env:
UMS_BASE_URL=https://ums.yourserver.com
UMS_APP_ID=APP-xxxxx
UMS_CALLBACK_URL=http://localhost:8000/ums/callback
UMS_VERIFY_SSL=false
Step 2: Buat Login Page
{{-- resources/views/login.blade.php --}}
<a href="{{ route('ums.login') }}">Login with UMS SSO</a>
Step 3: Buat Dashboard (Protected Page)
// routes/web.php
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
// app/Http/Controllers/DashboardController.php
<?php
namespace App\Http\Controllers;
class DashboardController extends Controller
{
public function index()
{
$user = ums_user();
if (!$user) {
return redirect()->route('ums.login');
}
return view('dashboard', compact('user'));
}
}
{{-- resources/views/dashboard.blade.php --}}
<h1>Welcome, {{ ums_user()->email }}</h1>
<p>Your modules: {{ implode(', ', ums_user()->getModuleNames()) }}</p>
<a href="{{ route('ums.logout') }}">Logout</a>
Step 4: Protect API Routes
// routes/api.php
use App\Http\Controllers\Api\ItemController;
Route::middleware(['ums.auth'])->group(function () {
Route::get('/me', function () {
return response()->json(['data' => ums_user()->toArray()]);
});
// CRUD dengan permission checks
Route::middleware(['ums.permission:inventory,read'])
->get('/items', [ItemController::class, 'index']);
Route::middleware(['ums.permission:inventory,create'])
->post('/items', [ItemController::class, 'store']);
Route::middleware(['ums.permission:inventory,update'])
->put('/items/{id}', [ItemController::class, 'update']);
Route::middleware(['ums.permission:inventory,delete'])
->delete('/items/{id}', [ItemController::class, 'destroy']);
// Admin-only
Route::middleware(['ums.role:inventory,admin'])
->post('/items/bulk-delete', [ItemController::class, 'bulkDelete']);
});
Step 5: Controller dengan Middleware
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ItemController extends Controller
{
public function __construct()
{
$this->middleware('ums.auth');
$this->middleware('ums.permission:inventory,read')->only(['index', 'show']);
$this->middleware('ums.permission:inventory,create')->only(['store']);
$this->middleware('ums.permission:inventory,update')->only(['update']);
$this->middleware('ums.permission:inventory,delete')->only(['destroy']);
}
public function index()
{
$items = \App\Models\Item::all();
return response()->json(['success' => true, 'data' => $items]);
}
public function store(Request $request)
{
$validated = $request->validate(['name' => 'required|string']);
$item = \App\Models\Item::create([
...$validated,
'created_by' => ums_user()->email,
]);
return response()->json(['success' => true, 'data' => $item], 201);
}
public function show(string $id)
{
$item = \App\Models\Item::findOrFail($id);
return response()->json(['success' => true, 'data' => $item]);
}
public function update(Request $request, string $id)
{
$item = \App\Models\Item::findOrFail($id);
$item->update($request->validated());
return response()->json(['success' => true, 'data' => $item]);
}
public function destroy(string $id)
{
\App\Models\Item::findOrFail($id)->delete();
return response()->json(['success' => true, 'message' => 'Deleted']);
}
}
Step 6: Conditional Logic di Service/Controller
// Tanpa middleware — cek manual di logic
public function exportReport()
{
if (!ums_can('reports', 'export')) {
abort(403, 'You do not have export permission');
}
// ... generate export
}
// Conditional content berdasarkan role
public function dashboard()
{
$data = ['items' => Item::all()];
if (ums_has_role('dashboard', 'admin')) {
$data['admin_stats'] = $this->getAdminStats();
}
return view('dashboard', $data);
}
Blade Template
Gunakan helper functions langsung di Blade:
{{-- Tampilkan info user --}}
@if(ums_user())
<span>Logged in as: {{ ums_user()->email }}</span>
@endif
{{-- Conditional berdasarkan permission --}}
@if(ums_can('inventory', 'create'))
<button>+ Add Item</button>
@endif
@if(ums_can('inventory', 'delete'))
<button class="btn-danger">Delete Selected</button>
@endif
{{-- Conditional berdasarkan role --}}
@if(ums_has_role('dashboard', 'admin'))
<a href="/admin">Admin Panel</a>
@endif
{{-- Conditional berdasarkan module --}}
@if(ums_has_module('reports'))
<a href="/reports">Reports</a>
@endif
{{-- Show/hide menu items --}}
<nav>
<a href="/dashboard">Dashboard</a>
@if(ums_has_module('inventory'))
<a href="/inventory">Inventory</a>
@endif
@if(ums_has_module('reports'))
<a href="/reports">Reports</a>
@endif
@if(ums_has_role('dashboard', 'admin'))
<a href="/settings">Settings</a>
@endif
</nav>
Advanced: Custom Routes
Jika ingin handle sendiri tanpa auto-registered routes:
UMS_ROUTES_ENABLED=false
Lalu buat sendiri:
// routes/web.php
use Kodepik\UMS\Services\UmsClient;
Route::get('/auth/login', function (UmsClient $ums) {
return redirect()->away($ums->getLoginUrl());
});
Route::get('/auth/callback', function (Request $request, UmsClient $ums) {
$token = $request->query('token');
$claims = $ums->validateToken($token);
session([
'ums_token' => $token,
'ums_refresh_token' => $request->query('refresh_token'),
'ums_user' => $claims->toArray(),
]);
return redirect('/dashboard');
});
Route::get('/auth/logout', function (UmsClient $ums) {
session()->flush();
return redirect()->away($ums->getLogoutUrl(url('/login')));
});
Advanced: Token Refresh
Otomatis (via endpoint)
SDK menyediakan POST /ums/refresh yang bisa dipanggil dari frontend:
// Panggil sebelum token expired
const response = await fetch('/ums/refresh', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
});
const data = await response.json();
// data.data.token = new access token
Manual di backend
use Kodepik\UMS\Facades\UMS;
$refreshToken = session('ums_refresh_token');
$result = UMS::refreshToken($refreshToken);
session([
'ums_token' => $result['token'],
'ums_refresh_token' => $result['refresh_token'],
]);
Troubleshooting
| Problem | Penyebab | Solusi |
|---|---|---|
"kid" empty, unable to lookup correct key |
UMS JWKS tidak memiliki kid field |
SDK sudah handle otomatis (v1.0+) |
Invalid or expired token |
Token JWT sudah expired | Login ulang atau panggil /ums/refresh |
SSL certificate problem |
Self-signed cert di staging | Set UMS_VERIFY_SSL=false di .env |
Missing authorization token |
Request tanpa header Authorization: Bearer ... |
Tambahkan header Bearer token |
Route /ums/login not found |
Package belum ter-register | Jalankan composer dump-autoload dan clear cache |
Redirect ke /login setelah callback |
Session hilang atau callback URL mismatch | Pastikan UMS_CALLBACK_URL match dengan yang terdaftar di UMS |
| 403 padahal user punya permission | Nama module/permission case-sensitive | Cek exact match dengan yang ada di UMS Admin |
| JWKS cache stale setelah key rotation | Cache belum expired | Panggil UMS::invalidateJwksCache() atau php artisan cache:clear |
Session Data
Setelah SSO login berhasil, data berikut tersimpan di Laravel session:
| Key | Type | Isi |
|---|---|---|
ums_token |
string | JWT access token |
ums_refresh_token |
string | Refresh token |
ums_user |
array | Decoded JWT claims |
Security Notes
- ✅ Token divalidasi secara lokal menggunakan RSA public key (RS256) dari JWKS endpoint
- ✅ JWKS key di-cache selama 1 jam (configurable) untuk minimalisir network calls
- ✅ Refresh token hanya disimpan di server-side session (tidak di-expose ke client)
- ⚠️
UMS_VERIFY_SSL=false— hanya untuk development. Di production selalutrue - ⚠️ Pastikan
APP_KEYLaravel sudah di-set (session encryption) - ⚠️ Gunakan HTTPS di production untuk protect token di callback URL
Sample Project
Lihat full working example di:
sample/laravel-ums-integration-sample/
Untuk menjalankan:
cd sample/laravel-ums-integration-sample
composer install
cp .env.example .env
php artisan key:generate
# Update .env dengan UMS config
php artisan serve
Buka http://localhost:8000 → Login with SSO → lihat dashboard test.