Skip to Content
Course content

118: Ktor Authentication Plugins

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen a lot of developers start their Ktor projects by treating security as a routing concern rather than a pipeline concern. When you're first building out a protected resource—say, a "Secure Document Vault" where users can only access their private PDFs—the instinct is to just check for a token right at the top of the route handler. It seems simple enough: grab the header, verify it, and if it's missing, throw a 401.

The Manual Gatekeeper Approach

In a naive implementation, your code ends up looking like this. You're essentially hand-rolling your own security check inside every single protected endpoint:

routing {
    get("/vault/documents/{id}") {
        val authHeader = call.request.headers["Authorization"]
        if (authHeader == null || !isValidToken(authHeader)) {
            call.respond(HttpStatusCode.Unauthorized, "Invalid Token")
            return@get
        }
        val docId = call.parameters["id"]
        call.respondText("Fetching document $docId")
    }
    
    get("/vault/settings") {
        val authHeader = call.request.headers["Authorization"]
        if (authHeader == null || !isValidToken(authHeader)) {
            call.respond(HttpStatusCode.Unauthorized, "Invalid Token")
            return@get
        }
        call.respondText("Vault settings page")
    }
}

This works for two routes. Maybe even five. But as a software engineer, you can already see where this falls apart. First, you're violating DRY (Don't Repeat Yourself) in a way that's actually dangerous. If you decide to change your token format or add a second way to authenticate, you have to hunt down every single route and update the logic. More importantly, it only takes one tired developer forgetting to paste that if block into a new route to create a massive security hole in your application. I've been on the receiving end of those kinds of bugs, and they're never fun to explain to a client.

Delegating to the Authentication Plugin

The better way is to move the identity logic out of the business logic and into the Ktor pipeline using the Authentication plugin. Instead of checking headers manually, you define a "provider" that handles the heavy lifting of verifying who the user is, and then you simply wrap your routes in an authenticate block.

Here is how I would actually structure this for a production-ready vault API:

install(Authentication) {
    jwt("auth-jwt") { 
        realm = "Access to Document Vault"
        verifier(myJwtVerifier)
        validate { credential ->
            if (credential.payload.getClaim("username").asString() != "") {
                JWTPrincipal(credential.payload)
            } else {
                null
            }
        }
    }
}

routing {
    authenticate("auth-jwt") {
        // Everything inside this block is now protected
        get("/vault/documents/{id}") {
            val principal = call.principal<JWTPrincipal>()
            val username = principal?.payload?.getClaim("username")?.asString()
            call.respondText("Hello $username, here is document ${call.parameters["id"]}")
        }
        get("/vault/settings") {
            call.respondText("Vault settings page")
        }
    }
    
    // Public routes stay outside the block
    get("/vault/health") {
        call.respondText("Vault is online")
    }
}

Trading Setup Time for Structural Integrity

The trade-off here is that the plugin approach requires a bit more boilerplate upfront. You have to define the provider, configure the verifier, and understand the concept of a Principal (which is essentially just a Ktor-friendly way of saying "the identity of the current user").

However, the gain in maintainability is huge. By using the authenticate {} wrapper, you've created a visual and logical boundary. You can see at a glance which parts of your API are public and which are private. If you need to add a second authentication method—like an API Key for third-party integrations—you don't have to rewrite your routes; you just add a second provider to the install block and list both in the authenticate call. It turns security from a repetitive chore into a configurable piece of infrastructure.




📋 Practical Task

Exercise: Implementing a Multi-Provider Guard for the Document Vault

You are tasked with upgrading the Document Vault API. Currently, it only supports JWT tokens for users. However, the company now needs to allow "Admin Service" accounts to access the vault using a simple static API Key found in the X-API-KEY header.

Your requirements:

  • Install the Authentication plugin.
  • Create two providers: one named "jwt-auth" and one named "api-key-auth".
  • For the "api-key-auth" provider, implement a custom verification logic that checks if the X-API-KEY header equals "secret-admin-key-123".
  • Wrap the /vault/documents/{id} and /vault/settings routes so that they are accessible if either the JWT is valid OR the API Key is valid.
  • Ensure the /vault/health route remains publicly accessible.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.