NAVER WORKS(LINE WORKS) IdP 모드로 Next.js App Router 기반 웹 애플리케이션에 SAML 2.0 SSO를 연동하는 실전 Best Practice 가이드.
이 가이드는 분량이 많아 4개 문서로 나뉘어져 있다.
SAML (Security Assertion Markup Language) 2.0은 서로 다른 도메인 간에 인증/인가 정보를 안전하게 교환하기 위한 XML 기반 표준 프로토콜이다. SSO(Single Sign-On)를 구현하는 가장 널리 사용되는 엔터프라이즈 표준 중 하나다.
NAVER WORKS(구 LINE WORKS)는 국내 기업용 메신저 및 협업 플랫폼으로, IdP(Identity Provider) 모드를 지원한다. 이를 활용하면:
NAVER WORKS IdP ←→ Next.js App Router (SP)
├── SAML 인증 플로우
├── 세션 관리 (JWE)
└── 접근 제어
| 용어 | 설명 | 예시 |
|---|---|---|
| IdP (Identity Provider) | 사용자 인증을 담당하는 시스템 | NAVER WORKS |
| SP (Service Provider) | 인증을 위임하는 애플리케이션 | Next.js 웹앱 |
| Assertion | 사용자 정보를 담은 XML 문서 | NameID, SessionIndex 등 |
| ACS (Assertion Consumer Service) | SP에서 SAML Response를 받는 엔드포인트 | /auth/callback |
| Entity ID (Issuer) | IdP/SP 각각의 고유 식별자 | https://your-domain.com |
| Binding | SAML 메시지 전달 방식 | HTTP-POST, HTTP-Redirect |
NAVER WORKS IdP 모드에서 가장 일반적인 패턴은 SP-Initiated SSO다. 사용자가 SP(우리 앱)에 먼저 접근하면, SP가 IdP로 인증을 요청하는 방식이다.
<saml2p:AuthnRequest
xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
AssertionConsumerServiceURL="https://your-domain.com/auth/callback"
ID="{고유 ID}"
IssueInstant="2025-01-15T07:42:35Z"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Version="2.0">
<saml2:Issuer
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
https://your-domain.com
</saml2:Issuer>
<saml2p:NameIDPolicy
Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"/>
</saml2p:AuthnRequest>
<saml2p:Response Destination="https://your-domain.com/auth/callback"
ID="{NAVER WORKS 발행 ID}"
InResponseTo="{AuthnRequest ID}"
IssueInstant="2025-01-15T07:42:27.614Z" Version="2.0">
<saml2:Issuer>https://auth.worksmobile.com/saml2/{domain}</saml2:Issuer>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">...</Signature>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:Assertion ID="{ID}" IssueInstant="..." Version="2.0">
<saml2:Subject>
<saml2:NameID Format="unspecified">user@company.com</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData
InResponseTo="{AuthnRequest ID}"
NotOnOrAfter="{만료 시간}"
Recipient="https://your-domain.com/auth/callback"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="{시작}" NotOnOrAfter="{종료}">
<saml2:AudienceRestriction>
<saml2:Audience>https://your-domain.com</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AuthnStatement AuthnInstant="..." SessionIndex="{세션 ID}">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:Password
</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
</saml2:Assertion>
</saml2p:Response>
NAVER WORKS는 표준 SAML 2.0을 따르지만, 다른 IdP(Google Workspace, Okta, Azure AD 등)와 비교해 몇 가지 고유한 특성이 있다. 이를 사전에 파악하지 않으면 연동 과정에서 예상치 못한 에러를 만나게 된다.
| 항목 | 일반적인 IdP (Google, Okta 등) | NAVER WORKS |
|---|---|---|
| Signature Reference URI | URI="#responseId" (특정 노드) |
URI="" (전체 문서) |
| 서명 위치 | Response 또는 Assertion 개별 | Response 레벨만 |
| Assertion 서명 | 개별 서명 포함 | 미포함 |
| SignatureMethod | SHA-256 (rsa-sha256) |
SHA-1 (rsa-sha1) |
| CanonicalizationMethod | xml-c14n-20010315 |
xml-c14n-20010315#WithComments |
| NameID Format | 다양 (email, persistent 등) | unspecified 고정 |
| NameID 값 | 설정에 따라 다름 | 사용자 NAVER WORKS ID (이메일) |
| Protocol Binding | 주로 HTTP-POST | HTTP-POST + HTTP-Redirect 모두 사용 |
핵심 주의: 빈 Signature Reference URI (
URI="")는 가장 큰 차별점이자 가장 많은 문제를 일으키는 특성이다. XML Digital Signature 스펙(RFC 3275)에서URI=""는 완전히 유효하지만, 많은 SAML 라이브러리가 이를 처리하지 못한다. 자세한 내용은 트러블슈팅 문서를 참고.
NAVER WORKS는 AuthnRequest의 ProtocolBinding 설정에 따라 응답 방식이 달라진다:
ACS 엔드포인트는 반드시 GET과 POST 모두 처리해야 한다.
{
"dependencies": {
"@node-saml/node-saml": "^5.1.0",
"xml-crypto": "^6.1.2",
"@xmldom/xmldom": "^0.8.11",
"jose": "^6.1.3"
}
}
| 패키지 | 역할 | 비고 |
|---|---|---|
@node-saml/node-saml |
SAML 프로토콜 처리 (1차) | AuthnRequest 생성, Response 검증 |
xml-crypto |
XML 전자서명 검증 (Fallback) | URI="" 지원 |
@xmldom/xmldom |
XML DOM 파싱 | Fallback 경로에서 사용 |
jose |
JWE 토큰 생성/검증 | 세션 관리 |
node-saml이 이미 xml-crypto와 @xmldom/xmldom에 의존하지만, pnpm의 strict 의존성 정책 때문에 다른 패키지의 내부 의존성을 직접 import할 수 없다. Fallback 검증 경로에서 이 패키지들을 직접 사용하므로, dependencies에 명시적으로 추가해야 한다.
pnpm add xml-crypto @xmldom/xmldom
npm이나 yarn을 사용하는 경우 호이스팅에 의해 별도 설치 없이 import 가능할 수 있지만, 명시적으로 추가하는 것이 안전하다.
src/
├── lib/auth/
│ ├── config.ts # 환경 변수 기반 설정 (SAML, 세션, 접근 제어)
│ ├── saml.ts # SAML 프로토콜 (AuthnRequest, Response 검증)
│ └── session.ts # JWE 세션 관리 (생성, 읽기, 삭제, 갱신)
├── app/(auth)/
│ └── auth/
│ ├── login/page.tsx # SSO 진입점 (Server Component)
│ ├── callback/route.ts # ACS 엔드포인트 (GET + POST)
│ ├── logout/route.ts # 로그아웃 + SLO
│ └── error/page.tsx # 인증 에러 페이지
└── app/(protected)/
└── layout.tsx # 인증 게이트 (세션 확인)
Route 핸들러는 HTTP 요청/응답만 담당하고, 인증 로직은 lib/auth/에 위임한다. 이 분리 덕분에:
session.ts만 수정