Second authentication method is ignored
See original GitHub issueKtor Version
1.2.1
Ktor Engine Used (client or server and name)
Netty
JVM Version, Operating System and Relevant Context
JDK 8, Mac
Issue
I’m building a multi-layer custom authentication for my web service. I declare it like this:
// in Application.module()
install(Authentication) {
appId(AUTH_APP_ID) { // this one is my custom one
validate {
if (AuthorizedApps.isReadAuthorized(it.appId)) // simplified logic
AppIdPrincipal(AuthorizedApps.getAppName(it.appId)!!)
else null
}
}
}
And routes look like this:
routing {
authenticate(AUTH_APP_ID) {
get("/info", controller<ServiceInformationController>()) // controller handles everything
}
}
So… this works as expected. It was actually much easier than anticipated.
THE PROBLEM
Now I want to add another custom authentication, for example “AppSecret”. Same as ID, just handles a different header (app_secret
) along with this app_id
header.
I won’t paste the whole thing here (see the bottom for more info), but it’s pretty much the same… except for this part:
authenticate(AUTH_APP_ID, AUTH_APP_SECRET) { // observe: two authentication methods
get("/info", controller<ServiceInformationController>())
}
I also tried the other approach, with nested authenticate
blocks.
In both cases, the second authentication method is ignored.
The truth is… yes, right now, I only need the one (secret), but let’s say in the future I want to add another one, that I want to combine with the others… for example JWT or something on top of AppID/AppSecret combo.
Why is my second authentication method completely ignored? I debugged this, Ktor never even tries after the first auth method is successful.
Here’s my custom authentication for reference (AppID):
package tokenizer.v1.security
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.auth.Authentication
import io.ktor.auth.AuthenticationFunction
import io.ktor.auth.AuthenticationPipeline
import io.ktor.auth.AuthenticationProvider
import io.ktor.auth.Credential
import io.ktor.auth.Principal
import io.ktor.http.HttpStatusCode
import io.ktor.request.ApplicationRequest
import io.ktor.request.header
import io.ktor.response.respond
import tokenizer.v1.Configuration.Headers
import tokenizer.v1.security.AppIdAuthenticationProvider.Configuration
data class AppIdCredential(val appId: String) : Credential
data class AppIdPrincipal(val appName: String) : Principal
class AppIdAuthenticationProvider(configuration: Configuration) : AuthenticationProvider(configuration) {
class Configuration(name: String?) : AuthenticationProvider.Configuration(name) {
// provides a validation function that will check given `AppIdCredential` instance
// and return `Principal` (or `null` if credential does not correspond to an authenticated principal)
var authenticationFunction: AuthenticationFunction<AppIdCredential> = { null }
fun validate(body: suspend ApplicationCall.(AppIdCredential) -> Principal?) {
authenticationFunction = body
}
}
val authenticationFunction = configuration.authenticationFunction
}
// installs an app ID authentication mechanism
fun Authentication.Configuration.appId(name: String? = null, configure: Configuration.() -> Unit) {
val provider = AppIdAuthenticationProvider(Configuration(name).apply(configure))
val authenticateFunction = provider.authenticationFunction
provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
val credential = call.request.appIdAuthenticationCredential()
val principal = credential?.let { authenticateFunction(call, it) }
val cause = when {
credential == null -> call.respond(HttpStatusCode.Forbidden)
principal == null -> call.respond(HttpStatusCode.Unauthorized)
else -> null
}
if (cause != null) {
// maybe respond with something? probably not...
}
principal?.let { context.principal(it) }
}
register(provider)
}
// retrieves App ID Credential for this `ApplicationRequest`
@Suppress("MoveVariableDeclarationIntoWhen")
fun ApplicationRequest.appIdAuthenticationCredential(): AppIdCredential? {
val appId = header(Headers.APP_ID)
// we need a valid app ID
if (appId.isNullOrBlank()) return null
// we have "something", so let's use this
return AppIdCredential(appId)
}
Issue Analytics
- State:
- Created 4 years ago
- Reactions:3
- Comments:11 (5 by maintainers)
Top GitHub Comments
I was wondering if I do something wrong using the same idea @VeselyJan92
even
for me would be natural it should pass all authentications in a list
So this is not possible?
Common scenario in REST API (Like RolesAllowed), you have top level route(auth) and for some paths is ok, but some path let say DELETE you do some extra authz
If the configurations behaves like OR why this test fails with
expected:<200> but was:<401>
for the second request? It works for different kinds of providers. Is there any rule that providers must be of different kinds.I would also suggest using similar syntax for AND authentication