Skip to content

Panduan Integrasi React.js ​

Panduan ini menjelaskan cara mengintegrasikan DUMEG Identity (Keycloak SSO) ke dalam aplikasi React.js (SPA) menggunakan TypeScript, Vite, dan library terawat react-oidc-context bersama oidc-client-ts.

Status Kode Referensi (Starter Template)

Contoh kode dan aplikasi demo di bawah ini adalah implementasi referensi (starter kit) yang telah diverifikasi alur protokolnya secara lokal menggunakan Mock OIDC Server, namun belum diuji menggunakan server Keycloak nyata (karena registrasi client resmi pada realm Keycloak production masih menunggu alokasi tim IAM). Kode ini BUKAN implementasi siap-production secara langsung. Setiap tim wajib melakukan penyesuaian dengan kebutuhan routing, state management, dan audit keamanan internal DUMEG sebelum diaplikasikan ke lingkungan production.


1. Arsitektur Integrasi React (Public Client) ​

Aplikasi React SPA berjalan sepenuhnya di sisi browser pengguna tanpa backend khusus, sehingga didaftarkan sebagai Public OIDC Client.

mermaid
flowchart LR
    A[Browser / React App\nlocalhost:5174] -->|1. Generate PKCE & Redirect| B{DUMEG Identity\nlogin.dumeg.com}
    B -->|2. Callback dengan Code & State| A
    A -->|3. Tukar Code + Verifier via HTTPS| B
    B -->|4. Kembalikan ID & Access Token| A
    A -->|5. Simpan di SessionStorage / Memory| C[(React Auth Context)]

Prinsip Keamanan Public Client: ​

  • DILARANG MENGGUNAKAN client_secret: Karena kode JavaScript browser dapat diinspeksi oleh siapa saja, public client tidak memiliki rahasia client.
  • Wajib PKCE S256: Pertukaran kode otorisasi dilindungi secara kriptografis menggunakan Proof Key for Code Exchange (RFC 7636).
  • In-Memory Storage (Prioritas): Token disimpan dalam memori runtime JavaScript (InMemoryWebStorage) untuk menghindari paparan di Web Storage (localStorage atau sessionStorage).

Batasan Keamanan In-Memory Storage & XSS

Menyimpan token dalam memori JavaScript secara efektif menghilangkan persistensi token pada Web Storage (localStorage/sessionStorage), sehingga mencegah skrip jahat mencuri token via storage exfiltration.

Namun, penyimpanan dalam memori TIDAK membuat aplikasi kebal dari serangan XSS. Jika penyerang berhasil menyuntikkan skrip ke dalam DOM, penyerang tetap dapat menginspeksi memori objek JavaScript atau mengeksekusi request API atas nama pengguna (session riding). Sanitasi input, escaping output, dan penerapan Content Security Policy (CSP) tetap merupakan pertahanan utama yang wajib diimplementasikan.


2. Persyaratan & Spesifikasi ​

  • Runtime: Node.js versi 20.x atau 22.x LTS
  • Build Tool: Vite 5.x / 6.x
  • Framework: React 18.x / 19.x (TypeScript)
  • Port Demo Lokal: http://localhost:5174

Dependencies yang Diperlukan: ​

bash
npm install react-oidc-context oidc-client-ts react-router-dom

3. Konfigurasi OIDC Client di Keycloak ​

Untuk menggunakan aplikasi React di lingkungan lokal, ajukan pendaftaran client publik dengan parameter berikut:

ParameterNilai KonfigurasiKeterangan
Client IDdumeg-react-demoIdentifier unik aplikasi
Client AuthenticationOff (Public Client)Tidak memerlukan client secret
Authentication FlowStandard Flow (Authorization Code)OIDC Standard
PKCE EnforcementS256Wajib untuk public client
Valid Redirect URIshttp://localhost:5174/callback
http://localhost:5174/
URL tujuan setelah login
Valid Post Logout URIshttp://localhost:5174/URL tujuan setelah logout
Web Origins (CORS)http://localhost:5174Mengizinkan request token via browser

4. Konfigurasi Environment (.env.example) ​

Buat file .env pada root proyek React:

dotenv
# Konfigurasi DUMEG Identity (Keycloak) untuk React SPA
VITE_DUMEG_AUTHORITY="https://login.dumeg.com/realms/dumeg-internal"
VITE_DUMEG_CLIENT_ID="dumeg-react-demo"
VITE_DUMEG_REDIRECT_URI="http://localhost:5174/callback"
VITE_DUMEG_POST_LOGOUT_URI="http://localhost:5174/"
VITE_DUMEG_SCOPE="openid profile email"

Tidak Ada Client Secret

Variabel di atas berawalan VITE_ dan dapat dibaca oleh browser. Jangan pernah menambahkan client secret di file ini.


5. Implementasi Kode ​

A. Konfigurasi OIDC Provider (src/config/oidc.ts) ​

Gunakan InMemoryWebStorage untuk token (in-memory) dan sessionStorage untuk parameter state PKCE:

typescript
// src/config/oidc.ts
import { AuthProviderProps } from 'react-oidc-context'
import { WebStorageStateStore, InMemoryWebStorage } from 'oidc-client-ts'

export const oidcConfig: AuthProviderProps = {
  authority: import.meta.env.VITE_DUMEG_AUTHORITY || 'https://login.dumeg.com/realms/dumeg-internal',
  client_id: import.meta.env.VITE_DUMEG_CLIENT_ID || 'dumeg-react-demo',
  redirect_uri: import.meta.env.VITE_DUMEG_REDIRECT_URI || 'http://localhost:5174/callback',
  post_logout_redirect_uri: import.meta.env.VITE_DUMEG_POST_LOGOUT_URI || 'http://localhost:5174/',
  response_type: 'code',
  scope: import.meta.env.VITE_DUMEG_SCOPE || 'openid profile email',
  
  // KEAMANAN: Simpan token dalam memori JavaScript murni (InMemoryWebStorage)
  // Mencegah pencurian kredensial via script scraping localStorage/sessionStorage
  userStore: new WebStorageStateStore({ store: new InMemoryWebStorage() }),

  // State dan code_verifier PKCE harus bertahan selama navigasi redirect browser
  stateStore: new WebStorageStateStore({ store: window.sessionStorage }),
  
  // Otomatis memperbarui token sebelum expired
  automaticSilentRenew: true,
  
  // Tangani pembersihan URL callback
  onSigninCallback: () => {
    window.history.replaceState({}, document.title, window.location.pathname)
  },
}

B. Inisialisasi Root Provider (src/main.tsx) ​

tsx
// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { AuthProvider } from 'react-oidc-context'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import { oidcConfig } from './config/oidc'
import './styles/app.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <AuthProvider {...oidcConfig}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </AuthProvider>
  </React.StrictMode>
)

C. Komponen Rute Terproteksi (src/components/ProtectedRoute.tsx) ​

tsx
// src/components/ProtectedRoute.tsx
import React from 'react'
import { useAuth } from 'react-oidc-context'
import { Navigate } from 'react-router-dom'

interface Props {
  children: React.ReactNode
}

export const ProtectedRoute: React.FC<Props> = ({ children }) => {
  const auth = useAuth()

  if (auth.isLoading) {
    return <div className="loading-state">Memeriksa status autentikasi...</div>
  }

  if (!auth.isAuthenticated) {
    return <Navigate to="/" replace />
  }

  return <>{children}</>
}

D. Penanganan Login & Profil (src/pages/HomePage.tsx) ​

tsx
// src/pages/HomePage.tsx
import React from 'react'
import { useAuth } from 'react-oidc-context'
import { Link } from 'react-router-dom'

export const HomePage: React.FC = () => {
  const auth = useAuth()

  if (auth.isLoading) {
    return <div className="loading-state">Memuat layanan identitas...</div>
  }

  if (auth.error) {
    return (
      <div className="error-box">
        <h3>Terjadi Kesalahan Autentikasi</h3>
        <p>{auth.error.message}</p>
        <button onClick={() => auth.signinRedirect()}>Coba Login Kembali</button>
      </div>
    )
  }

  return (
    <div className="home-container">
      <h1>Portal Aplikasi React</h1>
      <p>Terintegrasi dengan DUMEG Identity Single Sign-On.</p>

      {auth.isAuthenticated ? (
        <div className="user-banner">
          <p>Selamat datang, <strong>{auth.user?.profile.name || auth.user?.profile.preferred_username}</strong>!</p>
          <div className="btn-group">
            <Link to="/dashboard" className="btn-primary">Buka Dashboard</Link>
            <button onClick={() => auth.signoutRedirect()} className="btn-secondary">Logout</button>
          </div>
        </div>
      ) : (
        <div className="login-action">
          <button onClick={() => auth.signinRedirect()} className="btn-login-dumeg">
            Masuk dengan DUMEG Identity
          </button>
        </div>
      )}
    </div>
  )
}

6. Alur Logout Terpusat (RP-Initiated Logout) ​

Memanggil auth.signoutRedirect() akan:

  1. Menghapus data token dari sessionStorage browser.
  2. Mengarahkan browser ke endpoint logout Keycloak: https://login.dumeg.com/realms/dumeg-internal/protocol/openid-connect/logout
  3. Mengakhiri sesi SSO Keycloak di browser, lalu me-redirect kembali ke post_logout_redirect_uri.

7. Troubleshooting Masalah Umum ​

Masalah / ErrorPenyebabSolusi
CORS error saat menukar codeWeb Origins di Keycloak belum diaturTambahkan http://localhost:5174 pada kolom Web Origins di konfigurasi client Keycloak.
Invalid redirect_uriRedirect URI tidak cocok persisPastikan http://localhost:5174/callback terdaftar di Valid Redirect URIs Keycloak.
PKCE code_verifier mismatchsessionStorage terhapus atau dibersihkan ekstensiPastikan tidak ada ekstensi browser pemblokir cookie/storage yang menghapus verifier sebelum callback selesai.
Token hilang saat buka tab baruPenggunaan sessionStorageKarakteristik normal sessionStorage. Gunakan silent renew untuk mengaktifkan sesi di tab baru tanpa login ulang.

8. Menjalankan Aplikasi Contoh Lokal ​

Aplikasi contoh React lengkap tersedia di direktori examples/react-sso:

bash
cd examples/react-sso
npm install
cp .env.example .env
npm run dev

Buka browser pada alamat: http://localhost:5174

Pengujian Lokal Menggunakan Mock OIDC

Proyek ini menyediakan server Mock OIDC lokal di examples/mock-oidc (port 8088) yang dapat digunakan untuk menguji alur login PKCE, callback, pembacaan claims identitas, dan in-memory storage secara deterministik sebelum client resmi didaftarkan di Keycloak production.

Developer Portal Single Sign-On (SSO) Ekosistem DUMEG