Browse Source
- nginx/geonet-console.conf: tambah CORS header /api/ untuk my.gisportal.id - routes/api.php: POST /auth/refresh (throttle 30/menit) - AuthController: method refresh() validate JWT lama, issue JWT baru - LdapJwtService: expose groups di return validate() - docapi: tambah refresh-token.md + update index.mdmaster
6 changed files with 258 additions and 0 deletions
@ -0,0 +1,187 @@
@@ -0,0 +1,187 @@
|
||||
# Refresh Token |
||||
|
||||
## Tujuan |
||||
|
||||
Memperbarui JWT yang akan habis masa berlakunya tanpa harus login ulang dengan username/password LDAP. |
||||
Digunakan oleh FE PWA untuk menjaga sesi tetap aktif. |
||||
|
||||
## Business Rule |
||||
|
||||
- Token lama harus **masih valid** (belum expired) untuk bisa di-refresh |
||||
- Token baru diterbitkan dengan TTL penuh (`expires_in` dari awal lagi) |
||||
- Token lama tidak di-invalidate (stateless JWT — tidak ada blacklist) |
||||
- Groups dan is_admin diambil dari payload token lama, bukan re-query ke LDAP |
||||
- Rate limit: 30 request / menit per IP |
||||
|
||||
## Kapan Harus Refresh? |
||||
|
||||
Strategi yang direkomendasikan untuk PWA: |
||||
|
||||
``` |
||||
Token diterima saat login → expires_in: 3600 (1 jam) |
||||
↓ |
||||
Simpan: access_token, expires_at = Date.now() + (expires_in * 1000) |
||||
↓ |
||||
Sebelum setiap request API: |
||||
if (Date.now() > expires_at - 5 * 60 * 1000) // 5 menit sebelum expire |
||||
→ POST /auth/refresh |
||||
→ Simpan token baru |
||||
↓ |
||||
Jika refresh gagal (401) → redirect ke halaman login |
||||
``` |
||||
|
||||
## URL |
||||
|
||||
`POST https://console.gisportal.id/api/v1/auth/refresh` |
||||
|
||||
## HTTP Method |
||||
|
||||
`POST` |
||||
|
||||
## Authentication |
||||
|
||||
Token lama (yang akan di-refresh) dikirim di header: |
||||
|
||||
``` |
||||
Authorization: Bearer <jwt_lama_yang_masih_valid> |
||||
``` |
||||
|
||||
## Request Body |
||||
|
||||
Tidak diperlukan (kosong). |
||||
|
||||
## Response Success |
||||
|
||||
**HTTP 200** |
||||
|
||||
```json |
||||
{ |
||||
"data": { |
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huLmRvZSIsImlzX2FkbWluIjp0cnVlLCJleHAiOjE3MTk1MDM2MDB9.new_signature", |
||||
"token_type": "Bearer", |
||||
"expires_in": 3600, |
||||
"auth_type": "ldap_jwt" |
||||
} |
||||
} |
||||
``` |
||||
|
||||
## Response Error |
||||
|
||||
**HTTP 401 — Token tidak ada:** |
||||
```json |
||||
{ |
||||
"error": { |
||||
"code": "missing_token", |
||||
"message": "No token provided." |
||||
} |
||||
} |
||||
``` |
||||
|
||||
**HTTP 401 — Token expired atau invalid:** |
||||
```json |
||||
{ |
||||
"error": { |
||||
"code": "invalid_token", |
||||
"message": "Token invalid or expired." |
||||
} |
||||
} |
||||
``` |
||||
|
||||
## Status Code |
||||
|
||||
| Code | Kondisi | |
||||
|------|---------| |
||||
| 200 | Token berhasil di-refresh | |
||||
| 401 | Token tidak ada / expired / invalid | |
||||
| 429 | Rate limit (30x/menit) | |
||||
|
||||
## Contoh Request |
||||
|
||||
```bash |
||||
curl -s -X POST https://console.gisportal.id/api/v1/auth/refresh \ |
||||
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." |
||||
``` |
||||
|
||||
## Contoh Implementasi PWA (JavaScript) |
||||
|
||||
```javascript |
||||
const TOKEN_KEY = 'geonet_token'; |
||||
const EXPIRES_KEY = 'geonet_expires_at'; |
||||
|
||||
async function getValidToken() { |
||||
const expiresAt = parseInt(localStorage.getItem(EXPIRES_KEY) || '0'); |
||||
const token = localStorage.getItem(TOKEN_KEY); |
||||
|
||||
// Refresh jika kurang dari 5 menit lagi expire |
||||
if (Date.now() > expiresAt - 5 * 60 * 1000) { |
||||
const res = await fetch('https://console.gisportal.id/api/v1/auth/refresh', { |
||||
method: 'POST', |
||||
headers: { 'Authorization': `Bearer ${token}` } |
||||
}); |
||||
|
||||
if (!res.ok) { |
||||
// Token expired total → login ulang |
||||
window.location.href = '/login'; |
||||
return null; |
||||
} |
||||
|
||||
const data = await res.json(); |
||||
localStorage.setItem(TOKEN_KEY, data.data.access_token); |
||||
localStorage.setItem(EXPIRES_KEY, Date.now() + data.data.expires_in * 1000); |
||||
return data.data.access_token; |
||||
} |
||||
|
||||
return token; |
||||
} |
||||
|
||||
// Gunakan sebelum setiap API call |
||||
const token = await getValidToken(); |
||||
const res = await fetch('https://console.gisportal.id/api/v1/databases', { |
||||
headers: { 'Authorization': `Bearer ${token}` } |
||||
}); |
||||
``` |
||||
|
||||
## Sequence Flow |
||||
|
||||
```mermaid |
||||
sequenceDiagram |
||||
PWA->>API: POST /auth/refresh (Bearer token_lama) |
||||
API->>API: Validasi JWT signature & exp |
||||
alt Token masih valid |
||||
API->>API: Issue JWT baru (TTL penuh) |
||||
API->>AuditLog: log api.v1.auth.refresh |
||||
API-->>PWA: 200 {access_token baru, expires_in: 3600} |
||||
else Token expired/invalid |
||||
API-->>PWA: 401 invalid_token |
||||
PWA->>PWA: Redirect ke /login |
||||
end |
||||
``` |
||||
|
||||
## Database yang Diakses |
||||
|
||||
- Tidak ada (JWT stateless) |
||||
- `databaselist_audit` — write audit log |
||||
|
||||
## Audit Log |
||||
|
||||
Event: `api.v1.auth.refresh` |
||||
|
||||
## Rate Limit |
||||
|
||||
30 request / menit per IP. |
||||
|
||||
## Idempotent |
||||
|
||||
Tidak — setiap call menghasilkan token baru dengan exp baru. |
||||
|
||||
## Security |
||||
|
||||
- Token lama tidak di-blacklist (stateless JWT) |
||||
- Jika token bocor, revoke dengan cara: admin bisa rotate `APP_KEY` (invalidate semua JWT) |
||||
- Selalu gunakan HTTPS |
||||
|
||||
## Changelog |
||||
|
||||
| Tanggal | Perubahan | |
||||
|---------|-----------| |
||||
| 2026-06-23 | Endpoint dibuat untuk mendukung FE PWA my.gisportal.id | |
||||
Loading…
Reference in new issue