summaryrefslogtreecommitdiff
path: root/lib/saml
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-06-20 11:47:15 +0000
committerjoonhoekim <26rote@gmail.com>2025-06-20 11:47:15 +0000
commitabd9f950bbd95b9ad713a26d3fd8a7e0282b7c51 (patch)
treeaafc71d5ff23962c2d6d5e902c66ee070b7ac068 /lib/saml
parent994defd6446ce20c4b4e0d6cc91688b0e64230a4 (diff)
(김준회) SAML 2.0 SSO (Knox Portal) 추가
Diffstat (limited to 'lib/saml')
-rw-r--r--lib/saml/idp-metadata.ts86
-rw-r--r--lib/saml/sp-metadata.ts82
2 files changed, 168 insertions, 0 deletions
diff --git a/lib/saml/idp-metadata.ts b/lib/saml/idp-metadata.ts
new file mode 100644
index 00000000..a33ecad6
--- /dev/null
+++ b/lib/saml/idp-metadata.ts
@@ -0,0 +1,86 @@
+// SAML Identity Provider (IdP) 메타데이터 유틸리티
+
+export interface IDPMetadata {
+ entityId: string
+ ssoUrl: string
+ sloUrl?: string
+ certificate: string
+ nameIdFormat: string
+ organization: string
+ wantAuthnRequestsSigned: boolean
+}
+
+// IdP 메타데이터 가져오기 (환경변수 기반)
+export function getIDPMetadata(): IDPMetadata {
+ console.log('🔍 Loading IdP metadata from environment variables...')
+
+ // 필수 환경변수 검증
+ const entityId = process.env.SAML_IDP_ENTITY_ID
+ const ssoUrl = process.env.SAML_IDP_SSO_URL
+ const certificate = process.env.SAML_IDP_CERT
+
+ if (!entityId || !ssoUrl || !certificate) {
+ throw new Error('Required SAML IdP environment variables are missing: SAML_IDP_ENTITY_ID, SAML_IDP_SSO_URL, SAML_IDP_CERT')
+ }
+
+ const metadata: IDPMetadata = {
+ entityId,
+ ssoUrl,
+ sloUrl: process.env.SAML_IDP_SLO_URL,
+ certificate,
+ nameIdFormat: process.env.SAML_IDP_NAME_ID_FORMAT || 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
+ organization: process.env.SAML_IDP_ORGANIZATION || 'Unknown Organization',
+ wantAuthnRequestsSigned: process.env.SAML_IDP_WANT_AUTHN_REQUESTS_SIGNED === 'true'
+ }
+
+ console.log('✅ IdP metadata loaded from environment variables:', {
+ entityId,
+ ssoUrl,
+ sloUrl: metadata.sloUrl,
+ organization: metadata.organization,
+ wantAuthnRequestsSigned: metadata.wantAuthnRequestsSigned
+ })
+
+ return metadata
+}
+
+// 인증서 형식 정규화 (PEM 형식으로 변환)
+export function normalizeCertificate(cert: string): string {
+ // 이미 PEM 형식인 경우 그대로 반환
+ if (cert.includes('-----BEGIN CERTIFICATE-----')) {
+ return cert
+ }
+
+ // Base64 인증서를 PEM 형식으로 변환 (64자마다 줄바꿈)
+ const cleanCert = cert.replace(/\s+/g, '')
+ const formattedCert = cleanCert.match(/.{1,64}/g)?.join('\n') || cleanCert
+ return `-----BEGIN CERTIFICATE-----\n${formattedCert}\n-----END CERTIFICATE-----`
+}
+
+// 특정 용도의 인증서 가져오기
+export function getCertificateByUse(metadata: IDPMetadata, use: 'signing' | 'encryption'): string {
+ const cert = metadata.certificates.find(c => c.use === use)
+ return cert ? normalizeCertificate(cert.certificate) : ''
+}
+
+// 모든 인증서를 PEM 형식으로 변환
+export function getAllCertificatesAsPEM(metadata: IDPMetadata): { use: string; pem: string }[] {
+ return metadata.certificates.map(cert => ({
+ use: cert.use,
+ pem: normalizeCertificate(cert.certificate)
+ }))
+}
+
+// 레거시 호환성을 위한 함수 - 첫 번째 인증서를 문자열로 반환
+export function getFirstCertificateAsString(metadata: IDPMetadata): string {
+ return metadata.certificates[0]?.certificate || ''
+}
+
+// SP 메타데이터 생성을 위한 헬퍼
+export function getSPEntityId(): string {
+ return process.env.SAML_SP_ENTITY_ID || `${process.env.NEXTAUTH_URL}/saml/metadata`
+}
+
+export function getSPCallbackUrl(): string {
+ return `${process.env.NEXTAUTH_URL}/api/saml/callback`
+} \ No newline at end of file
diff --git a/lib/saml/sp-metadata.ts b/lib/saml/sp-metadata.ts
new file mode 100644
index 00000000..7f25f602
--- /dev/null
+++ b/lib/saml/sp-metadata.ts
@@ -0,0 +1,82 @@
+// SAML Service Provider (SP) 메타데이터 유틸리티
+
+export interface SPAssertionConsumerService {
+ binding: string
+ location: string
+ index: number
+ isDefault?: boolean
+}
+
+export interface SPMetadata {
+ entityId: string
+ // SPSSODescriptor 속성들
+ protocolSupportEnumeration: string[]
+ authnRequestsSigned: boolean
+ wantAssertionsSigned: boolean
+ // 서비스 엔드포인트들
+ assertionConsumerServices: SPAssertionConsumerService[]
+ // 편의를 위한 기본값들
+ defaultACS: SPAssertionConsumerService
+ callbackUrl: string
+}
+
+// SP 메타데이터 가져오기 (환경변수 기반)
+export function getSPMetadata(): SPMetadata {
+ console.log('🔍 Loading SP metadata from environment variables...')
+
+ // 필수 환경변수 검증
+ const entityId = process.env.SAML_SP_ENTITY_ID
+ const callbackUrl = process.env.SAML_SP_CALLBACK_URL || `${process.env.NEXTAUTH_URL}/api/saml/callback`
+
+ if (!entityId) {
+ throw new Error('Required SAML environment variable is missing: SAML_SP_ENTITY_ID')
+ }
+
+ if (!callbackUrl) {
+ throw new Error('SAML_SP_CALLBACK_URL or NEXTAUTH_URL environment variable is required')
+ }
+
+ // AssertionConsumerService 구성
+ const assertionConsumerServices: SPAssertionConsumerService[] = [
+ // 기본 HTTP-POST 바인딩
+ {
+ binding: process.env.SAML_SP_ACS_BINDING_PRIMARY || 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+ location: callbackUrl,
+ index: 0,
+ isDefault: true
+ }
+ ]
+
+ // 보조 HTTP-Redirect 바인딩 (선택사항)
+ const secondaryBinding = process.env.SAML_SP_ACS_BINDING_SECONDARY
+ if (secondaryBinding) {
+ assertionConsumerServices.push({
+ binding: secondaryBinding,
+ location: callbackUrl,
+ index: 1,
+ isDefault: false
+ })
+ }
+
+ const metadata: SPMetadata = {
+ entityId,
+ protocolSupportEnumeration: [
+ process.env.SAML_SP_PROTOCOL_SUPPORT || 'urn:oasis:names:tc:SAML:2.0:protocol'
+ ],
+ authnRequestsSigned: process.env.SAML_SP_AUTHN_REQUESTS_SIGNED === 'true',
+ wantAssertionsSigned: process.env.SAML_SP_WANT_ASSERTIONS_SIGNED === 'true',
+ assertionConsumerServices,
+ defaultACS: assertionConsumerServices[0],
+ callbackUrl
+ }
+
+ console.log('✅ SP metadata loaded from environment variables:', {
+ entityId,
+ callbackUrl,
+ authnRequestsSigned: metadata.authnRequestsSigned,
+ wantAssertionsSigned: metadata.wantAssertionsSigned,
+ acsCount: assertionConsumerServices.length
+ })
+
+ return metadata
+} \ No newline at end of file