diff options
Diffstat (limited to 'app/api/auth/saml')
| -rw-r--r-- | app/api/auth/saml/authn-request/route.ts | 98 | ||||
| -rw-r--r-- | app/api/auth/saml/mock-idp/route.ts | 141 |
2 files changed, 239 insertions, 0 deletions
diff --git a/app/api/auth/saml/authn-request/route.ts b/app/api/auth/saml/authn-request/route.ts new file mode 100644 index 00000000..6544a765 --- /dev/null +++ b/app/api/auth/saml/authn-request/route.ts @@ -0,0 +1,98 @@ +/** + * SAML 2.0 SSO AuthnRequest 생성 API + * + * 역할: + * - 프론트엔드에서 SAML 로그인 URL을 요청할 때 사용 + * - SAML AuthnRequest를 생성하고 IdP 로그인 URL 반환 + * - Mock 모드 지원으로 개발/테스트 환경에서 시뮬레이션 가능 + * + * 플로우: + * 1. 사용자가 "Knox SSO로 로그인" 버튼 클릭 + * 2. 프론트엔드에서 이 API 호출 + * 3. SAML AuthnRequest URL 생성 후 반환 + * 4. 프론트엔드에서 해당 URL로 리다이렉트 + * 5. IdP에서 인증 후 /api/saml/callback으로 SAML Response 전송 + */ + +import { NextResponse } from 'next/server' +import { createAuthnRequest } from '../../[...nextauth]/saml/utils' +import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils' + +// SAML 환경변수 상태 체크 +function validateSAMLEnvironment() { + const samlEnvironment = { + NODE_ENV: process.env.NODE_ENV, + SAML_MOCKING_IDP: process.env.SAML_MOCKING_IDP, + NEXTAUTH_URL: process.env.NEXTAUTH_URL, + SAML_SP_PRIVATE_KEY: process.env.SAML_SP_PRIVATE_KEY ? '✅ Set' : '❌ Missing', + SAML_SP_CERT: process.env.SAML_SP_CERT ? '✅ Set' : '❌ Missing', + } + + debugLog('📊 SAML Environment check:', JSON.stringify(samlEnvironment, null, 2)) + + // 필수 환경변수 검증 + const missingVars = [] + if (!process.env.NEXTAUTH_URL) missingVars.push('NEXTAUTH_URL') + + // 키 없어도 구현 가능해서 주석 처리함. + // if (!process.env.SAML_SP_PRIVATE_KEY) missingVars.push('SAML_SP_PRIVATE_KEY') + // if (!process.env.SAML_SP_CERT) missingVars.push('SAML_SP_CERT') + + if (missingVars.length > 0) { + throw new Error(`Missing required SAML environment variables: ${missingVars.join(', ')}`) + } + + return samlEnvironment +} + +/** + * SAML AuthnRequest URL 생성 엔드포인트 + * + * @returns {JSON} { loginUrl: string, success: boolean, isThisMocking?: boolean } + */ +export async function GET(request: Request) { + debugProcess('🚀 SAML AuthnRequest API started') + + try { + // URL에서 RelayState 매개변수 추출 + const url = new URL(request.url) + const relayState = url.searchParams.get('relayState') + + debugLog('RelayState parameter:', relayState) + + // 환경변수 검증 + const environment = validateSAMLEnvironment() + + debugProcess('SSO STEP 1: Create AuthnRequest') + + const startTime = Date.now() + const loginUrl = await createAuthnRequest(relayState || undefined) + const endTime = Date.now() + + debugSuccess('SAML AuthnRequest created successfully:', { + url: loginUrl.substring(0, 100) + '...', + urlLength: loginUrl.length, + processingTime: `${endTime - startTime}ms`, + mockMode: environment.SAML_MOCKING_IDP === 'true', + timestamp: new Date().toISOString() + }) + + return NextResponse.json({ + loginUrl, + success: true, + isThisMocking: environment.SAML_MOCKING_IDP === 'true' + }) + } catch (error) { + debugError('Failed to create SAML AuthnRequest:', { + error: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + timestamp: new Date().toISOString() + }) + + return NextResponse.json({ + error: 'Failed to create SAML AuthnRequest', + details: error instanceof Error ? error.message : 'Unknown error', + success: false + }, { status: 500 }) + } +} diff --git a/app/api/auth/saml/mock-idp/route.ts b/app/api/auth/saml/mock-idp/route.ts new file mode 100644 index 00000000..eccb6035 --- /dev/null +++ b/app/api/auth/saml/mock-idp/route.ts @@ -0,0 +1,141 @@ +import { NextRequest, NextResponse } from 'next/server' + +// Mock IdP 엔드포인트 - SAML Response HTML 폼 반환 +export async function GET(request: NextRequest) { + try { + // RelayState 파라미터 추출 + const url = new URL(request.url) + const relayState = url.searchParams.get('RelayState') || 'mock_test' + + console.log('🎭 Mock IdP endpoint accessed', { relayState }); + + // Mock SAML Response 데이터 (실제 형태와 일치하도록 문자열 형태) + const mockSAMLResponseData = { + nameID: "testuser@samsung.com", + nameIDFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress", + attributes: { + email: "testuser@samsung.com", + name: "홍길동", + } + }; + + // Mock XML SAML Response 생성 + const mockXML = ` + <samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + ID="_mock_response_${Date.now()}" + Version="2.0" + IssueInstant="${new Date().toISOString()}"> + <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">MockIdP</saml:Issuer> + <samlp:Status> + <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> + </samlp:Status> + <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + ID="_mock_assertion_${Date.now()}" + Version="2.0" + IssueInstant="${new Date().toISOString()}"> + <saml:Issuer>MockIdP</saml:Issuer> + <saml:Subject> + <saml:NameID Format="${mockSAMLResponseData.nameIDFormat}">${mockSAMLResponseData.nameID}</saml:NameID> + </saml:Subject> + <saml:AttributeStatement> + <saml:Attribute Name="email"> + <saml:AttributeValue>${mockSAMLResponseData.attributes.email}</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute Name="name"> + <saml:AttributeValue>${mockSAMLResponseData.attributes.name}</saml:AttributeValue> + </saml:Attribute> + </saml:AttributeStatement> + </saml:Assertion> + </samlp:Response> + `.trim(); + + // Base64 인코딩 + const encodedSAMLResponse = Buffer.from(mockXML, 'utf-8').toString('base64'); + + console.log("🎭 Mock SAML Response created:", { + nameID: mockSAMLResponseData.nameID, + email: mockSAMLResponseData.attributes.email, + name: mockSAMLResponseData.attributes.name, + encodedLength: encodedSAMLResponse.length + }); + + // 콜백 URL로 POST 요청을 시뮬레이션하는 HTML 폼 반환 + const callbackUrl = `${process.env.NEXTAUTH_URL}/api/saml/callback`; // process.env.SAML_SP_CALLBACK_URL + + const mockFormHTML = ` + <!DOCTYPE html> + <html> + <head> + <title>Mock SAML IdP</title> + <style> + body { font-family: Arial, sans-serif; padding: 20px; background-color: #f5f5f5; } + .container { max-width: 600px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } + .header { color: #e94560; text-align: center; margin-bottom: 20px; } + .info { background: #e3f2fd; padding: 15px; border-radius: 4px; margin-bottom: 20px; } + .button { background: #1976d2; color: white; padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; width: 100%; } + .button:hover { background: #1565c0; } + .details { font-size: 14px; color: #666; margin-top: 15px; } + .countdown { font-weight: bold; color: #1976d2; } + </style> + </head> + <body> + <div class="container"> + <h1 class="header">🎭 Mock SAML IdP</h1> + <div class="info"> + <strong>테스트 모드:</strong> 실제 IdP 대신 Mock 응답을 사용합니다.<br> + <em>실제 데이터 형태와 일치하도록 attributes를 문자열로 전송합니다.</em> + </div> + <form id="mockForm" method="POST" action="${callbackUrl}"> + <input type="hidden" name="SAMLResponse" value="${encodedSAMLResponse}" /> + <input type="hidden" name="RelayState" value="${relayState}" /> + <button type="submit" class="button">Continue with Mock Login</button> + </form> + <div class="details"> + <p><strong>테스트 사용자 정보:</strong></p> + <ul> + <li>이메일: ${mockSAMLResponseData.attributes.email}</li> + <li>이름: ${mockSAMLResponseData.attributes.name}</li> + </ul> + <p><span class="countdown" id="countdown">5</span>초 후 자동으로 로그인을 진행합니다...</p> + <p><em>프로덕션 환경에서는 SAML_MOCKING_IDP=false로 설정하세요.</em></p> + </div> + </div> + <script> + let countdown = 5; + const countdownEl = document.getElementById('countdown'); + + const timer = setInterval(() => { + countdown--; + countdownEl.textContent = countdown; + + if (countdown <= 0) { + clearInterval(timer); + console.log('🎭 Auto-submitting mock SAML form...'); + document.getElementById('mockForm').submit(); + } + }, 1000); + + // 사용자가 버튼을 클릭하면 타이머 취소 + document.getElementById('mockForm').addEventListener('submit', () => { + clearInterval(timer); + }); + </script> + </body> + </html> + `; + + return new NextResponse(mockFormHTML, { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + 'Cache-Control': 'no-cache, no-store, must-revalidate' + } + }); + + } catch (error) { + console.error('💥 Mock IdP error:', error); + return NextResponse.json({ + error: 'Mock IdP failed', + details: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 }); + } +}
\ No newline at end of file |
