In this tutorial, we will learn to create a custom Role-based authorization feature in Ktor. Till now it is not readily available in the Ktor framework. Recently Kotlin is gaining popularity and in effect to write fully asynchronous connected web server and client applications in Kotlin, Ktor web framework comes in the picture.

Using the Ktor framework, you can write your server or client application. In the application, there could be many APIs defined to serve different features. Given the nature of the features, few features need to be secured (with restricted use) and rest can be available to all. There are many ways to secure the API resources. One way is to allow access to only a limited category of users. For example, user management or application management related resources should be only accessible by Admin users. In the Jersey web framework, to secure such resources can be annotated with keyword RolesAllowed and pass the allowed user type values to it. But in Ktor it is not readily available, so on similar lines, we are going to develop our own custom RolesAllowed feature.

First, we will define the Roles to start with -

1 enum class Role(val roleStr: String) { 2 ADMIN("admin"), 3 USER("user") 4 }

Now we will create RoleAuthorization feature:

In Ktor, we need to provide custom configuration to feature which is our custom logic which authorizes the given role.

class RoleBasedAuthorizer { internal var authorizationFunction: suspend ApplicationCall.(Set<Role>)->Either<String, Unit> = { Unit.right() } fun validate(body: suspend ApplicationCall.(Set<Role>)->Either<String, Unit>) { authorizationFunction = body } }

Here RoleBasedAuthorizer is a provider class that has validate function which takes another function i.e. authorization function as arguments. Authorization function takes allowed roles as a parameter and applies given custom logic and returns Either: Unit on success and String on error.

Next, we need to configure the above provider for the feature. So below code is copying provider in the Configuration. Here RoleAuthorization is the main feature class that configures as per Ktor feature specs.

class RoleAuthorization internal constructor(config: Configuration) { val log = LoggerFactory.getLogger(RoleAuthorization::class.java) constructor(provider: RoleBasedAuthorizer): this(Configuration(provider)) private var config = config.copy() class Configuration internal constructor(provider: RoleBasedAuthorizer) { var provider = provider internal fun copy(): Configuration = Configuration(provider) } // class RoleBasedAuthorizer... // fun interceptPipeline... // Feature... }

Now, we will configure the actual feature:

companion object Feature : ApplicationFeature<ApplicationCallPipeline, RoleBasedAuthorizer, RoleAuthorization> { private val authorizationPhase = PipelinePhase("authorization") override val key: AttributeKey<RoleAuthorization> = AttributeKey("RoleAuthorization") @io.ktor.util.KtorExperimentalAPI override fun install( pipeline: ApplicationCallPipeline, configure: RoleBasedAuthorizer.() -> Unit ): RoleAuthorization { val configuration = RoleBasedAuthorizer().apply { configure } val feature = RoleAuthorization(configuration) return feature } }

Here, we are defining the authorization phase. Basically, it identifies this feature in the Ktor application pipeline when gets installed. It takes the provider configuration and installs it in the pipeline and returns the installed feature’s instance.

As we have configured the feature, let’s define when to call it and what to do when it gets invoked.

fun interceptPipeline(pipeline: ApplicationCallPipeline, roles: Set<Role>) { pipeline.insertPhaseAfter(ApplicationCallPipeline.Features, authorizationPhase) pipeline.intercept(authorizationPhase) { val call = call config.provider.authorizationFunction(call, roles).fold( { log.debug("Responding unauthorized because of error", it ) call.respond(HttpStatusCode.Forbidden,"Permission is denied") finish() } , { return@intercept } ) } }

Here, it defines when to call it that is what insertPhaseAfter doing here. It is installing our feature at the end of other features of the application pipeline. intercept function actually executes the RoleBasedAuthorizer provider function. Upon execution If it returns an error then it ends the call pipeline by calling finish() and responds with Forbidden HTTP error. otherwise continues with further application flow.

Till now we have created a complete feature and configured it.

rolesAllowed(Role.ADMIN) { route("/reports/usage/activity") { // some logic here when authorized... } }

As shown above to restrict/secure the routes to be used only by ADMIN. We need to define extension function rolesAllowed on Route . This extension function take allowed roles and authorizes it. On success, it returns authorisedRoute and continues further execution.

fun Route.rolesAllowed(vararg roles: Role, build: Route.() -> Unit): Route { val authorisedRoute = createChild(AuthorisedRouteSelector()) application.feature(RoleAuthorization).interceptPipeline(this.application, roles.toSet()) authorisedRoute.build() return authorisedRoute }

Above AuthorisedRouteSelector route selector is used to create a child route (created by Route.rolesAllowed DSL function). It is an authorization route node that will be used by our Role-Based Authorization feature. Below is the definition - class AuthorisedRouteSelector(): RouteSelector(RouteSelectorEvaluation.qualityConstant) { override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation = RouteSelectorEvaluation.Constant } As of now we have defined complete feature and provided extension function to use on routes to restrict access. But it doesn’t make any sense till we actually install it in the Ktor’s application pipeline.

fun Application.mainModule(ac: ApplicationContext) { install(RoleAuthorization) { validate { allowedRoles -> // your custom logic for role authorition } } }

Here we are actually installing our feature in application pipeline and we can provide our validate function definition with custom logic. validate function makes available roles those we specify on Route.rolesAllowed extension function of Route for your custom role authorization logic.

As we reached the end of our tutorial, I hope it helps.