Buku Tamu SSO

JavaScript / TypeScript

Contoh integrasi OAuth dengan Node.js + Express.

// oauth-client.ts
import express from "express";
import crypto from "crypto";

const app = express();

// Konfigurasi
const CONFIG = {
  clientId: "YOUR_CLIENT_ID",
  clientSecret: "YOUR_CLIENT_SECRET", // Hanya untuk confidential client
  redirectUri: "http://localhost:3000/callback",
  // Origin saja — path /api/auth/oauth2/... ditambahkan di bawah.
  // Issuer OIDC (untuk validasi iss) = origin + "/api/auth"
  authServerUrl: "https://bukutamu.tabalongkab.go.id",
  issuer: "https://bukutamu.tabalongkab.go.id/api/auth",
  scopes: ["openid", "profile", "email", "role", "organization"],
};

// Store untuk state dan code_verifier (gunakan Redis/session di produksi)
const authStore = new Map<string, { state: string; codeVerifier: string }>();

// Helper: Generate random string
function generateRandomString(length: number): string {
  return crypto.randomBytes(length).toString("base64url").slice(0, length);
}

// Helper: Generate PKCE code challenge
function generateCodeChallenge(codeVerifier: string): string {
  return crypto.createHash("sha256").update(codeVerifier).digest("base64url");
}

// Route: Mulai OAuth flow
app.get("/login", (req, res) => {
  // Generate PKCE values
  const codeVerifier = generateRandomString(64);
  const codeChallenge = generateCodeChallenge(codeVerifier);
  const state = generateRandomString(32);

  // Simpan untuk verifikasi nanti
  authStore.set(state, { state, codeVerifier });

  // Buat authorization URL
  const authUrl = new URL(`${CONFIG.authServerUrl}/api/auth/oauth2/authorize`);
  authUrl.searchParams.set("client_id", CONFIG.clientId);
  authUrl.searchParams.set("redirect_uri", CONFIG.redirectUri);
  authUrl.searchParams.set("response_type", "code");
  authUrl.searchParams.set("scope", CONFIG.scopes.join(" "));
  authUrl.searchParams.set("state", state);
  authUrl.searchParams.set("code_challenge", codeChallenge);
  authUrl.searchParams.set("code_challenge_method", "S256");

  // Redirect user ke SSO Tabalong
  res.redirect(authUrl.toString());
});

// Route: OAuth callback
app.get("/callback", async (req, res) => {
  const { code, state, error, error_description } = req.query;

  // Handle error dari OAuth server
  if (error) {
    return res.status(400).json({
      error,
      error_description,
    });
  }

  // Verifikasi state
  const stored = authStore.get(state as string);
  if (!stored) {
    return res.status(400).json({ error: "Invalid state parameter" });
  }

  // Hapus dari store
  authStore.delete(state as string);

  try {
    // Tukar code dengan token
    const tokenResponse = await fetch(
      `${CONFIG.authServerUrl}/api/auth/oauth2/token`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          grant_type: "authorization_code",
          code: code as string,
          redirect_uri: CONFIG.redirectUri,
          client_id: CONFIG.clientId,
          client_secret: CONFIG.clientSecret,
          code_verifier: stored.codeVerifier,
        }),
      },
    );

    if (!tokenResponse.ok) {
      const errorData = await tokenResponse.json();
      return res.status(400).json(errorData);
    }

    const tokens = await tokenResponse.json();

    // Ambil user info
    const userInfoResponse = await fetch(
      `${CONFIG.authServerUrl}/api/auth/oauth2/userinfo`,
      {
        headers: {
          Authorization: `Bearer ${tokens.access_token}`,
        },
      },
    );

    const userInfo = await userInfoResponse.json();

    // Tampilkan hasil (di produksi: buat session, redirect ke dashboard, dll)
    res.json({
      tokens,
      userInfo,
    });
  } catch (error) {
    console.error("OAuth error:", error);
    res.status(500).json({ error: "Internal server error" });
  }
});

// Route: Refresh token
app.post("/refresh", async (req, res) => {
  const { refreshToken } = req.body;

  try {
    const response = await fetch(
      `${CONFIG.authServerUrl}/api/auth/oauth2/token`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: refreshToken,
          client_id: CONFIG.clientId,
          client_secret: CONFIG.clientSecret,
        }),
      },
    );

    const tokens = await response.json();
    res.json(tokens);
  } catch (error) {
    res.status(500).json({ error: "Failed to refresh token" });
  }
});

app.listen(3000, () => {
  console.log("OAuth client running on http://localhost:3000");
});