Fix multiple jwt auth providers processing (#1586)

This commit is contained in:
Sergey Mashkov
2020-01-29 16:39:34 +03:00
parent b37f2029b7
commit 86fb0294aa
3 changed files with 84 additions and 27 deletions

View File

@@ -30,6 +30,8 @@ public final class io/ktor/auth/AuthenticationContext {
public fun <init> (Lio/ktor/application/ApplicationCall;)V
public final fun challenge (Ljava/lang/Object;Lio/ktor/auth/AuthenticationFailedCause;Lkotlin/jvm/functions/Function3;)V
public final fun error (Ljava/lang/Object;Lio/ktor/auth/AuthenticationFailedCause;)V
public final fun getAllErrors ()Ljava/util/List;
public final fun getAllFailures ()Ljava/util/List;
public final fun getCall ()Lio/ktor/application/ApplicationCall;
public final fun getChallenge ()Lio/ktor/auth/AuthenticationProcedureChallenge;
public final fun getErrors ()Ljava/util/HashMap;

View File

@@ -10,12 +10,12 @@ import com.auth0.jwt.algorithms.*
import com.nhaarman.mockito_kotlin.*
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.auth.Principal
import io.ktor.http.*
import io.ktor.http.auth.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.testing.*
import org.junit.Test
import java.security.*
import java.security.interfaces.*
import java.util.concurrent.*
@@ -72,6 +72,68 @@ class JWTAuthTest {
}
}
@Test
fun testJwtWithMultipleConfigurations() {
val validated = mutableSetOf<String>()
var currentPrincipal: (JWTCredential) -> Principal? = { null }
withApplication {
application.install(Authentication) {
jwt(name = "first") {
realm = "realm1"
verifier(makeJwtVerifier())
validate { validated.add("1"); currentPrincipal(it) }
challenge { _, _ ->
call.respond(UnauthorizedResponse(HttpAuthHeader.basicAuthChallenge("custom1", Charsets.UTF_8)))
}
}
jwt(name = "second") {
realm = "realm2"
verifier(makeJwtVerifier())
validate { validated.add("2"); currentPrincipal(it) }
challenge { _, _ ->
call.respond(UnauthorizedResponse(HttpAuthHeader.basicAuthChallenge("custom2", Charsets.UTF_8)))
}
}
}
application.routing {
authenticate("first", "second") {
get("/") {
val principal = call.authentication.principal<JWTPrincipal>()!!
call.respondText("Secret info, ${principal.payload.audience}")
}
}
}
val token = getToken()
handleRequestWithToken(token).let { call ->
verifyResponseUnauthorized(call)
assertEquals(
"Basic realm=custom1, charset=UTF-8",
call.response.headers[HttpHeaders.WWWAuthenticate]
)
}
assertEquals(setOf("1", "2"), validated)
currentPrincipal = { JWTPrincipal(it.payload) }
validated.clear()
handleRequestWithToken(token).let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals(
"Secret info, [$audience]",
call.response.content
)
assertNull(call.response.headers[HttpHeaders.WWWAuthenticate])
}
assertEquals(setOf("1"), validated)
}
}
@Test
fun testJwtSuccess() {
withApplication {

View File

@@ -131,14 +131,17 @@ class Authentication(config: Configuration) {
}
context.challenge.completed -> finish()
else -> {
if (!optional) {
executeChallenges(context, true)
if (!optional || context.hasInvalidCredentials()) {
executeChallenges(context)
}
}
}
}
}
private fun AuthenticationContext.hasInvalidCredentials(): Boolean =
allFailures.any { it == AuthenticationFailedCause.InvalidCredentials }
/**
* Installable feature for [Authentication].
*/
@@ -174,19 +177,11 @@ class Authentication(config: Configuration) {
finish()
return@intercept
}
if (context.challenge.register.all { it.first === AuthenticationFailedCause.NoCredentials }) {
return@intercept
}
// NOTE: we don't handle errors per-provider here, we do it in the end
executeChallenges(context, false)
}
}
private suspend fun PipelineContext<*, ApplicationCall>.executeChallenges(
context: AuthenticationContext,
handleErrors: Boolean
context: AuthenticationContext
) {
val challengePipeline = Pipeline<AuthenticationProcedureChallenge, ApplicationCall>(ChallengePhase)
val challenges = context.challenge.challenges
@@ -199,23 +194,21 @@ class Authentication(config: Configuration) {
}
}
if (handleErrors) {
for (challenge in context.challenge.errorChallenges) {
challengePipeline.intercept(ChallengePhase) {
challenge(it)
if (it.completed)
finish() // finish challenge pipeline if it has been completed
}
for (challenge in context.challenge.errorChallenges) {
challengePipeline.intercept(ChallengePhase) {
challenge(it)
if (it.completed)
finish() // finish challenge pipeline if it has been completed
}
}
for (error in context.errors.values.filterIsInstance<AuthenticationFailedCause.Error>()) {
challengePipeline.intercept(ChallengePhase) {
if (!it.completed) {
logger.trace("Responding unauthorized because of error ${error.cause}")
call.respond(UnauthorizedResponse())
it.complete()
finish()
}
for (error in context.allErrors) {
challengePipeline.intercept(ChallengePhase) {
if (!it.completed) {
logger.trace("Responding unauthorized because of error ${error.message}")
call.respond(UnauthorizedResponse())
it.complete()
finish()
}
}
}