Skip to content

Server SAML Auth. SSO authentication flow.#5416

Open
zibet27 wants to merge 4 commits intozibet27/server-auth-saml-corefrom
zibet27/server-auth-saml-flow
Open

Server SAML Auth. SSO authentication flow.#5416
zibet27 wants to merge 4 commits intozibet27/server-auth-saml-corefrom
zibet27/server-auth-saml-flow

Conversation

@zibet27
Copy link
Copy Markdown
Collaborator

@zibet27 zibet27 commented Mar 3, 2026

Subsystem
Server Auth

Motivation
KTOR-601 SAML Support
Chunk of #5392

Solution

  • SP-initiated SSO flow with configurable HTTP-Redirect/HTTP-POST bindings
  • IdP-initiated SSO support
  • AuthnRequest generation with XML signature support
  • SAML Response/Assertion parsing and validation
  • IdP metadata parsing from XML
  • SP metadata generation for sharing with IdPs

Security hardening:

  • XXE protection via secure XML parsing
  • XML Signature Wrapping (XSW) protection
  • Replay attack prevention with pluggable cache (in-memory default)
  • Signature algorithm allowlisting (blocks SHA-1 by default)
  • RelayState URL validation to prevent open redirects
  • Clock skew tolerance for timestamp validation

Configuration DSL:

 install(Authentication) {
      saml("saml-auth") {
          // Service Provider configuration
          sp = SamlSpMetadata {
              spEntityId = "https://myapp.example.com/saml/metadata"
              acsUrl = "https://myapp.example.com/saml/acs"
              signingCredential = SamlCrypto.loadCredential(
                  keystorePath = "/path/to/keystore.jks",
                  keystorePassword = "example_pass",
                  keyAlias = "sp-key",
                  keyPassword = "example_pass"
              )
              encryptionCredential = SamlCrypto.loadCredential(
                  keystorePath = "/path/to/keystore.jks",
                  keystorePassword = "password",
                  keyAlias = "sp-encryption-key",
                  keyPassword = "password"
              )
          }
 
          // Identity Provider metadata
          idp = parseSamlIdpMetadata(idpMetadataXml)
 
          // Validation logic
          validate { credential ->
              val nameId = credential.nameId
              val email = credential.getAttribute("email")
              if (email != null) {
                  SamlPrincipal(credential.assertion)
              } else {
                  null
              }
          }
      }
  }
 
  routing {
      authenticate("saml-auth") {
          get("/profile") {
              val principal = call.principal<SamlPrincipal>()!!
              call.respondText("Hello ${principal.nameId}")
          }
      }
 }

@zibet27 zibet27 requested review from bjhham, e5l and osipxd March 3, 2026 11:31
@zibet27 zibet27 self-assigned this Mar 3, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 3, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6929eba9-557a-465f-91f7-8e87be4da952

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Introduces comprehensive SAML 2.0 authentication plugin for Ktor Server with public APIs for authentication configuration, request/response processing, and session handling. Includes internal components for signature verification, replay protection, and relay state validation.

Changes

Cohort / File(s) Summary
Public API Surface
ktor-server-auth-saml.api
Exposes new public types: SamlAuthKt with extension functions, SamlAuthenticationProvider, SamlSession with kotlinx serialization support, SamlRedirectResult, and SamlValidationException. Establishes the public API contract for SAML authentication integration.
Core SAML Authentication
SamlAuth.kt, SamlAuthenticationProvider.kt
Implements SAML authentication provider with extension functions for registration on AuthenticationConfig. Handles SP-initiated Web Browser SSO with endpoints for AuthnRequest generation, IdP redirection, and SAML response processing. Includes session tracking, relay state validation, and configuration enforcement.
Request Building
SamlRequestBuilder.kt
Implements AuthnRequest construction for both HTTP-Redirect and HTTP-POST bindings. Provides signing capabilities, XML marshalling, Base64 encoding, and auto-submitting HTML form generation for POST binding. Includes request ID generation and signature application via OpenSAML.
Response Validation & Verification
SamlResponseProcessor.kt, SamlSignatureValidator.kt
Orchestrates SAML response validation: XXE-protected XML parsing, assertion decryption, signature verification, and semantic validation (issuer, destination, audience, in-response-to, replay protection). Signature verifier handles cryptographic verification and HTTP-Redirect query string signatures with algorithm allowlisting.
Utilities & Helpers
SamlUtils.kt
Provides marshalling/unmarshalling, SAML message encoding with deflate support, XML object building, redirect result construction, query string signing, and validation helper functions. Standardizes error handling via SamlValidationException.
Test Infrastructure & Suites
TestUtil.kt, RelayStateValidationTest.kt, SAMLPrincipalTest.kt, SAMLRequestBuilderTest.kt, SAMLResponseProcessorTest.kt, SamlAuthTest.kt
Introduces test utilities for credential generation, signing, encryption, and response creation. Comprehensive test coverage for relay state validation, principal handling, request building, response processing, and end-to-end SAML authentication flows including multi-provider scenarios and various binding methods.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • osipxd
  • e5l
  • bjhham
  • nomisRev
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Server SAML Auth. SSO authentication flow.' clearly summarizes the main change: introducing SAML-based Single Sign-On authentication functionality for Ktor Server.
Description check ✅ Passed The description follows the template structure with Subsystem, Motivation (with ticket reference KTOR-601), and Solution sections that comprehensively document the SAML implementation, security features, and provide a configuration DSL example.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch zibet27/server-auth-saml-flow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zibet27
Copy link
Copy Markdown
Collaborator Author

zibet27 commented Mar 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlUtils.kt (1)

87-94: Guard builder lookup/casts with validation errors instead of raw cast failures.

On Line 87-94 and Line 162-168, direct casts from getBuilder(key) can throw opaque cast/null errors; returning SamlValidationException here gives clearer diagnostics.

💡 Suggested refactor
 `@Suppress`("UNCHECKED_CAST")
 internal inline fun <O : SAMLObject> XMLObjectBuilderFactory.build(
     key: QName,
     crossinline configure: O.() -> Unit
 ): O {
-    val builder = getBuilder(key) as SAMLObjectBuilder<O>
+    val builder = samlRequire(getBuilder(key) as? SAMLObjectBuilder<O>) {
+        "No SAMLObjectBuilder found for element: $key"
+    }
     return builder.buildObject().apply(configure)
 }
@@
 `@Suppress`("UNCHECKED_CAST")
 internal inline fun <O : XMLObject> XMLObjectBuilderFactory.buildXmlObject(
     key: QName,
     crossinline configure: O.() -> Unit
 ): O {
-    val builder = getBuilder(key) as org.opensaml.core.xml.XMLObjectBuilder<O>
+    val builder = samlRequire(getBuilder(key) as? org.opensaml.core.xml.XMLObjectBuilder<O>) {
+        "No XMLObjectBuilder found for element: $key"
+    }
     return builder.buildObject(key).apply(configure)
 }

Also applies to: 162-168

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlUtils.kt`
around lines 87 - 94, The XMLObjectBuilderFactory.build extension currently does
an unchecked cast of getBuilder(key) to SAMLObjectBuilder<O> which can throw
ClassCastException or return null; replace the raw cast with a safe lookup and
explicit validation that throws a SamlValidationException with a helpful message
if the builder is missing or of the wrong type: call getBuilder(key), check for
null, verify it is an instance of SAMLObjectBuilder<*>, then cast safely to
SAMLObjectBuilder<O> and proceed to buildObject().apply(configure); apply the
same pattern to the other occurrence (the similar unchecked cast at the second
build site) to ensure failures produce SamlValidationException with context
(including the QName key and expected type).
ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlResponseProcessor.kt (1)

240-243: Add KDoc for public exception class parameters.

SamlValidationException is part of the public API but lacks documentation for its parameters.

📝 Proposed KDoc addition
 /**
  * Exception thrown when SAML validation fails.
+ *
+ * `@param` message A description of the validation failure
+ * `@param` cause The underlying exception that caused the validation failure, if any
  */
 public class SamlValidationException(message: String, cause: Throwable? = null) : Exception(message, cause)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlResponseProcessor.kt`
around lines 240 - 243, SamlValidationException is a public API but its
constructor parameters are undocumented; add KDoc for the class and explicitly
document the constructor parameters `message: String` and `cause: Throwable?`
(describe what message represents and that cause is the optional underlying
exception) directly above the class declaration
`SamlValidationException(message: String, cause: Throwable? = null)` so the
public exception class has proper KDoc for both parameters.
ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlAuthenticationProvider.kt (2)

335-339: Consider making SamlSession a data class.

SamlSession is a simple value holder. Declaring it as a data class would provide equals(), hashCode(), copy(), and componentN() functions, which can be useful for testing and session comparison.

📝 Proposed change
 `@Serializable`
-public class SamlSession(
+public data class SamlSession(
     public val requestId: String,
     public val logoutRequestId: String? = null
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlAuthenticationProvider.kt`
around lines 335 - 339, The SamlSession class is acting as a simple value holder
and should be converted to a data class to get generated
equals/hashCode/copy/componentN methods; change the declaration of SamlSession
to a data class (i.e., replace "class SamlSession" with "data class
SamlSession") while keeping the `@Serializable` annotation and the same primary
constructor properties (requestId and logoutRequestId) so existing usages of
SamlSession continue to work.

274-276: Path prefix matching may reject valid sub-paths.

The condition normalized == prefix || prefix.endsWith("/") means a prefix like /api will reject /api/users because /api doesn't end with /. Users must configure /api/ (with trailing slash) for sub-path matching to work.

Consider documenting this behavior or adjusting the logic to allow path-segment boundaries.

📝 Option A: Document the behavior

Add a note in the configuration DSL documentation that path prefixes must end with / to match sub-paths.

📝 Option B: Allow segment-boundary matching
 pathPrefixes.any { prefix ->
-    normalized.startsWith(prefix) && (normalized == prefix || prefix.endsWith("/"))
+    normalized.startsWith(prefix) && (normalized == prefix || prefix.endsWith("/") || normalized.getOrNull(prefix.length) == '/')
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlAuthenticationProvider.kt`
around lines 274 - 276, The current path prefix check inside pathPrefixes.any
(using variables normalized and prefix) rejects sub-paths unless the configured
prefix ends with '/', so update the condition to allow segment-boundary
matching: accept when normalized == prefix or normalized.startsWith(prefix +
"/") or when prefix already endsWith("/"); modify the lambda in pathPrefixes.any
accordingly (referencing the pathPrefixes.any { prefix -> ... } block) and
add/adjust tests or docs for SamlAuthenticationProvider to cover both
trailing-slash and segment-boundary matches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlSignatureValidator.kt`:
- Around line 111-116: The current logic in SamlSignatureValidator uses
queryString.indexOf("&Signature=") which fails when Signature is the first
parameter; update the removal to handle Signature at any position by locating
"Signature=" (not just "&Signature=") and removing the entire "Signature=...{&
or end}" segment, or better parse the query string into key=value pairs, filter
out the "Signature" key, then rejoin and convert to UTF_8 for
queryStringWithoutSignature; update references to signatureIdx and
queryStringWithoutSignature accordingly so verification works regardless of
parameter order.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlUtils.kt`:
- Around line 210-215: The helper withValidationException currently catches all
Exceptions and always throws a new SamlValidationException("SAML validation
failed", e), which swallows existing SamlValidationException details; change it
so that if the caught exception is already a SamlValidationException it is
rethrown unchanged (preserving its message and context), and only wrap
non-SamlValidationException instances in a new SamlValidationException
(including the original exception as the cause) so existing context is not lost.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/SAMLResponseProcessorTest.kt`:
- Around line 265-270: The test constructs an assertion with
SamlTestUtils.createTestAssertion where notBefore is set to the future
(Clock.System.now() + 10.minutes) to trigger the "not yet valid" condition, but
notOnOrAfter is incorrectly set to the past (Clock.System.now() - 20.minutes)
which could make the assertion expire instead; update the assertion parameters
so notOnOrAfter is after notBefore (e.g., set notOnOrAfter to Clock.System.now()
+ 30.minutes or any time after notBefore) to isolate the not-before check and
ensure the test fails only for "not yet valid" as intended.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/TestUtil.kt`:
- Around line 104-106: The signAssertion function uses OpenSAML builders but
does not call LibSaml.ensureInitialized(); to fix, add a call to
LibSaml.ensureInitialized() at the start of signAssertion (mirroring other
helpers) so the XMLObjectProviderRegistrySupport builder factory is initialized
before using it; update the signAssertion function to invoke
LibSaml.ensureInitialized() prior to obtaining the builderFactory and proceeding
with signature setup.

---

Nitpick comments:
In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlAuthenticationProvider.kt`:
- Around line 335-339: The SamlSession class is acting as a simple value holder
and should be converted to a data class to get generated
equals/hashCode/copy/componentN methods; change the declaration of SamlSession
to a data class (i.e., replace "class SamlSession" with "data class
SamlSession") while keeping the `@Serializable` annotation and the same primary
constructor properties (requestId and logoutRequestId) so existing usages of
SamlSession continue to work.
- Around line 274-276: The current path prefix check inside pathPrefixes.any
(using variables normalized and prefix) rejects sub-paths unless the configured
prefix ends with '/', so update the condition to allow segment-boundary
matching: accept when normalized == prefix or normalized.startsWith(prefix +
"/") or when prefix already endsWith("/"); modify the lambda in pathPrefixes.any
accordingly (referencing the pathPrefixes.any { prefix -> ... } block) and
add/adjust tests or docs for SamlAuthenticationProvider to cover both
trailing-slash and segment-boundary matches.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlResponseProcessor.kt`:
- Around line 240-243: SamlValidationException is a public API but its
constructor parameters are undocumented; add KDoc for the class and explicitly
document the constructor parameters `message: String` and `cause: Throwable?`
(describe what message represents and that cause is the optional underlying
exception) directly above the class declaration
`SamlValidationException(message: String, cause: Throwable? = null)` so the
public exception class has proper KDoc for both parameters.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlUtils.kt`:
- Around line 87-94: The XMLObjectBuilderFactory.build extension currently does
an unchecked cast of getBuilder(key) to SAMLObjectBuilder<O> which can throw
ClassCastException or return null; replace the raw cast with a safe lookup and
explicit validation that throws a SamlValidationException with a helpful message
if the builder is missing or of the wrong type: call getBuilder(key), check for
null, verify it is an instance of SAMLObjectBuilder<*>, then cast safely to
SAMLObjectBuilder<O> and proceed to buildObject().apply(configure); apply the
same pattern to the other occurrence (the similar unchecked cast at the second
build site) to ensure failures produce SamlValidationException with context
(including the QName key and expected type).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90866c0 and 72cb583.

📒 Files selected for processing (13)
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/api/ktor-server-auth-saml.api
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlAuth.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlAuthenticationProvider.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlRequestBuilder.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlResponseProcessor.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlSignatureValidator.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlUtils.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/RelayStateValidationTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/SAMLPrincipalTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/SAMLRequestBuilderTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/SAMLResponseProcessorTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/SamlAuthTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/TestUtil.kt

@zibet27 zibet27 requested a review from jk1 March 4, 2026 09:17
@zibet27 zibet27 force-pushed the zibet27/server-auth-saml-core branch from c69399e to 13a32a2 Compare March 10, 2026 12:24
Copy link
Copy Markdown
Member

@e5l e5l left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add integration tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants