summaryrefslogtreecommitdiff
path: root/app/api/saml/callback/route.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/saml/callback/route.ts')
-rw-r--r--app/api/saml/callback/route.ts145
1 files changed, 145 insertions, 0 deletions
diff --git a/app/api/saml/callback/route.ts b/app/api/saml/callback/route.ts
new file mode 100644
index 00000000..d13edc4a
--- /dev/null
+++ b/app/api/saml/callback/route.ts
@@ -0,0 +1,145 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { validateSAMLResponse, mapSAMLProfileToUser } from '../../auth/[...nextauth]/saml/utils'
+
+// IdP가 인증처리 후 POST 요청을 콜백 URL로 날려준다.
+// 이 라우트가 그 요청을 받아준다.
+// GET 요청시 SP 메타데이터를 반환해주는데, 이건 필요 없으면 지우면 된다.
+
+export async function POST(request: NextRequest) {
+ try {
+ console.log('🔐 SAML Callback received at /api/saml/callback')
+
+ // FormData에서 SAML Response 추출
+ const formData = await request.formData()
+ const samlResponse = formData.get('SAMLResponse') as string
+ const relayState = formData.get('RelayState') as string
+
+ console.log('📨 SAML Response received:', {
+ hasResponse: !!samlResponse,
+ relayState: relayState || 'none',
+ responseLength: samlResponse?.length || 0
+ })
+
+ // 🔍 SAML Response 디코딩 및 분석
+ if (samlResponse) {
+ try {
+ console.log('🔍 SAML Response 분석:')
+ console.log('1️⃣ 원본 SAMLResponse (일부):', samlResponse.substring(0, 100) + '...')
+
+ try {
+ // Base64 디코딩 시도
+ const base64Decoded = Buffer.from(samlResponse, 'base64').toString('utf-8')
+ console.log('2️⃣ Base64 디코딩된 XML:')
+ console.log('───────────────────────────────────')
+ console.log(base64Decoded)
+ console.log('───────────────────────────────────')
+
+ // XML 구조 분석
+ const xmlLines = base64Decoded.split('\n').filter(line => line.trim())
+ console.log('3️⃣ XML 구조 요약:')
+ xmlLines.forEach((line, index) => {
+ const trimmed = line.trim()
+ if (trimmed.includes('<saml') || trimmed.includes('<samlp') ||
+ trimmed.includes('ID=') || trimmed.includes('InResponseTo=') ||
+ trimmed.includes('<saml:Assertion') || trimmed.includes('<saml:Subject') ||
+ trimmed.includes('<saml:NameID') || trimmed.includes('<saml:Attribute')) {
+ console.log(` ${index + 1}: ${trimmed}`)
+ }
+ })
+
+ } catch (decodeError) {
+ console.log('2️⃣ Base64 디코딩 실패:', decodeError.message)
+ console.log(' SAMLResponse가 다른 형식으로 인코딩되었을 수 있습니다.')
+ }
+ } catch (analysisError) {
+ console.log('⚠️ SAML Response 분석 중 오류:', analysisError.message)
+ }
+ }
+
+ if (!samlResponse) {
+ console.error('❌ No SAML Response found in request')
+ return NextResponse.redirect(
+ new URL('/ko/auth/error?error=missing_saml_response', request.url)
+ )
+ }
+
+ // SAML Response 검증 및 파싱
+ let samlProfile
+ try {
+ console.log('🔍 Starting SAML Response validation...')
+ samlProfile = await validateSAMLResponse(samlResponse)
+ console.log('✅ SAML Response validated successfully:', {
+ nameId: samlProfile.nameID,
+ attributes: Object.keys(samlProfile.attributes || {})
+ })
+ } catch (error) {
+ console.error('❌ SAML Response validation failed:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ samlResponseLength: samlResponse.length,
+ timestamp: new Date().toISOString()
+ })
+
+ // 더 상세한 에러 정보를 포함한 리다이렉트
+ const errorMessage = error instanceof Error ? error.message : 'Unknown SAML validation error'
+ return NextResponse.redirect(
+ new URL(`/ko/auth/error?error=invalid_saml_response&detail=${encodeURIComponent(errorMessage)}`, request.url)
+ )
+ }
+
+ // SAML 프로필을 사용자 객체로 매핑
+ const user = mapSAMLProfileToUser(samlProfile)
+ console.log('👤 Mapped user:', {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ companyId: user.companyId
+ })
+
+ // NextAuth SAML Provider를 통한 직접 로그인 처리
+ // 서버 사이드에서 NextAuth signIn 호출은 불가능하므로
+ // NextAuth 콜백 엔드포인트로 리다이렉트
+ const nextAuthCallbackUrl = new URL('/api/auth/callback/credentials-saml', request.url)
+
+ // 사용자 데이터를 query parameter로 전달
+ nextAuthCallbackUrl.searchParams.set('user', JSON.stringify(user))
+ if (relayState) {
+ nextAuthCallbackUrl.searchParams.set('state', relayState)
+ }
+
+ console.log('🔄 Redirecting to NextAuth callback:', nextAuthCallbackUrl.toString())
+ return NextResponse.redirect(nextAuthCallbackUrl)
+
+ } catch (error) {
+ console.error('💥 SAML Callback processing failed:', error)
+ return NextResponse.redirect(
+ new URL('/ko/auth/error?error=callback_processing_failed', request.url)
+ )
+ }
+}
+
+// GET 요청에 대한 메타데이터 반환
+// 필요 없으면 굳이 노출할 필요 없음. 다만 SOAP WDSL과 유사하게 이렇게 스펙을 제공하는 건 SAML 2.0 구현 표준임. 필요하다면 IdP에게만 제공되도록 추가 처리하자.
+export async function GET(_request: NextRequest) {
+ try {
+ // GET 요청은 SP 메타데이터 반환 (정적 파일)
+ const fs = await import('fs/promises')
+ const path = await import('path')
+
+ const metadataPath = path.join(process.cwd(), 'config/saml/sp_metadata.xml')
+ const metadata = await fs.readFile(metadataPath, 'utf-8')
+
+ return new NextResponse(metadata, {
+ headers: {
+ 'Content-Type': 'application/xml',
+ 'Cache-Control': 'public, max-age=36000' // 10시간 캐시
+ }
+ })
+ } catch (error) {
+ console.error('Error reading SP metadata file:', error)
+ return NextResponse.json({
+ error: 'Failed to read SP metadata file',
+ details: error instanceof Error ? error.message : 'Unknown error'
+ }, { status: 500 })
+ }
+} \ No newline at end of file