Skip to content

Express.js

📋 Confidential client 등록이 필요합니다

이 가이드의 코드는 client_secret/oauth/token 에 보냅니다 (서버에서 안전하게 보관). Developer Console 에서 RP 등록 시 Client type: Confidential 을 선택하세요. Public/SPA 클라이언트라면 Public Clients 가이드를 따르고 client_secret 을 절대 클라이언트 코드에 두지 마세요.

js
import express from "express";
import crypto from "node:crypto";
import cookieParser from "cookie-parser";

const app = express();
app.use(cookieParser(process.env.COOKIE_SECRET));

const LOGI = process.env.LOGI_API_URL;
const CLIENT_ID = process.env.LOGI_CLIENT_ID;
const CLIENT_SECRET = process.env.LOGI_CLIENT_SECRET;
const REDIRECT = process.env.LOGI_REDIRECT_URI;

function b64url(buf) {
  return Buffer.from(buf).toString("base64url");
}

app.get("/auth/login", (req, res) => {
  const verifier = b64url(crypto.randomBytes(32));
  const challenge = b64url(crypto.createHash("sha256").update(verifier).digest());
  const state = b64url(crypto.randomBytes(16));

  res.cookie("logi_pkce", verifier, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 600_000 });
  res.cookie("logi_state", state, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 600_000 });

  const url = new URL(`${LOGI}/oauth/authorize`);
  url.searchParams.set("client_id", CLIENT_ID);
  url.searchParams.set("redirect_uri", REDIRECT);
  url.searchParams.set("response_type", "code");
  url.searchParams.set("scope", "openid profile:basic email");
  url.searchParams.set("state", state);
  url.searchParams.set("code_challenge", challenge);
  url.searchParams.set("code_challenge_method", "S256");
  res.redirect(url.toString());
});

app.get("/auth/callback", async (req, res) => {
  // code/state 누락 검증 — 악의적 callback 주입 방지
  if (!req.query.code || !req.query.state) {
    return res.status(400).send("missing code or state");
  }
  if (req.query.state !== req.cookies.logi_state) return res.status(400).send("state mismatch");

  const tokens = await fetch(`${LOGI}/oauth/token`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: req.query.code,
      redirect_uri: REDIRECT,
      code_verifier: req.cookies.logi_pkce,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
    }),
  }).then(r => r.json());

  res.cookie("logi_rt", tokens.refresh_token, { httpOnly: true, secure: true, sameSite: "strict" });
  res.clearCookie("logi_pkce");
  res.clearCookie("logi_state");
  res.redirect("/");
});

Health check endpoint

RP 활성 헬스 체크 프로토콜의 Express 측 핸들러입니다.

1. Env

bash
LOGI_CLIENT_ID=logi_xxxxxxxxxxxxxxxx
LOGI_RP_HEALTH_SECRET=<콘솔에서 복사>

2. Handler

js
import { createHmac, timingSafeEqual } from "node:crypto";

const MAX_SKEW = 300; // seconds

app.get("/.well-known/logi-rp-health", (req, res) => {
  // HTTP 헤더는 case-insensitive — 코드는 소문자 키로 받지만 발송 시 어느 케이스든 동작.
  const ts  = Number(req.get("x-logi-timestamp"));
  const cid = String(req.get("x-logi-client-id") || "");
  const sig = String(req.get("x-logi-signature") || "");

  if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > MAX_SKEW) {
    return res.sendStatus(401);
  }
  if (cid !== process.env.LOGI_CLIENT_ID) {
    return res.sendStatus(401);
  }

  // HMAC-SHA256 hex is always 64 hex chars. Reject malformed input BEFORE
  // Buffer.from(..., "hex") — invalid hex decodes to a shorter buffer and
  // timingSafeEqual would throw on length mismatch (⇒ 500).
  if (!/^[0-9a-f]{64}$/i.test(sig)) {
    return res.sendStatus(401);
  }

  const expected = createHmac("sha256", process.env.LOGI_RP_HEALTH_SECRET || "")
    .update(`${ts}.${cid}`)
    .digest("hex");

  if (!timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) {
    return res.sendStatus(401);
  }

  res.json({
    status: "ok",
    client_id: cid,
    timestamp: new Date().toISOString(),
    sdk_version: "logi-rp-integrate/1.0",
  });
});

3. App 에 mount

위 handler 를 별도 router 파일로 분리한 경우 app.use 로, 같은 app 에 직접 정의한 경우는 그대로 동작합니다.

js
// app.js (또는 server.js)
import logiHealthRouter from "./routes/logi-rp-health.js";
app.use(logiHealthRouter);
// 또는 handler 만 분리한 경우 직접 마운트:
// app.get("/.well-known/logi-rp-health", logiHealthHandler);

Express router 패턴이면 위처럼 mount. handler 만 분리한 경우 두 번째 형태.

4. 콘솔에서 활성화

개발자 콘솔 → 앱 → RP active health check ON → 저장 → 시크릿을 env 에 복사 → 재배포 → 1시간 이내 카드에 🟢.

Identity가 제품의 신뢰를 만듭니다.