summaryrefslogtreecommitdiff
path: root/app/api/auth/saml
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/auth/saml')
-rw-r--r--app/api/auth/saml/authn-request/route.ts98
-rw-r--r--app/api/auth/saml/mock-idp/route.ts141
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