@@ -279,29 +279,74 @@ public class SamlContactPerson {
279279 *
280280 * This class extracts and holds the essential information from SAML metadata
281281 * needed for the SP to interact with the IdP.
282- *
283- * @property entityId The IdP's entity ID
284- * @property ssoUrl The Single Sign-On service URL (default binding - HTTP-Redirect or HTTP-POST)
285- * @property sloUrl The Single Logout service URL (default binding - HTTP-Redirect or HTTP-POST), null if not available
286282 */
287- public class IdPMetadata internal constructor(
288- public val entityId : String ,
289- public val ssoUrl : String ,
290- public val sloUrl : String? ,
291- internal val signingCredentials : List <Credential >,
292- private val ssoUrlRedirect : String? = null ,
293- private val ssoUrlPost : String? = null ,
294- private val sloUrlRedirect : String? = null ,
295- private val sloUrlPost : String? = null
296- ) {
283+ public class IdPMetadata internal constructor() {
284+ /* *
285+ * The IdP's entity ID - a unique identifier for the Identity Provider.
286+ * Required.
287+ */
288+ public var entityId: String? = null
289+
290+ /* *
291+ * The Single Sign-On service URL (default binding).
292+ * This is the endpoint where SAML authentication requests should be sent.
293+ * Required.
294+ */
295+ public var ssoUrl: String? = null
296+
297+ /* *
298+ * The Single Logout service URL (default binding).
299+ * This is the endpoint where SAML logout requests and responses should be sent.
300+ * Optional.
301+ */
302+ public var sloUrl: String? = null
303+
304+ /* *
305+ * List of signing credentials (certificates) used to verify signatures from the IdP.
306+ * At least one credential is required for signature verification.
307+ */
308+ public var signingCredentials: List <Credential > = emptyList()
309+
310+ /* *
311+ * SSO URL for HTTP-Redirect binding.
312+ * Falls back to [ssoUrl] if not specified.
313+ */
314+ public var ssoUrlRedirect: String? = null
315+
316+ /* *
317+ * SSO URL for HTTP-POST binding.
318+ * Falls back to [ssoUrl] if not specified.
319+ */
320+ public var ssoUrlPost: String? = null
321+
322+ /* *
323+ * SLO URL for HTTP-Redirect binding.
324+ * Falls back to [sloUrl] if not specified.
325+ */
326+ public var sloUrlRedirect: String? = null
327+
328+ /* *
329+ * SLO URL for HTTP-POST binding.
330+ * Falls back to [sloUrl] if not specified.
331+ */
332+ public var sloUrlPost: String? = null
333+
334+ internal fun validate () {
335+ requireNotNull(entityId) { " Entity ID cannot be null" }
336+ requireNotNull(ssoUrl) { " SSO URL cannot be null" }
337+ require(signingCredentials.isNotEmpty()) {
338+ " No signing certificates found in IdP metadata. Signature verification will fail."
339+ }
340+ }
341+
297342 /* *
298343 * Returns the SSO URL for the specified binding.
299344 * Falls back to the default ssoUrl if binding-specific URL is not available.
300345 */
301346 internal fun getSsoUrlFor (binding : SamlBinding ): String {
302347 return when (binding) {
303- SamlBinding .HttpRedirect -> ssoUrlRedirect ? : ssoUrl
304- SamlBinding .HttpPost -> ssoUrlPost ? : ssoUrl
348+ SamlBinding .HttpRedirect -> checkNotNull( ssoUrlRedirect ? : ssoUrl)
349+ SamlBinding .HttpPost -> checkNotNull( ssoUrlPost ? : ssoUrl)
305350 }
306351 }
307352
@@ -317,6 +362,35 @@ public class IdPMetadata internal constructor(
317362 }
318363}
319364
365+ /* *
366+ * Creates a new SAML Identity Provider metadata configuration.
367+ *
368+ * This factory function allows creating a standalone [IdPMetadata] instance programmatically.
369+ * Typically used for testing or when IdP metadata needs to be constructed from non-XML sources.
370+ * For production use, prefer [parseSamlIdpMetadata] to parse actual IdP metadata XML.
371+ *
372+ * ## Example Usage
373+ *
374+ * ```kotlin
375+ * val idp = IdPMetadata {
376+ * entityId = "https://idp.example.com"
377+ * ssoUrl = "https://idp.example.com/sso"
378+ * sloUrl = "https://idp.example.com/slo"
379+ * signingCredentials = listOf(credential)
380+ * }
381+ * ```
382+ *
383+ * @param configure Configuration block for IdP metadata settings
384+ * @return A configured [IdPMetadata] instance
385+ * @throws IllegalArgumentException if [IdPMetadata.entityId] or [IdPMetadata.ssoUrl] is null
386+ * @throws IllegalArgumentException if [IdPMetadata.signingCredentials] is empty
387+ *
388+ * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.saml.IdPMetadata)
389+ */
390+ public fun IdPMetadata (configure : IdPMetadata .() -> Unit ): IdPMetadata {
391+ return IdPMetadata ().apply (configure).also { it.validate() }
392+ }
393+
320394private val CERT_EXPIRY_WARNING_THRESHOLD = 30 .days
321395
322396/* *
@@ -366,20 +440,17 @@ private fun EntityDescriptor.extractIdPMetadata(validateCertificateExpiration: B
366440
367441 // Extract signing certificates
368442 val signingCredentials = idpDescriptor.extractSigningCredentials(validateCertificateExpiration)
369- require(signingCredentials.isNotEmpty()) {
370- " No signing certificates found in IdP metadata. Signature verification will fail."
371- }
372443
373- return IdPMetadata (
374- entityId = entityId,
375- ssoUrl = ssoUrl,
376- sloUrl = sloUrl,
377- signingCredentials = signingCredentials,
378- ssoUrlRedirect = ssoUrlRedirect,
379- ssoUrlPost = ssoUrlPost,
380- sloUrlRedirect = sloUrlRedirect,
381- sloUrlPost = sloUrlPost
382- )
444+ return IdPMetadata {
445+ this . entityId = entityId
446+ this . ssoUrl = ssoUrl
447+ this . sloUrl = sloUrl
448+ this . signingCredentials = signingCredentials
449+ this . ssoUrlRedirect = ssoUrlRedirect
450+ this . ssoUrlPost = ssoUrlPost
451+ this . sloUrlRedirect = sloUrlRedirect
452+ this . sloUrlPost = sloUrlPost
453+ }
383454}
384455
385456private fun IDPSSODescriptor.extractSigningCredentials (
@@ -430,9 +501,7 @@ private fun X509Certificate.validate() {
430501 if (timeUntilExpiry < CERT_EXPIRY_WARNING_THRESHOLD ) {
431502 val daysUntilExpiry = timeUntilExpiry.inWholeDays
432503 LOGGER .warn(
433- " IdP signing certificate is expiring soon! Subject: $subjectX500Principal , " +
434- " Expires: $notAfter (in $daysUntilExpiry days). " +
435- " Contact your IdP administrator to renew the certificate."
504+ " IdP signing certificate is expiring soon! Subject: $subjectX500Principal , Expires: $notAfter (in $daysUntilExpiry days). Contact your IdP administrator to renew the certificate."
436505 )
437506 }
438507}
0 commit comments