WorkOS로 엔터프라이즈 SSO 붙이기

B2C 서비스를 잘 운영하다가 엔터프라이즈 고객과 계약을 앞두면 십중팔구 듣게 되는 말이 있는데요. “저희는 Okta로 SSO 연동해야 합니다. SAML 메타데이터 보내드릴게요.” “Azure AD 그룹 그대로 사용자 프로비저닝 되죠? SCIM 스펙 문서 공유 드립니다.”

이때부터 일이 커집니다. SAML 명세는 두껍고, 고객사마다 IdP가 Okta, Azure AD, Google Workspace, OneLogin, Ping 등으로 제각각이라 각 IdP의 설정 화면까지 안내해야 해요. WorkOS는 이 고통을 표준화된 API 하나로 감춰주는 서비스입니다. 이 글에서는 WorkOS가 어떤 문제를 풀어주는지, SSO와 Directory Sync를 실제로 어떻게 연동하는지, 그리고 Auth0 같은 범용 IdP와 어떻게 다른지 짚어보겠습니다.

WorkOS가 해결하는 문제

대부분의 인증 서비스가 “로그인 UI와 사용자 DB”를 제공한다면, WorkOS는 앱과 고객사 IdP 사이에 끼어 통역해 주는 미들웨어에 가깝습니다. 고객사가 어떤 IdP를 쓰든 앱 쪽은 WorkOS API만 호출하면 됩니다. 나머지 SAML이나 OIDC, SCIM 프로토콜의 방언은 WorkOS가 뒤에서 알아서 처리해요.

이런 방식이 왜 고마운지는 SAML을 직접 다뤄본 사람이라면 금방 공감합니다. SAML은 XML 서명 검증, 메타데이터 교환, SP-initiated/IdP-initiated 흐름 구분, NameID 포맷 조율까지 챙길 게 많은데요. 고객사 한 곳 붙일 때마다 며칠씩 걸리다 보면 영업팀이 가져오는 딜 속도를 인프라가 못 따라가는 상황이 벌어집니다.

WorkOS는 크게 세 덩어리의 제품을 제공합니다. 우선 SSO 는 SAML과 OIDC를 추상화해 OAuth 스타일의 코드 교환 흐름으로 통일해 줍니다. Directory Sync 는 SCIM 기반의 사용자·그룹 동기화를 웹훅으로 받게 해 주고요. AuthKit 은 비밀번호, 소셜 로그인, MFA까지 포함한 호스팅 로그인 UI로, Auth0의 Universal Login과 비슷한 포지션입니다.

SSO 연동 흐름

SSO 연동의 기본 구조는 OAuth 2.0과 거의 같습니다. 앱에서 WorkOS의 authorization URL로 사용자를 보내면, WorkOS가 고객사 IdP로 다시 리다이렉트해 인증을 받아온 뒤 code를 들고 콜백 URL로 돌아와요. 그 code를 프로필로 교환하는 게 전부입니다.

Node.js SDK로 보면 이렇습니다.

설치
bun add @workos-inc/node
auth.ts
import { WorkOS } from "@workos-inc/node";

const workos = new WorkOS(process.env.WORKOS_API_KEY);
const clientId = process.env.WORKOS_CLIENT_ID!;

export function getLoginUrl(organizationId: string) {
  return workos.sso.getAuthorizationUrl({
    organization: organizationId,
    redirectUri: "https://app.example.com/callback",
    clientId,
  });
}

export async function handleCallback(code: string) {
  const { profile } = await workos.sso.getProfileAndToken({ code, clientId });
  return {
    email: profile.email,
    firstName: profile.firstName,
    lastName: profile.lastName,
    organizationId: profile.organizationId,
  };
}

여기서 주목할 점은 organization이라는 매개변수인데요. WorkOS에서는 고객사 하나를 조직(Organization)으로 등록하고, 그 안에 SSO 연결(Okta SAML, Google OIDC 등)을 붙입니다. 로그인 요청에 조직 ID만 넘기면 WorkOS가 알아서 그 조직의 IdP로 라우팅해 줘요. 그래서 앱 쪽 코드에는 “이 고객은 Okta, 저 고객은 Azure AD”라는 분기가 전혀 필요 없습니다.

어떤 조직인지 판별하는 방법은 보통 이메일 도메인 매핑입니다. 사용자가 @acme.com으로 로그인 폼에 들어오면 Acme 조직의 SSO 연결로 보내는 식이에요. 이를 더 쉽게 해 주는 것이 Admin Portal인데, 고객사 IT 담당자가 직접 메타데이터를 업로드하고 속성 매핑을 확인할 수 있는 호스팅 관리 화면입니다. 이 부분이 가장 큰 시간 절약 포인트예요.

Directory Sync로 사용자 프로비저닝

SSO만으로는 부족한 경우가 종종 있는데요. 엔터프라이즈 고객은 “직원이 퇴사하면 IdP에서 비활성화되는 즉시 너희 앱에서도 로그아웃되어야 한다”라고 요구합니다. SSO는 로그인 시점에만 검증하므로 실시간 프로비저닝을 하려면 Directory Sync(SCIM)가 필요해요.

WorkOS는 IdP에서 오는 SCIM 이벤트를 받아 앱에 웹훅으로 전달해 줍니다.

webhook.ts
import { WorkOS } from "@workos-inc/node";

const workos = new WorkOS(process.env.WORKOS_API_KEY);

export async function handleWebhook(req: Request) {
  const payload = await req.text();
  const signature = req.headers.get("workos-signature")!;

  const webhook = await workos.webhooks.constructEvent({
    payload,
    sigHeader: signature,
    secret: process.env.WORKOS_WEBHOOK_SECRET!,
  });

  switch (webhook.event) {
    case "dsync.user.created":
      await createUser(webhook.data);
      break;
    case "dsync.user.updated":
      await updateUser(webhook.data);
      break;
    case "dsync.user.deleted":
      await deactivateUser(webhook.data.id);
      break;
  }

  return new Response("ok");
}

constructEvent가 HMAC 서명을 검증해 주기 때문에 정말 WorkOS가 보낸 요청인지 확인할 수 있어요. 이 검증을 빼먹으면 공격자가 임의의 사용자를 생성하거나 삭제할 수 있으니, 프로덕션에서는 반드시 켜야 합니다.

이벤트 종류는 dsync.user.*, dsync.group.*, dsync.group.user_added, dsync.group.user_removed 처럼 다양해서 조직이나 역할 매핑까지 IdP 기준으로 맞출 수 있습니다. 새로 들어온 사용자는 온보딩 이메일 트리거로 연결하고 그룹 멤버십은 권한 자동 부여로 엮으면 운영 리소스가 꽤 많이 줄어들어요.

AuthKit으로 일반 로그인까지 묶기

엔터프라이즈만 상대하는 서비스라면 SSO만으로 충분하겠지만, 보통은 무료 사용자는 비밀번호 로그인, 유료 워크스페이스는 SSO 같은 식으로 혼재합니다. 이런 상황에 AuthKit이 유용한데요. 비밀번호, 소셜 로그인, MFA, 매직 링크가 기본 탑재되어 있고, 조직 단위로 SSO를 붙이면 같은 로그인 박스에서 자연스럽게 IdP로 넘어갑니다.

authkit.ts
import { WorkOS } from "@workos-inc/node";

const workos = new WorkOS(process.env.WORKOS_API_KEY);

export function getAuthKitUrl() {
  return workos.userManagement.getAuthorizationUrl({
    provider: "authkit",
    redirectUri: "https://app.example.com/callback",
    clientId: process.env.WORKOS_CLIENT_ID!,
  });
}

provider: "authkit"으로 지정하면 WorkOS가 로그인 UI를 제공하는데, 같은 계정 DB 위에서 비밀번호 사용자와 SSO 사용자를 한 번에 관리할 수 있어요. 이 부분이 Auth0와 가장 비슷한 영역입니다.

Auth0와 비교

두 서비스 다 인증을 외주화해 준다는 점은 같지만 초점이 다릅니다. Auth0는 B2C 소셜 로그인부터 일반 로그인까지 포괄하는 범용 플랫폼이고 사용자 수 기반 과금이라 MAU가 늘수록 비용이 오릅니다. WorkOS는 엔터프라이즈 커넥터 수 기준으로 과금하기 때문에 수만 명짜리 조직을 붙이든 수백 명짜리 조직을 붙이든 가격 차이가 크지 않아요.

실무에서는 두 개를 섞어 쓰는 팀도 꽤 있습니다. 일반 사용자는 Better Auth 같은 오픈소스나 Auth0로 처리하고, 엔터프라이즈 고객만 WorkOS SSO로 연결하는 식이에요. 어떤 조합이 맞는지는 고객 스펙트럼에 따라 다르지만, “세일즈 팀이 SAML 요구를 들고 올 때부터”가 WorkOS 도입을 검토할 만한 타이밍입니다.

마치며

WorkOS는 엔터프라이즈 인증처럼 겉보기엔 단순해 보이지만 실제로는 손이 많이 가는 영역을 API 하나로 다듬어 주는 서비스입니다. 프로토콜 방언을 WorkOS가 대신 받아주니 앱 쪽에서는 OAuth 스타일 코드만 주고받으면 되고, Admin Portal이 고객사 IT 담당자와의 설정 핑퐁도 대신 맡아줘요.

B2C 로그인 관점에서 인증을 더 공부하고 싶다면 Auth0로 빠르게 로그인 기능 붙이기OAuth 2.0 쉽게 이해하기를 같이 보면 그림이 그려집니다. 공식 문서는 WorkOS Docs에서 확인할 수 있어요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord