ktor icon indicating copy to clipboard operation
ktor copied to clipboard

SAML Support

Open weickmanna opened this issue 5 years ago • 3 comments

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

weickmanna avatar Jun 28 '19 15:06 weickmanna

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 avatar Jul 22 '19 08:07 vngantk

@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

weickmanna avatar Aug 06 '19 10:08 weickmanna

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

oleg-larshin avatar Aug 10 '20 15:08 oleg-larshin