question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Second authentication method is ignored

See original GitHub issue

Ktor 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:closed
  • Created 4 years ago
  • Reactions:3
  • Comments:11 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
sysmatcommented, May 2, 2020

I was wondering if I do something wrong using the same idea @VeselyJan92

authenticate("first") {
    authenticate("second") {
        route("/both-and") { ... }
    }
}

even

authenticate("first", "second", ...) {
}

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

1reaction
VeselyJan92commented, Aug 13, 2019

So it looks like you need some provider-combinator, something like and (or all) function that combines several providers into a single one with AND rule while, currently, it is OR.

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.

     /**
     * If one of the providers fails the other one should be tried
     */
    @Test
    fun testMultipleConfigurations()  = withTestApplication {
        application.install(Authentication) {
            basic("first") { validate { c -> if (c.name == "first") UserIdPrincipal(c.name) else null } }
            basic("second") { validate { c -> if (c.name == "second") UserIdPrincipal(c.name) else null } }
        }

        application.routing {
            authenticate("first", "second") {
                route("/both-or") {
                    handle {
                        call.respondText("OK")
                    }
                }
            }
        }

        handleRequest(HttpMethod.Post, "/both-or") {
            addHeader(
                HttpHeaders.Authorization,
                HttpAuthHeader.Single("basic", Base64.getEncoder().encodeToString("first:password".toByteArray())).render()
            )
        }.let { call ->
            assertEquals(HttpStatusCode.OK.value, call.response.status()?.value)
        }

        handleRequest(HttpMethod.Post, "/both-or") {
            addHeader(
                HttpHeaders.Authorization,
                HttpAuthHeader.Single("basic", Base64.getEncoder().encodeToString("second:password".toByteArray())).render()
            )
        }.let { call ->
            assertEquals(HttpStatusCode.OK.value, call.response.status()?.value)
        }
    }

I would also suggest using similar syntax for AND authentication

authenticate("first") {
    authenticate("second") {
        route("/both-and") { ... }
    }
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Problem Solving Login Issues with Two-Factor Authentication
If you have already set up two-factor authentication and cannot access the authentication code on your mobile device, you will need to ask...
Read more >
Ignoring Two-Factor Authentication Is A Simple Mistake
A large number of people and businesses are missing out on a simple, effective online security solution by ignoring two-factor authentication (2FA/MFA).
Read more >
Why do most people ignore two-factor authentication? - Medium
It proposed “deprecating” SMS as a second authentication factor due to this technique's obvious security imperfections.
Read more >
Stop Ignoring Two-Factor Authentication Just Because You're ...
A large number of people and businesses are missing out on a simple, effective online security solution by ignoring two-factor authentication ( ...
Read more >
Two-factor authentication for Apple ID
Two-factor authentication is the default security method for most Apple IDs. Certain Apple services and features, such as Apple Pay and Sign in ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found