@starasia/sso
v3.0.1
Published
Starasia SSO for React SPAs — OIDC authorization code + PKCE (oidc-client-ts) against sso-api.
Keywords
Readme
@starasia/sso v3
Login Starasia SSO untuk React SPA memakai OIDC authorization code + PKCE
(oidc-client-ts) langsung ke sso-api sebagai
public client — tanpa client secret dan tanpa backend auth per aplikasi.
Token ditukar atas nama clientId aplikasi, sehingga sesi, refresh token, dan audit di SSO
tercatat sebagai aplikasi tersebut (v1 menukar token lewat backend lain sehingga sesi bisa
tercatat sebagai aplikasi lain).
v3 berisi perubahan breaking (refresh token tidak lagi diberikan ke kode app). Lihat Migrasi dari v2 dan
CHANGELOG.md.
Keamanan: SDK ini bukan batas otorisasi
ProtectedRoute, useProfile(), dan semua klaim ID token (name, email, role, dll.)
hanya untuk UX — menentukan apa yang ditampilkan. Semuanya berjalan di browser dan bisa
dimanipulasi user, jadi bukan batas keamanan.
Otorisasi wajib ditegakkan di backend: setiap request API memverifikasi access token
(signature, iss, aud, exp, scope/role) memakai createResourceServer dari
@starasia/sso-api. Jangan kirim ID token ke backend sebagai bukti akses, dan jangan anggap
data aman hanya karena halamannya dibungkus ProtectedRoute.
Prasyarat di SSO
Di client aplikasi (admin-dashboard → Applications):
isPublic = true,requirePkce = true- Redirect URIs:
<origin>/callbackuntuk setiap origin (mis.http://localhost:5173/callback,https://app-dev.web.app/callback) - Post-Logout Redirect URIs:
<origin>/ - Bila memakai opsi
resource: daftarkan URI API tersebut di allowlist resource client.
authority = issuer sso-api, sama persis dengan issuer di
<authority>/.well-known/openid-configuration.
Instalasi
pnpm add @starasia/ssoPeer: react ≥18. @tanstack/react-query v5 opsional (hanya untuk createAuthErrorSubscribers).
Paket ini publik di npm (publishConfig.access: "public"), jadi app bisa memasangnya tanpa
token npm.
Pemakaian
// core/configs/app.ts
import { setupSSO } from '@starasia/sso';
export const ssoConfig = setupSSO({
authority: import.meta.env.SPT_SSO_API_BASE_URL,
clientId: import.meta.env.SPT_CLIENT_ID
// resource: 'https://api.example.com/hris' — audience access token (lihat di bawah)
// redirectUri: default `${origin}/callback`
// postLogoutRedirectUri: default `${origin}/`
// scope: default 'openid profile email offline_access'
// appName: teks di bawah logo pada splash
});
export const SSO = ssoConfig.components;
export const { useProfile, useLogout, useAuth } = ssoConfig.hooks;// router — callback DI LUAR ProtectedRoute, sebelum route catch-all
createBrowserRouter([
{ path: '/callback', element: <SSO.CallbackRoute /> },
{ path: '/', element: <SSO.ProtectedRoute><App /></SSO.ProtectedRoute>, children: [/* ... */] }
]);// request backend — token selalu valid (auto-renew via refresh token)
const token = await ssoConfig.actions.getAccessToken();
fetch(url, { headers: { Authorization: `Bearer ${token}` } });
// react-query: 401 → perbarui sesi lalu refetch; gagal → login ulang
const { unsubscribeAll } = ssoConfig.query.createAuthErrorSubscribers(queryClient);Konfigurasi setupSSO(config)
| Opsi | Default | Keterangan |
|---|---|---|
| authority | — (wajib) | Issuer sso-api, sama persis dengan issuer di discovery. |
| clientId | — (wajib) | Client ID aplikasi (public client, PKCE). |
| redirectUri | ${origin}/callback | Harus terdaftar di Redirect URIs client. |
| postLogoutRedirectUri | ${origin}/ | Harus terdaftar di Post-Logout Redirect URIs client. |
| scope | openid profile email offline_access | sso-api hanya memberikan keempat scope ini; scope lain diabaikan. |
| resource | — | Resource indicator (RFC 8707): URI absolut API, dikirim sebagai resource di /authorize. Lihat Resource indicator. |
| extraQueryParams | — | Parameter tambahan untuk /authorize (digabung dengan resource; tidak dikirim ke end_session). |
| storage | window.localStorage | Storage token & state. |
| appName | Star Perkasa Technology | Teks di bawah logo pada splash layar penuh. |
| loadingFallback | splash layar penuh | Tampilan saat gate memeriksa / memperbarui sesi. |
Resource indicator (resource)
setupSSO({ authority, clientId, resource: 'https://api.example.com/hris' });- Dikirim sebagai parameter
resourcedi/authorize(digabung denganextraQueryParams; bila keduanya berisiresource, opsiresourceyang dipakai). - Nilainya harus ada di allowlist resource client di SSO; selain itu sso-api membalas
error=invalid_targetdanCallbackRoutemenampilkan error login. audaccess token = nilairesourcedalam bentuk baku seperti yang tampil di admin dashboard (origin tanpa path mendapat/di akhir, jadi lebih aman memakai identifier ber-path). Backend lalu memakai identifier yang sama sebagaiaudiencesdicreateResourceServer(@starasia/sso-api), mis.createResourceServer({ /* … */ audiences: ['https://api.example.com/hris'] }), sehingga token untuk API/aplikasi lain ditolak.
API setupSSO(config)
| Bagian | Isi |
|---|---|
| components | ProtectedRoute (gate: tanpa sesi → /authorize; token habis → refresh dulu), CallbackRoute (tukar code → token, kembali ke halaman asal; prop renderError) |
| hooks | useProfile() → { data: SSOProfile, isLoading } (klaim id_token), useAuth() → AuthModel (accessToken, idToken, expiresAt), useLogout() → { mutate, logout }, useAuthUser() → { user, loading } (user.refresh_token selalu undefined) |
| actions | login(returnTo?), logout() (cabut refresh token → end_session), getAccessToken(), renew() (hasil tanpa refresh_token) |
| storage | getMainTokenFromStorage() → AuthModel \| null (sinkron, bisa sudah kedaluwarsa) |
| query | createAuthErrorSubscribers(queryClient, { isUnauthorized?, onUnauthorized? }) |
| userManager | instance oidc-client-ts untuk kebutuhan lanjutan — akses mentah, termasuk refresh token. Jangan panggil signinSilent() langsung (melewati lock antar-tab di renew()). |
Juga diekspor: tipe SSOConfig, AuthModel, SSOProfile, User, dan AuthLayout.
Refresh token hanya dikelola SDK (disimpan oidc-client-ts di storage) dan tidak pernah
dikembalikan oleh hook, storage.getMainTokenFromStorage(), maupun actions.renew().
Splash & loading
Animasi logo SPT di tengah papan sirkuit (CSS murni, logo tersemat di bundle — tanpa aset atau stylesheet tambahan di app).
| Ekspor | Kegunaan |
|---|---|
| LoadingFallback | Splash layar penuh selama ter-mount. Default loadingFallback gate SSO. |
| useSplash(active) | Tahan splash layar penuh selama active — mis. saat app memeriksa akses user setelah login, agar splash berlanjut sampai halaman pertama siap. |
| Loading | Versi inline (mis. di dalam tabel). Prop width, label. |
| Splash | Komponen dasar (state, label, fullscreen, size). |
- Splash layar penuh adalah satu instance di
document.body, jadi perpindahan/callback→ halaman tujuan → cek akses app memakai splash yang sama (tidak mulai ulang). - Tampil minimal 1,5 detik sejak splash muncul (0,5 detik saat
prefers-reduced-motion) agar animasi pembuka selalu selesai, juga saat bundle app lambat dimuat; lalu keluar dengan animasi ~1 detik. Pemegang yang mengambil alih (mis./callback→ gate halaman) tidak memulai hitungan dari awal. Scroll dikunci selama tampil. - Teks di bawah logo:
setupSSO({ appName })(defaultStar Perkasa Technology). - Warna/font bisa di-override:
--sa-splash-navy,--sa-splash-azure,--sa-splash-font.
Perilaku penting:
- Perpanjangan token. Refresh token dirotasi sso-api setiap dipakai, dan refresh token yang
dipakai ulang dianggap pencurian (seluruh family dicabut → semua tab harus login ulang).
Karena itu
automaticSilentRenewoidc-client-ts dimatikan; eventaccessTokenExpiring(±60 detik sebelum habis),ProtectedRoute,getAccessToken(), dan handler 401 semuanya lewatrenew(). - Multi-tab.
renew()memakai Web Locks (navigator.locks, namastarasia-sso-refresh:<authority>:<clientId>) sehingga hanya satu tab yang refresh pada satu waktu. Di dalam lock storage dibaca ulang: bila tab lain sudah refresh, token barunya dipakai tanpa request. Tanpa Web Locks (browser lama, atau originhttp://non-localhost) → hanya dedupe dalam tab. - Sinkronisasi antar-tab. Token diperbarui di tab lain → tab ini memuat token baru (event
storage). User dihapus di tab lain (logout) →ProtectedRoutemenyembunyikan UI dan redirect ke login. - Logout. Refresh token dicabut dulu (
POST /revoke, RFC 7009, denganclient_id), lalu redirect keend_sessiondenganclient_id(+id_token_hintbila ada;client_idtidak pernah terkirim dua kali). Bila redirect gagal disiapkan (mis. discovery tidak bisa diambil), user lokal dihapus dan browser tetap diarahkan keend_session(endpoint discovery, atau<authority>/end_session) — supaya sesi di SSO ikut berakhir, bukan "logout palsu" yang langsung masuk lagi. - Token habis dan refresh gagal →
ProtectedRoutemengarahkan ke login ulang. - 401 berulang: setelah renew berhasil tapi 401 lagi dalam 10 detik → login ulang; 401 lagi dalam 60 detik setelah login ulang → redirect dihentikan (log error) untuk mencegah loop bila backend/gateway menolak token.
returnTohanya menerima path relatif same-origin (cegah open redirect).
Migrasi dari v2
| v2 | v3 |
|---|---|
| AuthModel.refreshToken (useAuth(), storage.getMainTokenFromStorage()) | dihapus — refresh token hanya dikelola SDK |
| useAuthUser().user.refresh_token, actions.renew() → User dengan refresh_token | refresh_token selalu undefined |
| automaticSilentRenew oidc-client-ts (timer per tab) | accessTokenExpiring → renew() dengan Web Lock antar-tab |
| logout() me-renew token kedaluwarsa demi id_token_hint | tanpa renew; refresh token dicabut lalu end_session dengan client_id |
| logout() gagal → hanya hapus user lokal, ke postLogoutRedirectUri | hapus user lokal, lalu tetap ke end_session |
| — | opsi resource (RFC 8707) |
Langkah per app:
- Hapus semua pemakaian
refreshToken/refresh_tokendari SDK. Untuk memanggil API cukupactions.getAccessToken(); perpanjangan sesi ditangani SDK. - Bila memakai
userManagerlangsung: jangan panggilsigninSilent()ataustartSilentRenew()— pakaiactions.renew(). - Opsional: set
resourcedan samakanaudiencesdi backend (createResourceServer). - Pastikan backend menegakkan otorisasi dari access token (lihat Keamanan).
Migrasi dari v1
| v1 | v2 |
|---|---|
| setupSSO({ app: { api, key, name, sso: { clientId, redirectUri }, storage } }) | setupSSO({ authority, clientId }) |
| api.baseUrl = company-management-service / backend /auth/* | authority = issuer sso-api |
| sso.redirectUri = halaman login SSO | redirectUri = <origin>/callback milik app (default) |
| ?authToken= + POST /auth/callback | /authorize + POST /token (PKCE) |
| key storage access / auth / otp | storage oidc-client-ts (oidc.user:<authority>:<clientId>) |
| useValidate, useSilentAuth, useAuthCallback, hooks.logout(token) | dihapus — ditangani ProtectedRoute / actions.logout() |
| storage.getMainTokenFromStorage<T>() | storage.getMainTokenFromStorage() atau actions.getAccessToken() |
| DecodeToken.organization / role | belum ada (sso-api belum mengirim klaim org) |
| peer react-router-dom | tidak dibutuhkan |
| Loading / LoadingFallback (lottie) | animasi logo SPT (CSS); Loading kini inline, LoadingFallback layar penuh; dependensi lottie-web dihapus |
Langkah per app: tambah route /callback, daftarkan Redirect/Post-Logout URIs di client SSO,
ganti env (SPT_SSO_API_BASE_URL = issuer; SPT_REDIRECT_URI, SPT_STORAGE_AUTH,
SPT_AUTH_SERVICE tidak dipakai lagi). Lalu ikuti Migrasi dari v2.
Belum didukung: pemilihan organisasi (multi-org). App yang membutuhkan
organizationdi token tetap di v1 sampai sso-api menyediakan klaim/alur org.
Build
pnpm install
pnpm type-check
pnpm test # vitest
pnpm build # dist/starasia-sso.{es,umd}.js + dist/types (tanpa source map)License
Private internal package. Redistribution is restricted.
