I have an API made with Ktor, I'm trying to implement authorization by roles as follows:
internal class RoleBaseConfiguration(
var requiredRoles: Set<String> = emptySet()
)
internal val globalconfig = RoleBaseConfiguration()
internal val RoleAuthorizationPlugin = createApplicationPlugin(
name = "RoleAuthorizationPlugin",
createConfiguration = ::RoleBaseConfiguration
){
pluginConfig.apply{
on(AuthenticationChecked){ call ->
val jwtToken = call.request.headers["Authorization"]?.toJWT() ?: throw
Exception("Missing principal")
val roles =
JWTConfig.verifier.verify(jwtToken).getClaim("role")
.toString().roleToSet()
println("${globalconfig.requiredRoles} - $roles")
if(roles.intersect(globalconfig.requiredRoles).isEmpty()){
call.respondText("You don`t have access to this resource.", status =
HttpStatusCode.Unauthorized)
}
}
}
}
fun Route.withRole(role: String, build: Route.() -> Unit): Route {
return withRoles(role, build = build)
}
fun Route.withRoles(vararg roles: String, build: Route.() -> Unit): Route {
val authenticatedRoute = createChild(AuthorizationRouteSelector)
/*authenticatedRoute.install(RoleAuthorizationPlugin) {
this.requiredRoles = roles.toSet()
}*/
globalconfig.requiredRoles = roles.toSet()
authenticatedRoute.build()
return authenticatedRoute
}
object AuthorizationRouteSelector : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int):
RouteSelectorEvaluation {
return RouteSelectorEvaluation.Transparent
}
override fun toString(): String = "(authorize \"default\" )"
}
fun String.roleToSet(): Set<String>{
return split(",").map{ it.trim().replace("\"", "") }.toSet()
}
fun String.toJWT() = removePrefix("Bearer ")
Then in the routes file, I call the withRole and withRoles methods as follows:
route("/auth") {
authenticate {
withRoles("user", "admin){
get("register"){
//Implementar el registro
}
}
}
withRole("admin"){
get("test"){
call.respond("Test")
}
}
}
When I make a request to /auth/register with a user type user, I get 401 and println prints "[admin] - [user]" from what I understand that only the roles of the last call to withRole or withRoles are saved .
I've been researching, but I can't find the solution to make it work correctly. I thought about using createRouteScopePlugin(...) instead of createApplicationPlugin(...) but it turns out that if I use the first option, I can only call withRole or withRoles once and if I call it more than once throwing an exception saying that the plugin is already installed.
I see two problems in your solution. First, you use a global config that is shared among routes. Second, the AuthenticationChecked
hook is executed only on a route level, but your plugin is application-scoped.
I recommend storing required roles in the plugin's configuration and installing the plugin for each authorization scope.
fun Route.withRoles(vararg roles: String, build: Route.() -> Unit) {
val route = createChild(object : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
return RouteSelectorEvaluation.Transparent
}
})
route.install(RoleAuthorizationPlugin) {
roles(roles.toSet())
}
route.build()
}
class RoleBaseConfiguration {
val requiredRoles = mutableSetOf<String>()
fun roles(roles: Set<String>) {
requiredRoles.addAll(roles)
}
}
val RoleAuthorizationPlugin = createRouteScopedPlugin("RoleAuthorizationPlugin", ::RoleBaseConfiguration) {
on(AuthenticationChecked) { call ->
val principal = call.principal<JWTPrincipal>() ?: return@on
val roles = principal.payload.getClaim("role").asList(String::class.java).toSet()
if (pluginConfig.requiredRoles.isNotEmpty() && roles.intersect(pluginConfig.requiredRoles).isEmpty()) {
call.respondText("You don`t have access to this resource.", status = HttpStatusCode.Unauthorized)
}
}
}
Here is an example of the plugin usage in the routing:
routing {
authenticate("auth-jwt") {
withRoles("admin") {
get("/admin") {
call.respondText { "For admin only" }
}
}
withRoles("registered") {
get("/reg") {
call.respondText { "For registered users" }
}
}
withRoles {
get("/anyone") {
call.respondText { "For anyone" }
}
}
}
}