ktor
ktor copied to clipboard
SAML Support
Motivation
Enterprises often have SAML-based SSO solutions in place. Web applications built on top of ktor should be able to integrate with this.
Problem
I managed to integrate SAML into a ktor application, based on https://github.com/onelogin/java-saml. However, I had to use internal API and reflection to make that happen.
java-saml only works based on a Servlet stack. Specifically, HttpServletRequest and HttpServletResponse are used. To facilitate this, I use the ktor Jetty deployment option. The problem is, that the underlying request and response objects are not accessible via public API, so I had to do this:
suspend fun PipelineContext<Unit, ApplicationCall>.requireSAMLEnabled(handler: suspend () -> Unit) {
if (!AppConfig.ssoSaml) call.respond(HttpStatusCode.BadRequest) else handler()
}
suspend fun PipelineContext<Unit, ApplicationCall>.withSAMLAuth(handler: suspend (Auth) -> Unit) {
val auth = Auth(SSOConfig.saml2Settings, call.getServletRequest(), call.getServletResponse())
handler(auth)
}
@UseExperimental(EngineAPI::class)
fun ApplicationCall.getServletRequest(): HttpServletRequest {
val servletRequest = getAsyncServletApplicationCall().request.servletRequest
// when running behind proxy with ssl offloading, the request must be customized to use the original scheme
// Jetty request customizers won't be executed by ktor so we have to do it manually here
(servletRequest as Request).scheme = request.origin.scheme
return servletRequest
}
@UseExperimental(EngineAPI::class)
fun ApplicationCall.getServletResponse(): HttpServletResponse {
val servletApplicationResponse = getAsyncServletApplicationCall().response
val responseField = ServletApplicationResponse::class.java.getDeclaredField("servletResponse")
responseField.isAccessible = true
return responseField.get(servletApplicationResponse) as HttpServletResponse
}
@UseExperimental(EngineAPI::class)
private fun ApplicationCall.getAsyncServletApplicationCall(): AsyncServletApplicationCall {
val routingApplicationCall = (request.call as RoutingApplicationCall)
val callField = RoutingApplicationCall::class.java.getDeclaredField("call")
callField.isAccessible = true
return callField.get(routingApplicationCall) as AsyncServletApplicationCall
}
Suggestion 1 - Fast, easy and ugly way to ease immediate pain
Expose servletRequest and servletResponse objects via public API so they can be passed to the java-saml library.
Suggestion 2 - Support SAML out of the box
Provide an official SAML feature that does all the work. Could be built on top of https://github.com/onelogin/java-saml/tree/master/core
It is possible to implement a SAML authentication provider without depending on the servlet framework. You can use opensaml, which is a low level API and does not require the use of the servlet framework. I have actually implemented one for my company. I have to check to see if I can share it here.
@vngantk opensaml has a big end of life disclaimer on their page. I'd like to use components that at least pretend to be still alive ;-)
Extracted the reflection hack into a reusable library, for anyone else wanting to go down this path: https://github.com/link-time/ktor-onelogin-saml
Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.