summaryrefslogtreecommitdiff
path: root/app/api/saml/callback
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/saml/callback')
-rw-r--r--app/api/saml/callback/route.ts124
1 files changed, 83 insertions, 41 deletions
diff --git a/app/api/saml/callback/route.ts b/app/api/saml/callback/route.ts
index d13edc4a..cf9ea772 100644
--- a/app/api/saml/callback/route.ts
+++ b/app/api/saml/callback/route.ts
@@ -1,5 +1,11 @@
import { NextRequest, NextResponse } from 'next/server'
import { validateSAMLResponse, mapSAMLProfileToUser } from '../../auth/[...nextauth]/saml/utils'
+import {
+ authenticateSAMLUser,
+ createNextAuthToken,
+ getSessionCookieName
+} from '../../auth/[...nextauth]/saml/provider'
+import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils'
// IdP가 인증처리 후 POST 요청을 콜백 URL로 날려준다.
// 이 라우트가 그 요청을 받아준다.
@@ -7,14 +13,25 @@ import { validateSAMLResponse, mapSAMLProfileToUser } from '../../auth/[...nexta
export async function POST(request: NextRequest) {
try {
- console.log('🔐 SAML Callback received at /api/saml/callback')
+ const isMockMode = process.env.SAML_MOCKING_IDP === 'true';
+ debugProcess(`SAML Callback received at /api/saml/callback ${isMockMode ? '(🎭 Mock Mode)' : ''}`)
+ debugLog('Request info:', {
+ url: request.url,
+ nextUrl: request.nextUrl?.toString(),
+ mockMode: isMockMode,
+ headers: {
+ host: request.headers.get('host'),
+ origin: request.headers.get('origin'),
+ referer: request.headers.get('referer')
+ }
+ })
// 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:', {
+ debugLog('SAML Response received:', {
hasResponse: !!samlResponse,
relayState: relayState || 'none',
responseLength: samlResponse?.length || 0
@@ -23,104 +40,129 @@ export async function POST(request: NextRequest) {
// 🔍 SAML Response 디코딩 및 분석
if (samlResponse) {
try {
- console.log('🔍 SAML Response 분석:')
- console.log('1️⃣ 원본 SAMLResponse (일부):', samlResponse.substring(0, 100) + '...')
+ debugLog('🔍 SAML Response 분석:')
+ debugLog('원본 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('───────────────────────────────────')
+ debugLog('Base64 디코딩된 XML:')
+ debugLog('───────────────────────────────────')
+ debugLog(base64Decoded)
+ debugLog('───────────────────────────────────')
// XML 구조 분석
const xmlLines = base64Decoded.split('\n').filter(line => line.trim())
- console.log('3️⃣ XML 구조 요약:')
+ debugLog('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}`)
+ debugLog(` ${index + 1}: ${trimmed}`)
}
})
} catch (decodeError) {
- console.log('2️⃣ Base64 디코딩 실패:', decodeError.message)
- console.log(' SAMLResponse가 다른 형식으로 인코딩되었을 수 있습니다.')
+ debugError('Base64 디코딩 실패:', (decodeError as Error).message)
+ debugLog('SAMLResponse가 다른 형식으로 인코딩되었을 수 있습니다.')
}
} catch (analysisError) {
- console.log('⚠️ SAML Response 분석 중 오류:', analysisError.message)
+ debugError('SAML Response 분석 중 오류:', (analysisError as Error).message)
}
}
if (!samlResponse) {
- console.error('❌ No SAML Response found in request')
+ debugError('No SAML Response found in request')
+ const baseUrl = request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000'
return NextResponse.redirect(
- new URL('/ko/auth/error?error=missing_saml_response', request.url)
+ new URL('/ko/evcp', baseUrl)
)
}
// SAML Response 검증 및 파싱
let samlProfile
try {
- console.log('🔍 Starting SAML Response validation...')
+ debugProcess('Starting SAML Response validation...')
samlProfile = await validateSAMLResponse(samlResponse)
- console.log('✅ SAML Response validated successfully:', {
+ debugSuccess('SAML Response validated successfully:', {
nameId: samlProfile.nameID,
attributes: Object.keys(samlProfile.attributes || {})
})
} catch (error) {
- console.error('❌ SAML Response validation failed:', {
+ debugError('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'
+ // SAML 검증 실패 시 evcp 페이지로 리다이렉트
+ const baseUrl = request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000'
return NextResponse.redirect(
- new URL(`/ko/auth/error?error=invalid_saml_response&detail=${encodeURIComponent(errorMessage)}`, request.url)
+ new URL('/ko/evcp', baseUrl)
)
}
// SAML 프로필을 사용자 객체로 매핑
- const user = mapSAMLProfileToUser(samlProfile)
- console.log('👤 Mapped user:', {
- id: user.id,
- email: user.email,
- name: user.name,
- companyId: user.companyId
+ const mappedUser = mapSAMLProfileToUser(samlProfile)
+ debugLog('Mapped user:', {
+ id: mappedUser.id,
+ email: mappedUser.email,
+ name: mappedUser.name,
})
- // NextAuth SAML Provider를 통한 직접 로그인 처리
- // 서버 사이드에서 NextAuth signIn 호출은 불가능하므로
- // NextAuth 콜백 엔드포인트로 리다이렉트
- const nextAuthCallbackUrl = new URL('/api/auth/callback/credentials-saml', request.url)
+ // SAMLProvider를 통한 사용자 인증 (JIT 포함)
+ debugProcess('Authenticating user through SAMLProvider...')
+ const authenticatedUser = await authenticateSAMLUser(mappedUser)
+
+ debugLog('Authenticated user:', authenticatedUser)
- // 사용자 데이터를 query parameter로 전달
- nextAuthCallbackUrl.searchParams.set('user', JSON.stringify(user))
- if (relayState) {
- nextAuthCallbackUrl.searchParams.set('state', relayState)
+ if (!authenticatedUser) {
+ debugError('SAML user authentication failed')
+ const baseUrl = request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000'
+ return NextResponse.redirect(new URL('/ko/evcp', baseUrl))
}
- console.log('🔄 Redirecting to NextAuth callback:', nextAuthCallbackUrl.toString())
- return NextResponse.redirect(nextAuthCallbackUrl)
+ debugSuccess('User authenticated successfully:', {
+ id: authenticatedUser.id,
+ email: authenticatedUser.email,
+ name: authenticatedUser.name
+ })
+
+ // NextAuth JWT 토큰 생성
+ const encodedToken = await createNextAuthToken(authenticatedUser)
+
+ // NextAuth 세션 쿠키 설정
+ const cookieName = getSessionCookieName()
+
+ const response = NextResponse.redirect(new URL('/ko/evcp', request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000'))
+
+ response.cookies.set(cookieName, encodedToken, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ path: '/',
+ maxAge: 30 * 24 * 60 * 60 // 30일
+ })
+
+ debugSuccess('SAML login successful, session created for:', authenticatedUser.email)
+ debugProcess('Authentication flow completed through NextAuth standard pipeline')
+ return response
} catch (error) {
- console.error('💥 SAML Callback processing failed:', error)
+ debugError('SAML Callback processing failed:', error)
+ const baseUrl = request.url || process.env.NEXTAUTH_URL || 'http://localhost:3000'
return NextResponse.redirect(
- new URL('/ko/auth/error?error=callback_processing_failed', request.url)
+ new URL('/ko/evcp', baseUrl)
)
}
}
// GET 요청에 대한 메타데이터 반환
// 필요 없으면 굳이 노출할 필요 없음. 다만 SOAP WDSL과 유사하게 이렇게 스펙을 제공하는 건 SAML 2.0 구현 표준임. 필요하다면 IdP에게만 제공되도록 추가 처리하자.
-export async function GET(_request: NextRequest) {
+export async function GET() {
try {
// GET 요청은 SP 메타데이터 반환 (정적 파일)
const fs = await import('fs/promises')
@@ -136,7 +178,7 @@ export async function GET(_request: NextRequest) {
}
})
} catch (error) {
- console.error('Error reading SP metadata file:', error)
+ debugError('Error reading SP metadata file:', error)
return NextResponse.json({
error: 'Failed to read SP metadata file',
details: error instanceof Error ? error.message : 'Unknown error'