diff options
Diffstat (limited to 'app/api/auth')
| -rw-r--r-- | app/api/auth/[...nextauth]/saml/provider.ts | 159 | ||||
| -rw-r--r-- | app/api/auth/[...nextauth]/saml/utils.ts | 175 | ||||
| -rw-r--r-- | app/api/auth/saml/authn-request/route.ts | 76 | ||||
| -rw-r--r-- | app/api/auth/saml/mock-idp/route.ts | 137 |
4 files changed, 462 insertions, 85 deletions
diff --git a/app/api/auth/[...nextauth]/saml/provider.ts b/app/api/auth/[...nextauth]/saml/provider.ts index 92099be0..1f891661 100644 --- a/app/api/auth/[...nextauth]/saml/provider.ts +++ b/app/api/auth/[...nextauth]/saml/provider.ts @@ -1,5 +1,9 @@ import CredentialsProvider from "next-auth/providers/credentials" import { getOrCreateSAMLUser, validateSAMLUserData } from '@/lib/users/saml-service' +import { encode } from 'next-auth/jwt' +import type { User } from 'next-auth' +import type { SAMLUser } from './utils' +import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils' interface SAMLProviderOptions { id: string @@ -28,59 +32,80 @@ export function SAMLProvider(options: SAMLProviderOptions) { } }, async authorize(credentials) { + debugLog('๐ SAMLProvider.authorize called with credentials:', credentials); + try { + debugLog('๐ Checking credentials.user:', { + hasCredentials: !!credentials, + hasUser: !!credentials?.user, + userType: typeof credentials?.user, + userValue: credentials?.user?.substring?.(0, 100) + '...' + }); + if (!credentials?.user) { - console.error('No user data provided') + debugError('No user data provided in credentials') return null } - console.log('๐ SAML Provider: Processing user data') + debugProcess('SAML Provider: Processing user data') // ์ฌ์ฉ์ ๋ฐ์ดํฐ ํ์ฑ (UTF-8 ์ฒ๋ฆฌ ๊ฐ์ ) const userDataString = credentials.user - console.log('๐ค Raw user data string:', userDataString.substring(0, 200) + '...') + debugLog('๐ค Raw user data string:', userDataString.substring(0, 200) + '...') - const userData = JSON.parse(userDataString) + let userData; + try { + userData = JSON.parse(userDataString); + debugSuccess('JSON parsing successful:', userData); + } catch (parseError) { + debugError('JSON parsing failed:', parseError); + debugError('Raw string that failed to parse:', userDataString); + return null; + } // ํ์ฑ๋ ๋ฐ์ดํฐ์ UTF-8 ํ์ธ - console.log('๐ค Parsed user data UTF-8 check:', { + debugLog('๐ค Parsed user data UTF-8 check:', { name: userData.name, nameLength: userData.name?.length, charCodes: userData.name ? [...userData.name].map(c => c.charCodeAt(0)) : [] }) if (!userData.id || !userData.email) { - console.error('Invalid SAML user data:', userData) + debugError('Invalid SAML user data:', userData) return null } - console.log('โ
SAML Provider: User authenticated successfully', { + debugSuccess('SAML Provider: User authenticated successfully', { id: userData.id, email: userData.email, name: userData.name }) // ๐ฅ SAML ์ฌ์ฉ์ ๋ฐ์ดํฐ ๊ฒ์ฆ + debugProcess('Validating SAML user data structure...'); const isValidData = await validateSAMLUserData(userData) + debugLog('Validation result:', isValidData); if (!isValidData) { - console.error('Invalid SAML user data structure:', userData) + debugError('Invalid SAML user data structure:', userData) return null } // ๐ฅ JIT (Just-In-Time) ์ฌ์ฉ์ ์์ฑ ๋๋ ์กฐํ - const dbUser = await getOrCreateSAMLUser({ + debugProcess('Creating/getting SAML user from database...'); + const userCreateData = { email: userData.email, name: userData.name, - // companyId: userData.companyId, - // techCompanyId: userData.techCompanyId, - // ! domain = evcp ์ด๋ฉด vendor๊ฐ ๊ฐ๋ companyId, techCompanyId๋ null companyId: undefined, techCompanyId: undefined, domain: userData.domain - }) + }; + debugLog('User create data:', userCreateData); + + const dbUser = await getOrCreateSAMLUser(userCreateData) + debugLog('Database user result:', dbUser); if (!dbUser) { - console.error('Failed to get or create SAML user') + debugError('Failed to get or create SAML user') return null } @@ -95,10 +120,15 @@ export function SAMLProvider(options: SAMLProviderOptions) { imageUrl: dbUser.imageUrl, // DB์ ์ค์ ์ด๋ฏธ์ง URL } - console.log('โ
SAML Provider: Returning user data to NextAuth:', userResult) + debugSuccess('SAML Provider: Returning user data to NextAuth:', userResult) return userResult } catch (error) { - console.error('โ SAML Provider: Authentication failed', error) + debugError('SAML Provider: Authentication failed', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + errorType: typeof error, + credentials: credentials + }); return null } } @@ -125,4 +155,101 @@ export function validateSAMLOptions(options: SAMLProviderOptions): boolean { return required.every(field => field && field.length > 0) } + +// SAMLProvider์ authorize ํจ์๋ฅผ ์ง์ ํธ์ถํ๊ธฐ ์ํ ํฌํผ +export async function authenticateSAMLUser(userData: SAMLUser) { + debugLog('authenticateSAMLUser called with:', userData); + + try { + // SAMLProvider ๋์ ์ง์ ๋ก์ง ์คํ (Provider ๋ํผ ์์ด) + debugProcess('SAML User Authentication: Processing user data') + + // ์ฌ์ฉ์ ๋ฐ์ดํฐ ๊ฒ์ฆ + if (!userData.id || !userData.email) { + debugError('Invalid SAML user data:', userData) + return null + } + + debugSuccess('SAML User data validated successfully', { + id: userData.id, + email: userData.email, + name: userData.name + }) + + // ๐ฅ SAML ์ฌ์ฉ์ ๋ฐ์ดํฐ ๊ฒ์ฆ + debugLog('Validating SAML user data structure...'); + const isValidData = await validateSAMLUserData(userData) + debugLog('Validation result:', isValidData); + if (!isValidData) { + debugError('Invalid SAML user data structure:', userData) + return null + } + + // ๐ฅ JIT (Just-In-Time) ์ฌ์ฉ์ ์์ฑ ๋๋ ์กฐํ + debugLog('Creating/getting SAML user from database...'); + const userCreateData = { + email: userData.email, + name: userData.name, + companyId: undefined, + techCompanyId: undefined, + domain: userData.domain + }; + debugLog('User create data:', userCreateData); + + const dbUser = await getOrCreateSAMLUser(userCreateData) + debugLog('Database user result:', dbUser); + + if (!dbUser) { + debugError('Failed to get or create SAML user') + return null + } + + // DB์์ ๊ฐ์ ธ์จ ์ค์ ์ฌ์ฉ์ ์ ๋ณด ๋ฐํ + const userResult = { + id: String(dbUser.id), // DB์ ์ค์ ID + name: dbUser.name, // DB์ ์ค์ ์ด๋ฆ + email: dbUser.email, // DB์ ์ค์ ์ด๋ฉ์ผ + companyId: dbUser.companyId, // DB์ ์ค์ ํ์ฌ ID + techCompanyId: dbUser.techCompanyId, // DB์ ์ค์ ๊ธฐ์ ํ์ฌ ID + domain: dbUser.domain, // DB์ ์ค์ ๋๋ฉ์ธ + imageUrl: dbUser.imageUrl, // DB์ ์ค์ ์ด๋ฏธ์ง URL + } + + debugSuccess('SAML User Authentication completed:', userResult) + return userResult; + + } catch (error) { + debugError('authenticateSAMLUser error:', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + userData + }); + return null; + } +} + +// NextAuth JWT ํ ํฐ ์์ฑ ํฌํผ +export async function createNextAuthToken(user: User): Promise<string> { + const token = { + id: user.id, + email: user.email, + name: user.name, + companyId: user.companyId, + techCompanyId: user.techCompanyId, + domain: user.domain, + imageUrl: user.imageUrl, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30์ผ + }; + + const secret = process.env.NEXTAUTH_SECRET!; + return await encode({ token, secret }); +} + +// NextAuth ์ธ์
์ฟ ํค ์ด๋ฆ ๊ฐ์ ธ์ค๊ธฐ +export function getSessionCookieName(): string { + return process.env.NODE_ENV === 'production' + ? '__Secure-next-auth.session-token' + : 'next-auth.session-token'; +}
\ No newline at end of file diff --git a/app/api/auth/[...nextauth]/saml/utils.ts b/app/api/auth/[...nextauth]/saml/utils.ts index 7dfe9581..73c00bf6 100644 --- a/app/api/auth/[...nextauth]/saml/utils.ts +++ b/app/api/auth/[...nextauth]/saml/utils.ts @@ -6,11 +6,12 @@ import { import { getSPMetadata, } from "@/lib/saml/sp-metadata"; +import { debugLog, debugError, debugSuccess, debugProcess, debugMock } from '@/lib/debug-utils'; export interface SAMLProfile { nameID?: string; nameIDFormat?: string; - attributes?: Record<string, string[]>; + attributes?: Record<string, string | string[]>; // ๋ฌธ์์ด ๋๋ ๋ฐฐ์ด ๋ชจ๋ ์ง์ [key: string]: unknown; } @@ -100,6 +101,12 @@ export async function createAuthnRequest(): Promise<string> { "use server"; console.log("SSO STEP 2: Create AuthnRequest"); + + // Mock IdP ๋ชจ๋ ์ฒดํฌ + if (process.env.SAML_MOCKING_IDP === 'true') { + debugMock("Mock IdP mode enabled - simulating SAML response"); + return createMockSAMLFlow(); + } try { const config = createSAMLConfig(); @@ -170,7 +177,7 @@ export async function createAuthnRequest(): Promise<string> { ); try { - const zlib = require("zlib"); + const zlib = await import("zlib"); const decompressed = zlib .inflateRawSync(base64DecodedBuffer) .toString("utf-8"); @@ -182,9 +189,9 @@ export async function createAuthnRequest(): Promise<string> { // XML ๊ตฌ์กฐ ๋ถ์ const xmlLines = decompressed .split("\n") - .filter((line) => line.trim()); + .filter((line: string) => line.trim()); console.log("XML ๊ตฌ์กฐ ์์ฝ:"); - xmlLines.forEach((line, index) => { + xmlLines.forEach((line: string, index: number) => { const trimmed = line.trim(); if ( trimmed.includes("<saml") || @@ -224,7 +231,7 @@ export async function createAuthnRequest(): Promise<string> { ` Callback URL: ${acsMatch ? acsMatch[1] : "์์"}` ); } catch (inflateError) { - console.log("โ Deflate ์์ถ ํด์ ์คํจ:", inflateError.message); + console.log("โ Deflate ์์ถ ํด์ ์คํจ:", (inflateError as Error).message); console.log( " ์๋ณธ ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ (hex):", base64DecodedBuffer.toString("hex").substring(0, 100) + "..." @@ -232,11 +239,11 @@ export async function createAuthnRequest(): Promise<string> { } } } catch (decodeError) { - console.log("โ Base64 ๋์ฝ๋ฉ ์คํจ:", decodeError.message); + console.log("โ Base64 ๋์ฝ๋ฉ ์คํจ:", (decodeError as Error).message); } } } catch (analysisError) { - console.log("โ ๏ธ SAML AuthnRequest ๋ถ์ ์ค ์ค๋ฅ:", analysisError.message); + console.log("โ ๏ธ SAML AuthnRequest ๋ถ์ ์ค ์ค๋ฅ:", (analysisError as Error).message); } console.log("โ
SAML AuthnRequest URL generated:", { @@ -271,9 +278,15 @@ export async function validateSAMLResponse( timestamp: new Date().toISOString(), }); + // Mock IdP ๋ชจ๋ ์ฒดํฌ + if (process.env.SAML_MOCKING_IDP === 'true') { + debugMock("Mock IdP mode - returning mock SAML profile"); + return createMockSAMLProfile(samlResponse); + } + // ์ค์ SAML ๊ฒ์ฆ ์ํ (๊ธฐ๋ณธ๊ฐ) console.log( - "๐ Using Real SAML validation (SAML_USE_MOCKUP=false or not set)" + "๐ Using Real SAML validation (SAML_MOCKING_IDP=false or not set)" ); try { @@ -293,11 +306,11 @@ export async function validateSAMLResponse( throw new Error("No profile returned from SAML validation"); } - // SAMLProfile ํํ๋ก ๋ณํ + // SAMLProfile ํํ๋ก ๋ณํ (ํ์
์์ ์ฑ ํ๋ณด) const samlProfile: SAMLProfile = { - nameID: profile.nameID, - nameIDFormat: profile.nameIDFormat, - attributes: profile.attributes || {}, + nameID: profile.nameID as string | undefined, + nameIDFormat: profile.nameIDFormat as string | undefined, + attributes: profile.attributes as Record<string, string | string[]> | undefined, }; console.log("โ
Real SAML Profile validated successfully:", { @@ -332,71 +345,133 @@ export function mapSAMLProfileToUser(profile: SAMLProfile): SAMLUser { attributes: profile.attributes, }); + // SAML attributes๋ ๋ฌธ์์ด ๋๋ ๋ฐฐ์ด ํํ์ผ ์ ์์ + const extractAttributeValue = (key: string): string | undefined => { + const value = profile.attributes?.[key]; + if (Array.isArray(value)) { + return value.length > 0 ? value[0] : undefined; + } + return typeof value === 'string' ? value : undefined; + }; + // ๊ธฐ๋ณธ์ ์ผ๋ก nameID๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ attributes์์ ์ถ์ถ - const id = - profile.nameID || - profile.attributes?.uid?.[0] || - profile.attributes?.employeeNumber?.[0] || - ""; - const email = - profile.attributes?.email?.[0] || - profile.attributes?.mail?.[0] || - profile.nameID || - ""; - // UTF-8 ์ด๋ฆ ์ฒ๋ฆฌ ๊ฐ์ - let name = - profile.attributes?.displayName?.[0] || - profile.attributes?.cn?.[0] || - profile.attributes?.name?.[0] || - (profile.attributes?.givenName?.[0] && profile.attributes?.sn?.[0] - ? profile.attributes.givenName[0] + " " + profile.attributes.sn[0] - : "") || - ""; + const id = profile.nameID || extractAttributeValue('id') || extractAttributeValue('sub'); + const email = extractAttributeValue('email') || extractAttributeValue('emailAddress'); + const name = extractAttributeValue('name') || extractAttributeValue('displayName') || extractAttributeValue('cn'); + + // ํ์ ํ๋ ๊ฒ์ฆ + if (!id) { + throw new Error('SAML profile missing required field: id (nameID)'); + } + if (!email) { + throw new Error('SAML profile missing required field: email'); + } + if (!name) { + throw new Error('SAML profile missing required field: name'); + } // UTF-8 ๋ฌธ์์ด ์ ๊ทํ ๋ฐ ๊ฒ์ฆ - if (name && typeof name === "string") { - name = name.normalize("NFC").trim(); - - // ํ๊ธ์ด ๊นจ์ง ๊ฒฝ์ฐ ๊ฐ์ง ๋ฐ ๋ก๊ทธ - const hasInvalidChars = /[\uFFFD\x00-\x1F\x7F-\x9F]/.test(name); - if (hasInvalidChars) { - console.warn("โ ๏ธ Invalid UTF-8 characters detected in name:", { - originalName: name, - charCodes: [...name].map((c) => c.charCodeAt(0)), - hexDump: [...name] - .map((c) => "\\x" + c.charCodeAt(0).toString(16).padStart(2, "0")) - .join(""), - }); - } + const normalizedName = name.normalize("NFC").trim(); + + // ํ๊ธ์ด ๊นจ์ง ๊ฒฝ์ฐ ๊ฐ์ง ๋ฐ ๋ก๊ทธ + const hasInvalidChars = /[\uFFFD\x00-\x1F\x7F-\x9F]/.test(normalizedName); + if (hasInvalidChars) { + console.warn("โ ๏ธ Invalid UTF-8 characters detected in name:", { + originalName: name, + normalizedName, + charCodes: [...normalizedName].map((c) => c.charCodeAt(0)), + hexDump: [...normalizedName] + .map((c) => "\\x" + c.charCodeAt(0).toString(16).padStart(2, "0")) + .join(""), + }); } - // ํ์ฌ ์ ๋ณด๋ SSO ๋ก๊ทธ์ธ ์ ์์ + // ํ์ฌ ์ ๋ณด๋ SSO ๋ก๊ทธ์ธ ์ ์์ (evcp ๋๋ฉ์ธ) const companyId = undefined; const techCompanyId = undefined; const domain = 'evcp'; - const user = { + const user: SAMLUser = { id, email, - name: name.trim(), + name: normalizedName, companyId, techCompanyId, domain, }; - console.log("๐ค Mapped user object:", user); + console.log("๐ค Mapped user object:", JSON.stringify(user)); return user; } +// Mock SAML ํ๋ก์ฐ ์์ฑ (ํ
์คํธ์ฉ) +function createMockSAMLFlow(): string { + debugMock("Creating mock SAML flow..."); + + // Mock ๋ชจ๋์์๋ Mock IdP ์๋ํฌ์ธํธ๋ก ๋ฆฌ๋ค์ด๋ ์
+ const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; + const mockIdpUrl = `${baseUrl}/api/auth/saml/mock-idp`; + + debugMock("Mock SAML Flow - redirecting to Mock IdP:", mockIdpUrl); + + return mockIdpUrl; +} + +// Mock SAML Profile ์์ฑ (ํ
์คํธ์ฉ) +function createMockSAMLProfile(samlResponse: string): SAMLProfile { + console.log("๐ญ Creating mock SAML profile from response..."); + + try { + // SAML Response๊ฐ ์ฐ๋ฆฌ๊ฐ ์์ฑํ Mock์ธ์ง ํ์ธ + const decodedXML = Buffer.from(samlResponse, 'base64').toString('utf-8'); + const isMockResponse = decodedXML.includes('MockIdP'); + + if (!isMockResponse) { + console.warn("โ ๏ธ Mock mode enabled but received non-mock SAML Response"); + } + + console.log("๐ญ Mock SAML XML preview:", decodedXML.substring(0, 200) + "..."); + } catch (error) { + console.warn("โ ๏ธ Could not decode SAML Response for mock analysis:", (error as Error).message); + } + + // Mock SAML Profile ๋ฐํ (์ค์ SAML Response์ ์ผ์นํ๋๋ก ๋ฌธ์์ด ํํ) + const mockProfile: SAMLProfile = { + nameID: "testuser@samsung.com", + nameIDFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress", + attributes: { + email: "testuser@samsung.com", + name: "ํ
์คํธ ์ฌ์ฉ์", + displayName: "Test User Samsung", + // ์ถ๊ฐ ํ
์คํธ ์์ฑ๋ค + department: "๊ฐ๋ฐํ", + employeeId: "TEST001", + mobile: "010-1234-5678" + } + }; + + console.log("๐ญ Mock SAML Profile created:", { + nameID: mockProfile.nameID, + nameIDFormat: mockProfile.nameIDFormat, + attributeCount: Object.keys(mockProfile.attributes || {}).length, + attributes: Object.keys(mockProfile.attributes || {}), + timestamp: new Date().toISOString(), + }); + + return mockProfile; +} + // SAML ๋ก๊ทธ์์ URL ์์ฑ (์๋ฒ ์ก์
) // ๋ก๊ทธ์์ ์ง์ ์ํจ. ์ผ๋จ ๊ตฌ์กฐ๋ง ์ ์ฌํ๊ฒ ์์ฑํด๋ . export async function createLogoutRequest(nameID: string): Promise<string> { "use server"; const saml = new SAML(createSAMLConfig()); + // Profile ๊ฐ์ฒด ํํ๋ก ์ ๋ฌ + const profile = { nameID }; return await saml.getLogoutUrlAsync( - nameID, + profile, "", // RelayState { nameIDFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", diff --git a/app/api/auth/saml/authn-request/route.ts b/app/api/auth/saml/authn-request/route.ts index e3cb8a47..f079aea0 100644 --- a/app/api/auth/saml/authn-request/route.ts +++ b/app/api/auth/saml/authn-request/route.ts @@ -1,45 +1,83 @@ -import { NextRequest, NextResponse } from 'next/server' +/** + * 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' -const samlEnvironment = { - NODE_ENV: process.env.NODE_ENV, - SAML_USE_MOCKUP: process.env.SAML_USE_MOCKUP, - NEXTAUTH_URL: process.env.NEXTAUTH_URL, -} +// 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') -// ํ๊ฒฝ๋ณ์ ์ฒดํฌ -function checkEnvironment() { - console.log('๐ Environment check:', JSON.stringify(samlEnvironment, null, 2)) + // ํค ์์ด๋ ๊ตฌํ ๊ฐ๋ฅํด์ ์ฃผ์ ์ฒ๋ฆฌํจ. + // 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() { - console.log('๐ SAML AuthnRequest API started') - checkEnvironment() - + debugProcess('๐ SAML AuthnRequest API started') + try { - console.log('SSO STEP 1: Create AuthnRequest') + // ํ๊ฒฝ๋ณ์ ๊ฒ์ฆ + const environment = validateSAMLEnvironment() + + debugProcess('SSO STEP 1: Create AuthnRequest') const startTime = Date.now() const loginUrl = await createAuthnRequest() const endTime = Date.now() - console.log('SAML AuthnRequest created successfully:', { - url: loginUrl, + 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, - mode: 'real', - message: 'Using real SAML IdP' + isThisMocking: environment.SAML_MOCKING_IDP === 'true' }) } catch (error) { - console.error('Failed to create SAML AuthnRequest:', { + 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() 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..45c670b0 --- /dev/null +++ b/app/api/auth/saml/mock-idp/route.ts @@ -0,0 +1,137 @@ +import { NextRequest, NextResponse } from 'next/server' + +// Mock IdP ์๋ํฌ์ธํธ - SAML Response HTML ํผ ๋ฐํ +export async function GET(request: NextRequest) { + try { + console.log('๐ญ Mock IdP endpoint accessed'); + + // 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="mock_test" /> + <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 |
