Skip to content

Server SAML Auth. Logout support.#5417

Open
zibet27 wants to merge 5 commits intozibet27/server-auth-saml-flowfrom
zibet27/server-auth-saml-logout
Open

Server SAML Auth. Logout support.#5417
zibet27 wants to merge 5 commits intozibet27/server-auth-saml-flowfrom
zibet27/server-auth-saml-logout

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
This PR adds SAML 2.0 Single Logout support to the new server auth SAML plugin.
Logout features:

  • SP-initiated logout via call.samlLogout() – builds a signed LogoutRequest and redirects to IdP
  • IdP-initiated logout handling – validates incoming LogoutRequests and sends LogoutResponses
  • Session index tracking for proper logout correlation
  • InResponseTo validation for logout responses

@zibet27 zibet27 requested review from bjhham, e5l and osipxd March 3, 2026 12:28
@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: dfba35df-c1ac-404f-a323-7d160ad1da55

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

Adds SAML Single Logout (SLO) support to the Ktor SAML authentication plugin through new public APIs, internal processors for handling logout requests and responses, and integration into the authentication provider. Includes SP-initiated and IdP-initiated logout flows with session management, signature verification, and replay protection.

Changes

Cohort / File(s) Summary
Public API
ktor-server-auth-saml.api, SamlAuth.kt
Added two overloads of samlLogout() function accepting either SamlPrincipal or explicit nameId parameters, with support for signature algorithms and relay state; corresponding synthetic default wrappers added to API surface.
Logout Processing
SamlLogoutBuilder.kt, SamlLogoutProcessor.kt
Introduced internal logout flow builders for constructing SAML LogoutRequest and LogoutResponse redirects with optional signing, and a processor for validating and parsing incoming logout requests/responses with issuer validation, IssueInstant freshness checks, and replay protection.
Authentication Provider Integration
SamlAuthenticationProvider.kt
Extended authentication provider with SLO endpoint handling, IdP logout request processing, LogoutResponse validation, session clearing, and HTTP binding detection; added lazy initialization of logout processor guarded by enableSingleLogout flag.
SAML Utilities
SamlUtils.kt
Added internal extension function decodeSamlMessage() supporting both plain Base64 and deflate-compressed SAML message decoding.
Unit Tests
SamlLogoutTest.kt
Added comprehensive test suite covering logout request/response construction, signing validation, issuer validation, IssueInstant validation, replay protection, and failure scenarios.
Integration Tests
SamlLogoutIntegrationTest.kt
Added end-to-end test suite covering SP-initiated logout, IdP-initiated logout via POST and GET, logout response processing with various statuses, RelayState handling, and disabled SLO behavior.
Test Utilities
TestUtil.kt
Added helper functions for creating test IdP metadata with SLO endpoints, generating logout requests and responses, encoding SAML for POST binding, and constructing signed HTTP-Redirect logout messages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.00% 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 accurately describes the main change: adding logout support to the Server SAML Auth plugin, which is the primary objective of this changeset.
Description check ✅ Passed The description follows the template with all required sections (Subsystem, Motivation, Solution) properly filled out with specific details about SAML logout features and references to the related ticket and PR.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch zibet27/server-auth-saml-logout
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

You can disable the changed files summary in the walkthrough.

Disable the reviews.changed_files_summary setting to disable the changed files summary in the walkthrough.

@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: 5

🧹 Nitpick comments (3)
ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/SamlLogoutTest.kt (1)

164-173: Consider consolidating repeated SamlLogoutProcessor setup in tests.

There are multiple near-identical constructor blocks; reusing createProcessor(...) (or a variant with flags) would reduce drift.

Also applies to: 216-225, 305-314, 361-370, 409-418

🤖 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/test/io/ktor/server/auth/saml/SamlLogoutTest.kt`
around lines 164 - 173, Multiple tests repeatedly instantiate
SamlLogoutProcessor with the same arguments; refactor by adding a helper factory
(e.g., createProcessor or createProcessor(requireSignedLogoutRequest: Boolean =
true, requireSignedLogoutResponse: Boolean = true, requireDestination: Boolean =
true, other overrides...)) in SamlLogoutTest (or a companion/test util) that
returns a configured SamlLogoutProcessor using idpMetadata,
SamlSignatureVerifier(idpMetadata), clockSkew, and InMemorySamlReplayCache;
replace the repeated constructor blocks (the one shown and the ones at the other
ranges) with calls to this helper, passing only overrides when tests need
different flags.
ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/TestUtil.kt (1)

360-372: Deduplicate IdP metadata helper construction.

createTestIdPMetadata and createTestIdPMetadataWithSlo build the same object shape and can be unified to avoid divergence.

Also applies to: 479-491

🤖 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/test/io/ktor/server/auth/saml/TestUtil.kt`
around lines 360 - 372, Two helper functions createTestIdPMetadata and
createTestIdPMetadataWithSlo duplicate constructing the same IdPMetadata;
consolidate them into a single function. Replace both helpers with one
createTestIdPMetadata(entityId: String = "https://idp.example.com", ssoUrl:
String = "https://idp.example.com/sso", sloUrl: String? =
"https://idp.example.com/slo") that calls generateTestCredentials() and returns
IdPMetadata(entityId, ssoUrl, sloUrl, signingCredentials =
listOf(credentials.credential)); then update all call sites that used
createTestIdPMetadataWithSlo to call the unified createTestIdPMetadata (passing
sloUrl as needed).
ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlAuth.kt (1)

103-175: Document exception behavior and session prerequisite in public samlLogout KDoc.

These overloads can fail via require(...)/checkNotNull(...), but notable failure cases are not documented in the public API docs.

💡 Proposed KDoc additions
 /**
  * Initiates SP-initiated SAML Single Logout.
  *
  * `@param` principal The authenticated SAML principal containing NameID and session info
  * `@param` spMetadata The Service Provider metadata containing the entity ID and signing credential
  * `@param` idpSloUrl The IdP's SLO URL
  * `@param` relayState Optional URL to redirect to after logout completes
  * `@param` signatureAlgorithm The signature algorithm to use for signing the LogoutRequest
  * `@return` [SamlRedirectResult] containing the request ID and redirect URL to the IdP
+ * `@throws` IllegalArgumentException if `nameId`, `idpSloUrl`, or `spEntityId` is blank
+ * `@throws` IllegalStateException if no [SamlSession] is available in [ApplicationCall.sessions]
  */

As per coding guidelines "Public API must include KDoc documentation for parameters, return values, and notable exceptions".

🤖 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/SamlAuth.kt`
around lines 103 - 175, The public KDoc for both ApplicationCall.samlLogout
overloads must document the failure cases: add notes that require(...) checks
will throw IllegalArgumentException when spEntityId, nameId, or idpSloUrl are
blank, and that the session lookup via checkNotNull(sessions.get<SamlSession>())
will throw IllegalStateException if no SamlSession is present; also document the
prerequisite that sessions must be installed and an authenticated session (e.g.,
via authenticate()) must exist before calling samlLogout, referencing the
functions ApplicationCall.samlLogout, the use of sessions.get<SamlSession>() and
sessions.set(...), and the SamlSession.logoutRequestId usage so callers know to
expect and handle these exceptions.
🤖 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/SamlAuthenticationProvider.kt`:
- Around line 294-301: The SLO handler currently silently prefers samlRequest
when both samlRequest and samlResponse are present; change the branching in
SamlAuthenticationProvider (the block that inspects samlRequest and samlResponse
and calls handleIdpLogoutRequest or handleLogoutResponse) to explicitly detect
the invalid case where both parameters are present, log an error
(logger.debug/error) and respond with HttpStatusCode.BadRequest (with a clear
message), otherwise keep the existing behavior: call handleIdpLogoutRequest when
only samlRequest is present and handleLogoutResponse when only samlResponse is
present.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlLogoutProcessor.kt`:
- Around line 159-170: processResponse currently decodes and parses the SAML
response without normalizing failures, so wrap the decode/parse/unmarshall
sequence inside the same withValidationException used by processRequest so
malformed Base64/XML throws SamlValidationException; specifically, enclose the
call to samlResponseBase64.decodeSamlMessage(...), the
LibSaml.parserPool.parse(responseXml.toByteArray().inputStream()), and
document.documentElement.unmarshall<LogoutResponse>() within
withValidationException to translate parsing errors into
SamlValidationException.
- Around line 171-173: The current check in SamlLogoutProcessor uses
samlAssert(expectedRequestId == null || inResponseTo == expectedRequestId) which
lets a null expectedRequestId bypass validation; change it to require strict
correlation by asserting inResponseTo == expectedRequestId and fail if
expectedRequestId is null (e.g., samlAssert(expectedRequestId != null &&
inResponseTo == expectedRequestId) with a clear message like "InResponseTo
mismatch or missing expectedRequestId"), ensuring logoutResponse.inResponseTo is
always validated for SP-initiated flows.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlUtils.kt`:
- Around line 95-103: In decodeSamlMessage, the custom Inflater created for
decompression must be explicitly ended and the InflaterInputStream closed to
avoid native buffer leaks; update the function (decodeSamlMessage) to open the
Inflater and InflaterInputStream in a try/finally or Kotlin use{}-style block,
ensure inflaterInputStream.close() and call inflater.end() in the finally (or
after use) so both the stream and the Inflater are properly released when
isDeflated is true.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/SamlLogoutIntegrationTest.kt`:
- Around line 123-159: The test currently posts LogoutResponse without a prior
session-stored logoutRequestId so InResponseTo correlation isn't exercised;
update the test `test LogoutResponse processing with success and failure status`
to first create a session logout request (e.g., call the logout initiation
endpoint or otherwise populate the session with the logoutRequestId used by the
SLO flow) before posting the success and failure SAMLResponses (use the same ID
for the success case to exercise the correlation path and a different/missing ID
for the failure case), then assert the behavior when `InResponseTo` matches the
stored logoutRequestId versus when it does not (locations to change:
`configureSamlAuth`, the test function, and where you build
`successResponseXml`/`failureResponseXml` using
`SamlTestUtils.createLogoutResponse` and `SLO_PATH`).

---

Nitpick comments:
In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlAuth.kt`:
- Around line 103-175: The public KDoc for both ApplicationCall.samlLogout
overloads must document the failure cases: add notes that require(...) checks
will throw IllegalArgumentException when spEntityId, nameId, or idpSloUrl are
blank, and that the session lookup via checkNotNull(sessions.get<SamlSession>())
will throw IllegalStateException if no SamlSession is present; also document the
prerequisite that sessions must be installed and an authenticated session (e.g.,
via authenticate()) must exist before calling samlLogout, referencing the
functions ApplicationCall.samlLogout, the use of sessions.get<SamlSession>() and
sessions.set(...), and the SamlSession.logoutRequestId usage so callers know to
expect and handle these exceptions.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/SamlLogoutTest.kt`:
- Around line 164-173: Multiple tests repeatedly instantiate SamlLogoutProcessor
with the same arguments; refactor by adding a helper factory (e.g.,
createProcessor or createProcessor(requireSignedLogoutRequest: Boolean = true,
requireSignedLogoutResponse: Boolean = true, requireDestination: Boolean = true,
other overrides...)) in SamlLogoutTest (or a companion/test util) that returns a
configured SamlLogoutProcessor using idpMetadata,
SamlSignatureVerifier(idpMetadata), clockSkew, and InMemorySamlReplayCache;
replace the repeated constructor blocks (the one shown and the ones at the other
ranges) with calls to this helper, passing only overrides when tests need
different flags.

In
`@ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/TestUtil.kt`:
- Around line 360-372: Two helper functions createTestIdPMetadata and
createTestIdPMetadataWithSlo duplicate constructing the same IdPMetadata;
consolidate them into a single function. Replace both helpers with one
createTestIdPMetadata(entityId: String = "https://idp.example.com", ssoUrl:
String = "https://idp.example.com/sso", sloUrl: String? =
"https://idp.example.com/slo") that calls generateTestCredentials() and returns
IdPMetadata(entityId, ssoUrl, sloUrl, signingCredentials =
listOf(credentials.credential)); then update all call sites that used
createTestIdPMetadataWithSlo to call the unified createTestIdPMetadata (passing
sloUrl as needed).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

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

📒 Files selected for processing (9)
  • 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/SamlLogoutBuilder.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/src/io/ktor/server/auth/saml/SamlLogoutProcessor.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/SamlLogoutIntegrationTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/SamlLogoutTest.kt
  • ktor-server/ktor-server-plugins/ktor-server-auth-saml/jvm/test/io/ktor/server/auth/saml/TestUtil.kt

@zibet27 zibet27 requested review from jk1 March 3, 2026 12:39
@zibet27 zibet27 force-pushed the zibet27/server-auth-saml-logout branch from 5b9b71e to c57c368 Compare March 6, 2026 14:08

when {
samlRequest != null && samlResponse != null -> {
logger.debug("SLO endpoint failed. Both `SAMLRequest` and `SAMLRequest` are present")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

SAMLRequest and SAMLResponse?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

good catch

idpSloUrl = idpSloUrl,
inResponseTo = logoutRequest.requestId,
statusCodeValue = StatusCode.SUCCESS,
relayState = parameters["RelayState"],
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's usually a good idea to validate every incoming relay state against an allowed list to avoid open redirect attacks. As a framework, Ktor obviously lacks context to implement transparent relay state validation under the hood. Yet it can support optional user-defined validators.

This is also applicable to other flows involving the relay state parameter, e.g. IdP-initiated sign-in.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, we already check the relay state in AuthenticationContext.handleChallenge, but it should be done everywhere.
Atm, we accept optional allowedRelayStateUrls, but I'll improve it and support custom validators.

@zibet27 zibet27 requested a review from jk1 March 18, 2026 10:05
document.documentElement.unmarshall<LogoutResponse>()
}

val inResponseTo = logoutResponse.inResponseTo
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we verify inResponseTo?

}

val destination = logoutRequest.destination
samlAssert(!requireDestination || destination != null) { "LogoutRequest Destination is not present" }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the validation (both for request and response) is complicated and it is easy to miss a field. Please consider extracting it to the utility function and avoid mixing validation and authentication logic

signatureVerifier = signatureVerifier,
requireDestination = config.requireDestination,
requireSignedLogoutRequest = config.requireSignedLogoutRequest,
requireSignedLogoutResponse = config.requireSignedResponse,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you explain how the flag requireSignedResponse will work?

)

// Store the logout request ID in the session for InResponseTo validation
val currentSession = checkNotNull(sessions.get<SamlSession>()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add test with expires session. It looks like it will fail with wrong message and status code

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