NAVER WORKS IdP SAML SSO 적용 가이드의 하위 문서. NAVER WORKS 고유 이슈 대응과 로컬 HTTPS 개발 환경 구성을 다룬다.
signature reference uri not foundTypeError: signature reference uri not found
at assertRequired (node-saml/lib/utility.js:10:15)
at getVerifiedXml (node-saml/lib/xml.js:84:38)
SAML Response 수신은 성공하지만, 서명 검증 단계에서 에러 발생.
NAVER WORKS IdP가 생성하는 SAML Response의 XML Signature에서 <Reference URI="">를 사용한다.
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<Reference URI=""> <!-- 빈 URI = 전체 문서 서명 -->
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>...</DigestValue>
</Reference>
</SignedInfo>
</Signature>
XML Digital Signature 스펙(RFC 3275)에서 URI=""는 **"전체 문서를 서명 대상으로 한다"**는 의미로 유효하다. 그러나 node-saml의 getVerifiedXml 함수가 이를 처리하지 못한다:
// node-saml/lib/xml.js (v5.1.0)
const refUri = ref.uri // ""
const refId = refUri[0] === '#' ? refUri.substring(1) : refUri // ""
assertRequired(refId, 'signature reference uri not found') // "" → throw!
xml-crypto(node-saml의 내부 의존성)는 URI=""를 정상 처리하지만, node-saml의 래퍼 코드에서 빈 문자열을 falsy로 판단하여 에러를 던진다.
| 접근 | 왜 실패하는가 |
|---|---|
wantAssertionsSigned: false |
서명이 없을 때 허용 여부를 제어할 뿐, 서명이 있는데 검증 실패하는 경우는 해결 불가 |
XML에서 URI=""를 URI="#id"로 수정 |
<SignedInfo>의 정규화(canonicalization) 결과가 달라져 서명 검증 자체가 실패 |
XML Signature에서 서명은 원본
<SignedInfo>에 대해 계산된 것이므로, URI 값을 바꾸면 정규화 결과가 달라져 검증이 반드시 실패한다.
node-saml이 실패하면 xml-crypto로 직접 서명을 검증하고, DOM 파싱으로 프로필을 추출하는 fallback 경로를 추가한다. 상세 구현은 구현 가이드의 "SAML Response 검증 (2단계 Fallback)" 섹션 참고.
export async function validateSAMLResponse(body) {
try {
return await standardValidation(body) // 1차: node-saml
} catch (error) {
if (error.message === 'signature reference uri not found') {
return validateWithEmptyURI(body.SAMLResponse) // 2차: xml-crypto
}
throw error
}
}
node-saml은 널리 쓰이지만, NAVER WORKS처럼 URI=""를 사용하는 IdP와의 호환성 문제가 있다xml-crypto는 URI=""를 올바르게 처리한다. node-saml이 실패할 때 fallback으로 사용 가능/auth/callback?SAMLResponse=xVdZk6pKEn... 405 in 728ms
NAVER WORKS에서 인증 후 콜백 URL로 돌아올 때 405 에러 발생.
Route Handler에 POST만 정의되어 있었다. NAVER WORKS IdP는 경우에 따라 SAMLResponse를 GET 쿼리 파라미터로 전달한다 (HTTP Redirect Binding).
// 문제 코드: POST만 존재
export async function POST(request: NextRequest) { ... }
GET 핸들러를 추가하고, POST/GET 공통 로직을 handleSAMLCallback으로 추출한다.
async function handleSAMLCallback(
request: NextRequest,
samlResponse: string,
relayState: string | null
) {
// 공통 검증 + 세션 생성 + 리다이렉트
}
// HTTP Redirect Binding
export async function GET(request: NextRequest) {
const samlResponse = request.nextUrl.searchParams.get('SAMLResponse')
const relayState = request.nextUrl.searchParams.get('RelayState')
return await handleSAMLCallback(request, samlResponse, relayState)
}
// HTTP POST Binding
export async function POST(request: NextRequest) {
const formData = await request.formData()
const samlResponse = formData.get('SAMLResponse')
const relayState = formData.get('RelayState')
return await handleSAMLCallback(request, samlResponse, relayState)
}
SAML SP를 구현할 때 ACS 엔드포인트는 POST와 GET 모두 처리해야 한다. IdP마다 바인딩 방식이 다르고, 같은 IdP도 상황에 따라 다른 바인딩을 사용할 수 있다.
SAML 인증은 성공하지만, 보호된 페이지로 리다이렉트 후 다시 로그인 페이지로 돌아간다. 세션이 유지되지 않음.
POST /auth/callback → 307 /admin → 307 /auth/login → SAML 재요청 (무한 루프)
원인 1: cookies().set() (Next.js next/headers API)으로 쿠키를 설정한 뒤 NextResponse.redirect()를 반환하면, 쿠키가 리다이렉트 응답에 포함되지 않는 경우가 있다.
// 문제 코드
await createSession(user) // 내부에서 cookies().set() 호출
return NextResponse.redirect(new URL('/admin', baseUrl)) // 쿠키 누락
원인 2: NextResponse.redirect()의 기본 상태 코드는 **307 (Temporary Redirect)**인데, 307은 HTTP 메서드를 보존한다. POST callback에서 307로 리다이렉트하면 다음 페이지에도 POST로 요청이 간다.
response.cookies.set())// 세션 토큰을 직접 생성
const { token, exp } = await createSessionToken(user)
// 303 리다이렉트 + 쿠키를 응답에 직접 설정
const response = NextResponse.redirect(
new URL(callbackUrl, baseUrl),
303 // POST → GET 전환
)
response.cookies.set('auth_session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
expires: new Date(exp * 1000),
})
return response
cookies().set() + NextResponse.redirect() 조합은 쿠키 누락 위험이 있다. 리다이렉트 응답에 response.cookies.set()으로 직접 설정하는 것이 안전하다.callbackUrl is requiredTypeError: callbackUrl is required
at SAML.initialize (node-saml/lib/saml.js:51:38)
로그인 페이지 접근 시 SAML 클라이언트 초기화 실패.
.env.local에 SAML 관련 환경 변수가 없었다. SAML_CALLBACK_URL이 빈 문자열이면 node-saml이 초기화 시점에 에러를 던진다.
환경 변수 파일에 필수 값을 모두 추가하고, 서버 시작 시 validateConfig()으로 누락 여부를 사전에 체크한다.
⚠ Cross origin request detected from local.your-domain.com to /_next/* resource.
로컬 nginx 프록시(local.your-domain.com) → Next.js dev 서버(localhost:3000)로 프록시할 때 발생.
next.config.ts에 허용 도메인 추가:
const nextConfig: NextConfig = {
allowedDevOrigins: ['local.your-domain.com'],
}
SAML SSO는 HTTPS가 필수다. 로컬에서 HTTPS 환경을 구성하는 방법.
mkdir -p certs/local
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout certs/local/local.your-domain.com.key \
-out certs/local/local.your-domain.com.crt \
-subj "/C=KR/ST=Seoul/L=Seoul/O=YourCompany/CN=local.your-domain.com" \
-addext "subjectAltName=DNS:local.your-domain.com"
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain \
certs/local/local.your-domain.com.crt
127.0.0.1 local.your-domain.com
# docker-compose.local-nginx.yml
services:
local-nginx:
image: nginx:alpine
container_name: local-nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./docker-compose.local-nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs/local:/etc/nginx/certs:ro
extra_hosts:
- "host.docker.internal:host-gateway"
// next.config.ts
const nextConfig: NextConfig = {
allowedDevOrigins: ['local.your-domain.com'],
}
# .env.local
SAML_ENTRY_POINT=https://auth.worksmobile.com/saml2/idp/{your-domain}
SAML_ISSUER=https://local.your-domain.com
SAML_CALLBACK_URL=https://local.your-domain.com/auth/callback
SAML_CERT_PATH=./certs/saml-idp.pem
SAML_RESPONSE_ISSUER=https://auth.worksmobile.com/saml2/{your-domain}
AUTH_SECRET={최소 32자}
ALLOW_DEV_AUTH_BYPASS=true
NAVER WORKS Developer Console에서
https://local.your-domain.com/auth/callback을 ACS URL로 등록해야 실제 SAML 플로우가 동작한다.
pnpm dev # Next.js (localhost:3000)
docker compose -f docker-compose.local-nginx.yml up -d # nginx SSL 프록시
# https://local.your-domain.com 접속
| 항목 | 일반적인 IdP | NAVER WORKS |
|---|---|---|
| Signature Reference URI | URI="#responseId" |
URI="" (전체 문서) |
| 서명 위치 | Response 또는 Assertion | Response 레벨만 |
| Assertion 서명 | 개별 서명 포함 | 미포함 |
| SignatureMethod | SHA-256 | SHA-1 (rsa-sha1) |
| CanonicalizationMethod | xml-c14n-20010315 |
xml-c14n-20010315#WithComments |