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)でCookieを設定した後、NextResponse.redirect()を返すと、Cookieがリダイレクト応答に含まれない場合がある。
// 問題コード
await createSession(user) // 内部でcookies().set()呼び出し
return NextResponse.redirect(new URL('/admin', baseUrl)) // Cookie欠落
原因2: NextResponse.redirect()のデフォルトステータスコードは**307 (Temporary Redirect)**で、307はHTTPメソッドを保持する。POSTコールバックで307リダイレクトすると、次のページにもPOSTでリクエストが行く。
response.cookies.set())// セッショントークンを直接生成
const { token, exp } = await createSessionToken(user)
// 303リダイレクト + Cookieを応答に直接設定
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()の組み合わせはCookie欠落のリスクがある。リダイレクト応答に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 |