Skip to content

Custom Authentication Scheme using Feature of Ktor

Ktor framework, with its built-in Features, is one of the framework's finest achievements. It allows developers to install some well-known functionality required by most of the applications with just a few lines of code and, on top of that create new custom extendable pluggable mechanisms without worrying explicitly about the internal architecture of ktor. This enables faster development with less boilerplate code while reducing building from scratch and making applications adaptable to all kinds of complex functionality.

To build a custom scheme using the Authentication Feature, you need to wrap your head around features and how it uses Pipelines to provide functionality.

Features in Ktor

Features in ktor allow us to include functionality as per our requirements in application, such as Authentication, CORS, Compression, Logging, etc. In some frameworks like ExpressJS, these are known as Middleware.

A Feature consists of two parts:

  • Initialization: It initializes the feature and configures the functionality.
  • Execution: which does the heavy lifting for getting things done for each incoming request.

Generally, we install the feature and configure it or use the default configured parameter. The feature itself will do the rest of the part for you. For example, if we need compression, we would call install(Compression) in our application setup and configure the parameter required in the lambda block. Thus our application supports the compression now.

fun Application.app() {
    install(Compression) {
    gzip {
        priority = 1.0
    }
    deflate {
        priority = 10.0
        minimumSize(1024)
    }
    }
}

ktor framework comes with many in-built Features which are easy to use, lightweight and flexible. There are many features that Ktor supports out of the box. You can read about them on their official site.

Before implementing a custom scheme using Authentication Feature of Ktor. We shall be going through some of the core concepts on which features have been built.

Pipelines in Ktor

The pipeline is a structure containing a sequence of functions (blocks/lambdas) that are called one after another, distributed in phases topologically ordered, with the ability to mutate the sequence and to call the remaining functions in the pipeline and then return to current block.

Pipelines are used in Ktor as an extension mechanism to plug functionality in at the right place. For example, a Ktor application defines five main phases: Setup, Monitoring, Features, Call and Fallback. The routing feature defines its own nested pipeline inside the application’s call phase.

In simple terms, pipelines contain collections of functions which are executed one after another in a specific and predefined order, where each function does some manipulation to an incoming request and then returns to current block.

Phases

Ktor Pipelines consists of phases, a group of interceptors that perform a certain task for each request and response.

There are five main standard phases in the application call pipeline in ktor.

  • Setup: This phase is used for preparing the call and its attributes for processing (CallId Feature)

  • Monitoring: This phase is used for tracing calls it is useful for logging, metrics, error handling, and so on (CallLogging Feature)

  • Features: All the features installed in our application should intercept this phase (Authentication Feature).

  • Call: features and interceptors used to complete the call (Routing Feature)

  • Fallback: features that process unhandled calls in a normal way and resolve them (StatusPages Feature)

A high level overview of how ApplicationCallPipeline is implemented in ktor.

open class ApplicationCallPipeline : Pipeline<Unit, ApplicationCall>(Setup, Monitoring, Features, Call, Fallback) {
    val receivePipeline = ApplicationReceivePipeline()
    val sendPipeline = ApplicationSendPipeline()

    companion object {
        val Setup = PipelinePhase("Setup")
        val Monitoring = PipelinePhase("Monitoring")
        val Features = PipelinePhase("Features")
        val Call = PipelinePhase("Call")
        val Fallback = PipelinePhase("Fallback")
    }
}

Pipeline class has four APIs with which you can create your custom phase and intercept that phase when a request is received.

class PipelinePhase(val name: String)
class Pipeline <TSubject : Any, TContext : Any> {
    constructor(vararg phases: PipelinePhase)

    val attributes: Attributes

    fun addPhase(phase: PipelinePhase)
    fun insertPhaseAfter(reference: PipelinePhase, phase: PipelinePhase)
    fun insertPhaseBefore(reference: PipelinePhase, phase: PipelinePhase)

    fun intercept(phase: PipelinePhase, block: suspend PipelineContext.(TSubject) -> Unit)
}

You can register a phase in your ApplicationCallPipeline using insertPhaseAfter,insertPhaseBefore, addPhase. For instance, If you want two custom phases in your ApplicationCallPipeline before Feature phases kick in, you can define it like:

val pipelinePhase1 = PipelinePhase("Phase1")
val pipelinePhase2 = PipelinePhase("Phase2")

pipeline.insertPhaseBefore(ApplicationCallPipeline.Features, pipelinePhase2)
pipeline.insertPhaseBefore(pipelinePhase2, pipelinePhase1)

Before we implement our custom scheme for authentication, we need to to familiarize by some terms

A principal is an entity that can be authenticated: a user, a computer, a service, etc. In Ktor, various authentication providers might use different principals. For example, the basic and form providers authenticate UserIdPrincipal while the jwt provider verifies JWTPrincipal.

A credential is a set of properties for a server to authenticate a principal: a user/password pair, an API key, and so on. For instance, the basic and form providers use UserPasswordCredential to validate a username and password while jwt validates JWTCredential.

Now that all pieces of puzzle are in place we can move further to implement our own custom scheme using Authentication Feature.

Implementation of Scheme using Authentication feature

Consider a scenario where you want to authenticate the user based on the username, password, and opaque token. The user needs to pass credentials in the headers section to access the routes. Keeping this in mind, we will build our scheme.

A credentials class containes the attribute of user.

data class Credentials(
    val username: String,
    val password: String,
    val token: String,
)

Authentication Provider

First, we will look at the Configuration class, which contains all the attributes required to authenticate the user. For instance, we will need a validate function to validate user credentials. Configuration will be the inner class of CustomAuthenticationProvider, a Feature instance class, which should contain immutable methods and properties to ensure thread-safety as Ktor works in a highly asynchronous environment.

//Feature   
class CustomAuthenticationProvider internal constructor(config: Configuration) : AuthenticationProvider(config) {

    internal val authToken: (ApplicationCall) -> String? = config.authToken
    internal val username: (ApplicationCall) -> String? = config.username
    internal val password: (ApplicationCall) -> String? = config.password
    internal val authenticationFunction = config.authenticationFunction

    //Configuration 
    class Configuration internal constructor(name: String?) : AuthenticationProvider.Configuration(name) {

        internal var authenticationFunction: AuthenticationFunction<Credentials> = {
            throw NotImplementedError(
                "auth validate function is not specified. Use CustomAuth { validate { ... } } to fix."
            )
        }

        internal var authToken: (ApplicationCall) -> String? = { call -> call.request.header("auth-token") }

        internal var username: (ApplicationCall) -> String? = { call -> call.request.header("username") }

        internal var password: (ApplicationCall) -> String? = { call -> call.request.header("password") }

        internal fun build() = CustomAuthenticationProvider(this)

        internal fun validate(body: AuthenticationFunction<Credentials>) {
            authenticationFunction = body
        }
    }
}

Installation of Scheme

Installation of our customAuth scheme using Authentication Feature. The most critical function is validate that validates a username,password and token, which serves the core functionality required by our application.

install(Authentication){
        customAuth {
            validate { creds ->
                buildUserContext(creds)
            }
        }
    }

Validation Function

buildUserContext will return a Principal if the user credentials are correct; else it will return null, indicating the user credentails are incorrect.

fun buildUserContext(creds: Credentials): UserIdPrincipal? =
    if (creds.username == "username" && creds.password == "password" && creds.token == "token") {
        UserIdPrincipal(creds.username)
    } else {
        null
    }

Intercepting The Pipeline

customAuth is an extension function that does all the magic. CustomAuthenticationProvider class is instantiated first, which will gather the configuration passed in customAuth. Now we will intercept the Pipeline at RequestAuthentication Phase, a predefined phase of Authentication Feature. Further, getAuthenticationCredentials is invoked to get all attributes from the request and returns Credentials . Now that we have credentials, we validated those credentials using the validate function, which we have defined earlier.

If any credentials or principal are found null, we will respond to the user with 401 HttpStatusCode.Unauthorized with message "Invalid Credentials" and finish the Pipeline execution else we will set Principal in context of that call.

fun Authentication.Configuration.customAuth(
    name: String? = null,
    configure: CustomAuthenticationProvider.Configuration.() -> Unit,
) {
    val provider = CustomAuthenticationProvider.Configuration(name).apply(configure).build()

    val authenticate = provider.authenticationFunction

    provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->

        val credentials = call.request.getAuthenticationCredentials(provider)

        val principal = credentials?.let { authenticate(call, it) }

        val cause = when {
            credentials == null -> AuthenticationFailedCause.NoCredentials
            principal == null -> AuthenticationFailedCause.InvalidCredentials
            else -> null
        }

        if (cause != null) {
            context.challenge("Invalid Credentials", cause) {
                call.respond(HttpStatusCode.Unauthorized, "Invalid Credentials")
                it.complete()
            }
        }

        if (principal != null) {
            context.principal(principal)
        }
    }
    register(provider)
}

fun ApplicationRequest.getAuthenticationCredentials(provider: CustomAuthenticationProvider): Credentials? {
    val token = provider.authToken(call)
    val username = provider.username(call)
    val password = provider.password(call)

    return if (username == null || password == null || token == null) {
        null
    } else Credentials(username, password, token)
}

See It In Action

Below is how to run and protect route "/users" while wrapping inside the authenticate lambda function. Now one must pass credentials to access the route. We have used a netty engine for demonstration purposes, but you can use the engine of your choice.

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)


fun Application.module() {

    install(Authentication){
        customAuth {
            validate { creds ->
                buildUserContext(creds)
            }
        }
    }

    routing {
        authenticate {
            route("/users") {
                get {
                    call.respond("User Authenticated")
                }
            }
        }
    }
}
Application - Autoreload is disabled because the development mode is off.
Application - Responding at http://0.0.0.0:8080
ktor {
    deployment {
        port = 8080
        port = ${?PORT}
    }
    application {
        modules = [ ApplicationKt.module ]
    }
}

Closing Notes

The above implementation is a simplified version of a custom-built scheme that explains the core concepts and how to implement a custom scheme using Authentication Feature. At Vayana Network, we use built-in features and some complex homegrown features built by our team.

The code used in this article has been made available here.

Back to top