mirror of
https://github.com/jlengrand/ktor.git
synced 2026-03-10 08:31:20 +00:00
Fix multiple jwt auth providers processing (#1586)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user