echoes
echoes

Reputation: 63

Ktor: How to check authentication inside a route handler?

I'm using ktor v0.9.2 and I want to send diferent content for the same route, based if the user is authenticated or not.

The problem I'm having is that I'm not being able to access the principal outside the authenticate { } block.

My setup is like this:

data class User(
    val userId: Int
) : io.ktor.auth.Principal

fun Application.myApp() {
    install(Authentication) {
        jwt {
            verifier(JwtConfig.verifier)
            validate { credential ->
                val userId = credential.payload.getClaim("userId").asInt()
                when {
                    userId > 0 -> User(userId)
                    else -> null
                }
            }
        }
    }
    install(DefaultHeaders)
    install(CallLogging)
    install(ContentNegotiation) {
        jackson { }
    }
    install(Routing) {
        authenticate {
            get("/protected") {
                // This works fine!!
                val user = call.authentication.principal<User>()
                call.respond(user)
            }
        }

        get("/") {
            val user = call.authentication.principal<User>() // -> User is always null here
            if (user == null) {
                call.respondText("Not Logged User")
            } else {
                call.respondText("Logged User")
            }
        }
    }
}

The /protected route works fine, but in the / route the principal is always null. I thinks this is some pipeline thing, but I am a little lost. Could someone give some insights? Thanks!

Upvotes: 4

Views: 5672

Answers (1)

avolkmann
avolkmann

Reputation: 3105

What version of ktor are you using? Can you show us your auth setup?

You should have something like this (0.9.2):

install(Authentication) {
        jwt {
            verifier(JwtConfig.verifier)
            realm = JwtConfig.realm
            validate {
                val email = it.payload.getClaim("email").toString()
                userRepository.findUser(email)?.let { user ->
                    val token = JwtConfig.makeToken(user)
                    user.copy(token = token)
                }
            }
        }

}

If the auth process is successful, the user will be available via principal.

Here is the updated code for 0.9.3. Starting off with a test to verify behavior. Simply add the optional flag.

class KtorTest {

    @Test fun server() {
        withTestApplication({ myApp() }) {
            val userId = 918354853
            val token = JwtConfig.makeToken(User(userId))
            // The protected route
            handleRequest {
                uri = "/protected"
                addHeader("Authorization", "Bearer $token")
            }.let {
                it.requestHandled shouldBe true
                it.response.content.shouldNotBeNullOrBlank() shouldContain userId.toString()
            }

            // Optional route without token
            handleRequest {}.let {
                it.requestHandled shouldBe true
                it.response.content.shouldNotBeNullOrBlank() shouldBeEqualTo "Not Logged User"
            }

            // Optional route with token
            handleRequest {
                addHeader("Authorization", "Bearer $token")
            }.let {
                it.requestHandled shouldBe true
                it.response.content.shouldNotBeNullOrBlank() shouldBeEqualTo "Logged User"
            }
        }
    }

}

data class User(val userId: Int) : Principal

fun Application.myApp() {
    install(Authentication) {
        jwt {
            verifier(JwtConfig.verifier)
            validate { credential ->
                val userId = credential.payload.getClaim("userId").asInt()
                when {
                    userId > 0 -> User(userId)
                    else -> null
                }
            }
        }
    }
    install(DefaultHeaders)
    install(CallLogging)
    install(ContentNegotiation) { jackson { } }
    install(Routing) {
        authenticate {
            get("/protected") {
                // This works fine!!
                val user = call.authentication.principal<User>()!!
                call.respond(user)
            }
        }

        authenticate(optional = true) {
            get("/") {
                val user = call.authentication.principal<User>() // -> User is always null here
                if (user == null) {
                    call.respondText("Not Logged User")
                } else {
                    call.respondText("Logged User")
                }
            }
        }
    }
}

Upvotes: 5

Related Questions