Developer Portal

Portlogics ID 연동 가이드

사내 서비스와 MCP 서버가 Portlogics 통합 인증을 사용하도록 연동하는 방법을 안내합니다. OAuth 2.1 / OpenID Connect 및 MCP 2025-11-25 명세를 따릅니다.

소개

Portlogics ID 는 Portlogics 사내 서비스의 통합 인증 플랫폼입니다.

  • ID Token 식별자: 불변 sub UUID. 이메일 매칭 금지 — 사용자의 이메일은 바뀔 수 있습니다
  • 지원 Grant: authorization code + PKCE, refresh_token, device_code
  • access_token: JWT (RS256). 사용자 속성은 ext 네임스페이스의 email / name / email_verified / affiliation 에 실려 있어 /userinfo round-trip 없이 1회 서명 검증으로 식별 가능합니다

빠른 시작

  1. 클라이언트 등록 내 클라이언트 페이지 에서 사내 구성원이 직접 등록 (Portlogics 로그인 필요). 사내 전용 RP 는 requires_affiliation 체크박스로 외부 사용자 차단
  2. Discovery 문서 확인 /.well-known/openid-configuration 을 호출해 authorize / token / userinfo / jwks URL 을 동적으로 로드하세요
  3. Authorize 리다이렉트code_challenge (PKCE) 포함하여 /oauth2/auth?response_type=code&client_id=... 로 사용자를 리다이렉트
  4. Callback 에서 code 수령 → token 교환 code_verifier 와 함께 POST /oauth2/token 호출. id_token access_token 수신
  5. 사용자 식별자 저장id_token sub (UUID) 를 RP DB 의 사용자 키로 저장. email / name 은 표시용, 식별용 아님

엔드포인트

Base URL 은 https://id.portlogics.kr 입니다. 모든 엔드포인트는 Discovery 문서에서 동적으로 읽어오는 것을 권장합니다 — 경로/정책이 변경되어도 RP 코드 수정이 필요 없습니다.

목적경로
OpenID Discovery/.well-known/openid-configuration
OAuth 2.0 AS Metadata (RFC 8414)/.well-known/oauth-authorization-server
JSON Web Key Set/.well-known/jwks.json
Authorization/oauth2/auth
Token 교환 / 갱신/oauth2/token
Userinfo/userinfo
Revocation/oauth2/revoke
Dynamic Client Registration (DCR)/oauth2/register

클라이언트 등록

Portlogics 사내 구성원은 내 클라이언트 페이지 에서 OAuth 클라이언트를 직접 등록 · 관리할 수 있습니다. 로그인 후 접근하면 이 계정이 소유한 클라이언트 목록이 보이고, 언제든 새로 만들거나 시크릿을 회전하거나 삭제할 수 있습니다.

등록 시 설정 항목

  • 이름 — 사용자 동의 화면에 표시되고, 관리자가 이 앱을 식별하는 데 쓰입니다
  • Redirect URIs — OAuth 완료 후 브라우저가 돌아올 URL. 정확히 일치해야 하며 와일드카드 불가. HTTPS 권장(로컬 개발의localhost 는 HTTP 허용)
  • Scopeopenid / email / profile / offline_access 중 필요한 것. 이 4개로 고정되어 있으며 커스텀 scope 은 추가할 수 없습니다
  • Audience — MCP 서버 같이 토큰을 수신할 리소스 URL. 일반 RP 는 비워두세요 (RFC 8707)
  • 사내 구성원 전용 — 체크하면 Portlogics 구성원만 이 앱에 로그인할 수 있습니다. 외부 사용자는 consent 단계에서 거부됩니다

셀프서비스 제한

모든 클라이언트는 confidential 타입으로 등록됩니다 (서버 사이드에서 시크릿을 안전히 보관할 수 있어야 함). 다음 경우에는 관리자(happy@portlogics.com) 에게 요청해 주세요:

  • SPA · 모바일 네이티브 등 public 클라이언트 (시크릿 없이 PKCE 만 사용)
  • Claude Desktop · claude.ai 같이 여러 사용자가 공유 하는 호스팅 클라이언트
  • 외부 파트너와 공유하는 통합 클라이언트

스코프 & 클레임

각 스코프 요청 시 id_tokenaccess_token (ext 클레임) 에 포함되는 값입니다.

Scope클레임설명
openidsub필수. 사용자의 불변 UUID 식별자
emailemail, email_verified이메일 및 검증 여부 (표시용)
profilename, affiliation사용자 이름 + 소속 구분 (portlogics_member / external)
offline_accessrefresh_token 발급. 장기 세션 필요 시

Auth Code + PKCE 예시

플로우 개요

User → RP(/login) → Portlogics ID(/oauth2/auth?code_challenge=...)
     → 사용자 로그인 + 동의
     → RP callback(?code=...)
     → POST /oauth2/token (code + code_verifier)
     → id_token + access_token + (refresh_token)

TypeScript 예시

// 1) authorize 리다이렉트 URL 생성
const verifier = randomUrlSafe(64);
const challenge = base64url(sha256(verifier));

const authorizeUrl = new URL("https://id.portlogics.kr/oauth2/auth");
authorizeUrl.searchParams.set("response_type", "code");
authorizeUrl.searchParams.set("client_id", "_internal.my-app");
authorizeUrl.searchParams.set("redirect_uri", "https://my-app.portlogics.kr/cb");
authorizeUrl.searchParams.set("scope", "openid email profile offline_access");
authorizeUrl.searchParams.set("state", randomUrlSafe(16));
authorizeUrl.searchParams.set("nonce", randomUrlSafe(16));
authorizeUrl.searchParams.set("code_challenge", challenge);
authorizeUrl.searchParams.set("code_challenge_method", "S256");

// 2) callback 에서 code 수령 후 token 교환
const tokenResp = await fetch("https://id.portlogics.kr/oauth2/token", {
  method: "POST",
  headers: {
    "content-type": "application/x-www-form-urlencoded",
    authorization: "Basic " + btoa(clientId + ":" + clientSecret),
  },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code,
    redirect_uri: "https://my-app.portlogics.kr/cb",
    code_verifier: verifier,
  }),
});
const tokens = await tokenResp.json();
// tokens.id_token, tokens.access_token, tokens.refresh_token

curl 로 token 교환

curl -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d "grant_type=authorization_code" \
  -d "code=$CODE" \
  -d "redirect_uri=https://my-app.portlogics.kr/cb" \
  -d "code_verifier=$VERIFIER" \
  https://id.portlogics.kr/oauth2/token

MCP 서버 연동

MCP 서버는 Portlogics ID 발급 access_token (JWT) 을 검증해 사용자 식별자를 얻습니다. /userinfo round-trip 없이 JWKS 로 서명만 확인하면 충분합니다.

JWT 검증 코드 (jose)

import { createRemoteJWKSet, jwtVerify } from "jose";

const ISSUER = "https://id.portlogics.kr";
const JWKS = createRemoteJWKSet(
  new URL(ISSUER + "/.well-known/jwks.json"),
);

async function verify(bearer: string) {
  const { payload } = await jwtVerify(bearer, JWKS, {
    issuer: ISSUER,
    audience: "https://my-mcp.portlogics.kr", // client 등록 시 바인딩한 값
  });
  // payload.sub            → 사용자 UUID (식별자)
  // payload.ext.email      → 표시용 이메일
  // payload.ext.affiliation → "portlogics_member" 또는 "external"
  return payload;
}

이중 방어: 서버 측 affiliation 게이트

IdP 쪽 consent 게이트만으로는 충분하지 않습니다. RP/MCP 서버도 같은 조건을 재검증해 좀비 토큰이나 misconfiguration 을 차단하세요.

const ALLOWED_AFFILIATIONS = ["portlogics_member"];
const affiliation = (payload.ext as { affiliation?: string })?.affiliation;
if (!affiliation || !ALLOWED_AFFILIATIONS.includes(affiliation)) {
  return new Response("Forbidden", { status: 403 });
}