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