Hi, I’m implementing a service providing endpoints...
# ktor
c
Hi, I’m implementing a service providing endpoints with and without authentication. After repeating the same procedures for some endpoints I wonder if there is a better way in KTor providing repeating context information for services, let me try to give an abstract example:
Copy code
routing {
  get("/test") {
    call.respondText("test called")
  }
  authenticate("auth-jwt") {
    route("/featureA") {
      get("/") {
        val principalSubject = call.principal<JWTPrincipal>()?.subject
        featureAService.handleGet(call, principalSubject)
      }
      post("/") {
        val principalSubject = call.principal<JWTPrincipal>()?.subject
        featureAService.handlePost(call, principalSubject)
      }
      put("/") {
        val principalSubject = call.principal<JWTPrincipal>()?.subject
        featureAService.handlePut(call, principalSubject)
      }
    }
    route("/featureB") {
      get("/") {
        val principalSubject = call.principal<JWTPrincipal>()?.subject
        featureBService.handleGet(call, principalSubject)
      } 
    }
  }  
}
In this case I’d like to provide the principalSubject to each method (in reality there are a few more context parameters that are shared across those methods like parameters etc.). I don’t want to start each method with retrieving unsers and other context parameters and I also don’t want to repeat those lines in the
routing
section. Is there any way to reduce the amount of code here by having some preprocessor (could then use
context-receivers
)?
a
you can create a function that returns a lambda that does the repeating tasks
so, something like this:
Copy code
fun myWrapper(
    handler: suspend PipelineContext<Unit, ApplicationCall>.(
        principalSubject: String?
    ) -> Unit
): PipelineInterceptor<Unit, ApplicationCall> {
    return {
        val principalSubject = call.principal<JWTPrincipal>()?.subject
        this.handler(principalSubject)
    }
}
you can invoke it like this:
Copy code
get("/foo", myWrapper { principal -> 
    featureAService.handleFoo(call, principal)
})
r
You can also create a simple plugin that will extract auth data and add it to the attributes of the call
c
Thanks for the solutions. I tried the first one but that will end in repeating the same thing over and over again (in fact I use a class for holding my context values). So I’ll check out how to add it to the attributes of the call.
r
it should look something like this
Copy code
val plugin = createRouteScopedPlugin("myPlugin") {

  on(AuthenticationChecked) { call ->
    val data = call.principal.extractData() 
    call.attributes.put(MyKey, data)
  }
}
and you also can create an extension to make it easier on calling side like
Copy code
val ApplicationCall.myAuthData
  get() = attributes.get(MyKey)
s
I write small typed middleware functions for this, https://github.com/nomisRev/ktor-arrow-example/blob/main/src/main/kotlin/io/github/nomisrev/auth/jwt.kt And then you can wrap your routes with
service.auth { token -> }
, and you can easily switch out implementations in testing as well.
a
for this particular case you could even just write an extension function on call that calls
call.principal<JWTPrincipal>()?.subject
(call-ception)
s
This still requires you to deal with nullability everywhere 😭
a
I’d probably throw in a
!!
for that one
I don’t mind a 500 if i try to ask for auth stuff outside of authenticated routes
s
It definitely works. I don't use
!!
anywhere, the errors it throws are much too vague to debug on Grafana. Additionally, I found it confusing to distinct in this way between required -and optional authentication. Given the small size in which you can wrap Ktor to provide a more meaningful domain DSL is totally worth it for me.
I also had a need for
suspend
while verifying the JWT tokens, which is not supported in the default authentication validator IIRC.
c
I’d like to go with the approach of @Rustam Siniukov, but somehow the
authentication
/
principal
is null. I installed the plugin in my module section within the
embeddedServer
. But when calling
Copy code
call.principal<JWTPrincipal>()
is null (and there is no
principal.extractData()
function). When I receive the call within my
routing
configuration it is set properly and contains a
JWTPrincipal
.
Okay, my fault. I did not install the plugin within the routeScope but on application level. After installing it within routeScope as the name says it works like a charm. Thanks all for support and the great ideas.
K 2
👌 2