diff options
| author | joonhoekim <26rote@gmail.com> | 2025-06-23 06:44:34 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-06-23 06:44:34 +0000 |
| commit | ebe273ef4564d55f9bf193adc51a9e58211e72e9 (patch) | |
| tree | 30e8c48be41d14751eceb4c24d88c18d03e9102b /app/api/saml | |
| parent | abd9f950bbd95b9ad713a26d3fd8a7e0282b7c51 (diff) | |
(김준회 SAML 2.0 SSO 리팩터링, 디버깅 유틸리티 추가, MOCK 처리 추가
Diffstat (limited to 'app/api/saml')
| -rw-r--r-- | app/api/saml/callback/route.ts | 124 |
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' |
