Skip to content

Commit 13a32a2

Browse files
committed
add IdPMetadata public factory
1 parent affd59e commit 13a32a2

File tree

2 files changed

+115
-32
lines changed

2 files changed

+115
-32
lines changed

ktor-server/ktor-server-plugins/ktor-server-auth-saml/api/ktor-server-auth-saml.api

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,21 @@ public final class io/ktor/server/auth/saml/DigestAlgorithm$Companion {
2121

2222
public final class io/ktor/server/auth/saml/IdPMetadata {
2323
public final fun getEntityId ()Ljava/lang/String;
24+
public final fun getSigningCredentials ()Ljava/util/List;
2425
public final fun getSloUrl ()Ljava/lang/String;
26+
public final fun getSloUrlPost ()Ljava/lang/String;
27+
public final fun getSloUrlRedirect ()Ljava/lang/String;
2528
public final fun getSsoUrl ()Ljava/lang/String;
29+
public final fun getSsoUrlPost ()Ljava/lang/String;
30+
public final fun getSsoUrlRedirect ()Ljava/lang/String;
31+
public final fun setEntityId (Ljava/lang/String;)V
32+
public final fun setSigningCredentials (Ljava/util/List;)V
33+
public final fun setSloUrl (Ljava/lang/String;)V
34+
public final fun setSloUrlPost (Ljava/lang/String;)V
35+
public final fun setSloUrlRedirect (Ljava/lang/String;)V
36+
public final fun setSsoUrl (Ljava/lang/String;)V
37+
public final fun setSsoUrlPost (Ljava/lang/String;)V
38+
public final fun setSsoUrlRedirect (Ljava/lang/String;)V
2639
}
2740

2841
public final class io/ktor/server/auth/saml/InMemorySamlReplayCache : io/ktor/server/auth/saml/SamlReplayCache {
@@ -173,6 +186,7 @@ public final class io/ktor/server/auth/saml/SamlCrypto {
173186
}
174187

175188
public final class io/ktor/server/auth/saml/SamlMetadataKt {
189+
public static final fun IdPMetadata (Lkotlin/jvm/functions/Function1;)Lio/ktor/server/auth/saml/IdPMetadata;
176190
public static final fun SamlSpMetadata (Lkotlin/jvm/functions/Function1;)Lio/ktor/server/auth/saml/SamlSpMetadata;
177191
public static final fun parseSamlIdpMetadata (Ljava/lang/String;Z)Lio/ktor/server/auth/saml/IdPMetadata;
178192
public static synthetic fun parseSamlIdpMetadata$default (Ljava/lang/String;ZILjava/lang/Object;)Lio/ktor/server/auth/saml/IdPMetadata;

ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlMetadata.kt

Lines changed: 101 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
320394
private 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

385456
private 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

Comments
 (0)