Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,15 @@ public final class io/ktor/server/auth/apikey/ApiKeyAuthenticationProvider$Confi
public final fun validate (Lkotlin/jvm/functions/Function3;)V
}

public final class io/ktor/server/auth/apikey/typesafe/TypedApiKeyAuthConfig {
public fun <init> ()V
public final fun buildProvider (Ljava/lang/String;)Lio/ktor/server/auth/apikey/ApiKeyAuthenticationProvider;
public final fun getDescription ()Ljava/lang/String;
public final fun getHeaderName ()Ljava/lang/String;
public final fun getOnUnauthorized ()Lkotlin/jvm/functions/Function3;
public final fun setDescription (Ljava/lang/String;)V
public final fun setHeaderName (Ljava/lang/String;)V
public final fun setOnUnauthorized (Lkotlin/jvm/functions/Function3;)V
public final fun validate (Lkotlin/jvm/functions/Function3;)V
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@
// - Show declarations: true

// Library unique name: <io.ktor:ktor-server-auth-api-key>
final class <#A: kotlin/Any> io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig { // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig|null[0]
constructor <init>() // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.<init>|<init>(){}[0]

final var description // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.description|{}description[0]
final fun <get-description>(): kotlin/String? // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.description.<get-description>|<get-description>(){}[0]
final fun <set-description>(kotlin/String?) // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.description.<set-description>|<set-description>(kotlin.String?){}[0]
final var headerName // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.headerName|{}headerName[0]
final fun <get-headerName>(): kotlin/String // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.headerName.<get-headerName>|<get-headerName>(){}[0]
final fun <set-headerName>(kotlin/String) // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.headerName.<set-headerName>|<set-headerName>(kotlin.String){}[0]
final var onUnauthorized // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.onUnauthorized|{}onUnauthorized[0]
final fun <get-onUnauthorized>(): kotlin.coroutines/SuspendFunction2<io.ktor.server.application/ApplicationCall, io.ktor.server.auth/AuthenticationFailedCause, kotlin/Unit>? // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.onUnauthorized.<get-onUnauthorized>|<get-onUnauthorized>(){}[0]
final fun <set-onUnauthorized>(kotlin.coroutines/SuspendFunction2<io.ktor.server.application/ApplicationCall, io.ktor.server.auth/AuthenticationFailedCause, kotlin/Unit>?) // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.onUnauthorized.<set-onUnauthorized>|<set-onUnauthorized>(kotlin.coroutines.SuspendFunction2<io.ktor.server.application.ApplicationCall,io.ktor.server.auth.AuthenticationFailedCause,kotlin.Unit>?){}[0]

final fun buildProvider(kotlin/String): io.ktor.server.auth.apikey/ApiKeyAuthenticationProvider // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.buildProvider|buildProvider(kotlin.String){}[0]
final fun validate(kotlin.coroutines/SuspendFunction2<io.ktor.server.application/ApplicationCall, kotlin/String, #A?>) // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.validate|validate(kotlin.coroutines.SuspendFunction2<io.ktor.server.application.ApplicationCall,kotlin.String,1:0?>){}[0]
}

final class io.ktor.server.auth.apikey/ApiKeyAuthenticationProvider : io.ktor.server.auth/AuthenticationProvider { // io.ktor.server.auth.apikey/ApiKeyAuthenticationProvider|null[0]
final val headerName // io.ktor.server.auth.apikey/ApiKeyAuthenticationProvider.headerName|{}headerName[0]
final fun <get-headerName>(): kotlin/String // io.ktor.server.auth.apikey/ApiKeyAuthenticationProvider.headerName.<get-headerName>|<get-headerName>(){}[0]
Expand All @@ -32,3 +49,4 @@ final object io.ktor.server.auth.apikey/ApiKeyAuth { // io.ktor.server.auth.apik

final fun (io.ktor.server.auth/AuthenticationConfig).io.ktor.server.auth.apikey/apiKey(kotlin/String? = ..., kotlin/Function1<io.ktor.server.auth.apikey/ApiKeyAuthenticationProvider.Configuration, kotlin/Unit>) // io.ktor.server.auth.apikey/apiKey|apiKey@io.ktor.server.auth.AuthenticationConfig(kotlin.String?;kotlin.Function1<io.ktor.server.auth.apikey.ApiKeyAuthenticationProvider.Configuration,kotlin.Unit>){}[0]
final fun (io.ktor.server.auth/AuthenticationConfig).io.ktor.server.auth.apikey/apiKey(kotlin/String? = ..., kotlin/String? = ..., kotlin/Function1<io.ktor.server.auth.apikey/ApiKeyAuthenticationProvider.Configuration, kotlin/Unit>) // io.ktor.server.auth.apikey/apiKey|apiKey@io.ktor.server.auth.AuthenticationConfig(kotlin.String?;kotlin.String?;kotlin.Function1<io.ktor.server.auth.apikey.ApiKeyAuthenticationProvider.Configuration,kotlin.Unit>){}[0]
final inline fun <#A: reified kotlin/Any> io.ktor.server.auth.apikey.typesafe/apiKey(kotlin/String, kotlin/Function1<io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig<#A>, kotlin/Unit>): io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, io.ktor.server.auth.typesafe/DefaultAuthenticatedContext<#A>> // io.ktor.server.auth.apikey.typesafe/apiKey|apiKey(kotlin.String;kotlin.Function1<io.ktor.server.auth.apikey.typesafe.TypedApiKeyAuthConfig<0:0>,kotlin.Unit>){0§<kotlin.Any>}[0]
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ plugins {
}

kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcontext-parameters")
}
sourceSets {
commonMain.dependencies {
api(projects.ktorServerAuth)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public class ApiKeyAuthenticationProvider internal constructor(
}
}
if (principal != null) {
context.principal(principal)
context.principal(name, principal)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2014-2026 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.server.auth.apikey.typesafe

import io.ktor.server.auth.typesafe.DefaultAuthScheme
import io.ktor.server.auth.typesafe.DefaultAuthenticatedContext
import io.ktor.utils.io.*

/**
* Creates a typed API key authentication scheme.
*
* The [validate][TypedApiKeyAuthConfig.validate] callback returns a principal of type [P]. Use the returned scheme
* with [io.ktor.server.auth.typesafe.authenticateWith] to protect routes and access [io.ktor.server.auth.typesafe.principal] without casts.
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.apiKey)
*
* @param name name that identifies the API key authentication scheme.
* @param configure configures API key authentication for this scheme.
* @return a typed authentication scheme that produces principals of type [P].
*/
@ExperimentalKtorApi
public inline fun <reified P : Any> apiKey(
name: String,
configure: TypedApiKeyAuthConfig<P>.() -> Unit
): DefaultAuthScheme<P, DefaultAuthenticatedContext<P>> {
val typedConfig = TypedApiKeyAuthConfig<P>().apply(configure)
return DefaultAuthScheme.Companion.withDefaultContext(
name,
typedConfig.buildProvider(name),
typedConfig.onUnauthorized
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2014-2026 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.server.auth.apikey.typesafe

import io.ktor.server.application.*
import io.ktor.server.auth.apikey.*
import io.ktor.server.auth.typesafe.UnauthorizedHandler
import io.ktor.utils.io.*

/**
* Configures a typed API key authentication scheme.
*
* Unlike [ApiKeyAuthenticationProvider.Configuration], [validate] returns [P] so routes protected by
* [io.ktor.server.auth.typesafe.authenticateWith] can read [io.ktor.server.auth.typesafe.principal] as the configured type.
*
* This config does not expose provider-level `challenge`. Set [onUnauthorized] or pass `onUnauthorized` to
* [io.ktor.server.auth.typesafe.authenticateWith] to customize failure responses.
*
* Challenge strategy: a route-level `onUnauthorized` is used first, then [onUnauthorized]. If neither is configured,
* API key authentication responds with `401 Unauthorized` and uses the scheme name as the authentication challenge key.
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedApiKeyAuthConfig)
*
* @param P the principal type produced by this scheme.
*/
@ExperimentalKtorApi
@KtorDsl
public class TypedApiKeyAuthConfig<P : Any> @PublishedApi internal constructor() {
/**
* Human-readable description of this authentication scheme.
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedApiKeyAuthConfig.description)
*/
public var description: String? = null

/**
* Header name used to read the API key.
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedApiKeyAuthConfig.headerName)
*/
public var headerName: String = ApiKeyAuth.DEFAULT_HEADER_NAME

/**
* Default handler for authentication failures.
*
* A route-level `onUnauthorized` passed to [io.ktor.server.auth.typesafe.authenticateWith] overrides this handler. If both are `null`, API key
* authentication sends the default challenge described by this configuration.
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedApiKeyAuthConfig.onUnauthorized)
*/
public var onUnauthorized: UnauthorizedHandler? = null

private var validateFn: (suspend ApplicationCall.(String) -> P?)? = null

/**
* Sets a validation function for the API key string read from [headerName].
*
* Return a principal of type [P] when authentication succeeds, or `null` when the key is invalid.
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedApiKeyAuthConfig.validate)
*
* @param body validation function called with the API key header value.
*/
public fun validate(body: suspend ApplicationCall.(String) -> P?) {
validateFn = body
}

@PublishedApi
internal fun buildProvider(name: String): ApiKeyAuthenticationProvider {
val config = ApiKeyAuthenticationProvider.Configuration(name, description)
config.headerName = headerName
config.authScheme = name
validateFn?.let { fn -> config.validate { apiKey -> fn(apiKey) } }
return ApiKeyAuthenticationProvider(config)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ class ApiKeyAuthTest {

val module = buildApplicationModule {
headerName = header
challenge { call -> call.respond(errorStatus) }
challenge { call ->
call.authentication.allErrors
call.respond(errorStatus)
}
validate { header -> header.takeIf { it == apiKey }?.let { ApiKeyPrincipal(it) } }
}
testApplication {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright 2014-2026 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

@file:OptIn(ExperimentalKtorApi::class)

package io.ktor.server.auth.apikey

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.auth.typesafe.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import io.ktor.utils.io.*
import kotlin.test.*
import io.ktor.server.auth.apikey.typesafe.apiKey as typedApiKey

class TypedApiKeyAuthTest {

@Test
fun `api key scheme authenticates and rejects`() = testApplication {
val scheme = typedApiKey<ApiKeyPrincipal>("typed-api-key") {
validate { apiKey ->
if (apiKey == "valid") ApiKeyPrincipal(apiKey) else null
}
}

routing {
authenticateWith(
scheme,
onUnauthorized = { call, cause ->
call.respondText(cause::class.simpleName!!, status = HttpStatusCode.Unauthorized)
}
) {
get("/protected") {
call.respondText(principal.key)
}
}
}

val ok = client.get("/protected") {
header(ApiKeyAuth.DEFAULT_HEADER_NAME, "valid")
}
assertEquals(HttpStatusCode.OK, ok.status)
assertEquals("valid", ok.bodyAsText())

val missing = client.get("/protected")
assertEquals(HttpStatusCode.Unauthorized, missing.status)
assertEquals("NoCredentials", missing.bodyAsText())

val invalid = client.get("/protected") {
header(ApiKeyAuth.DEFAULT_HEADER_NAME, "invalid")
}
assertEquals(HttpStatusCode.Unauthorized, invalid.status)
assertEquals("InvalidCredentials", invalid.bodyAsText())
}

@Test
fun `api key scheme accepts configured header`() = testApplication {
val scheme = typedApiKey<ApiKeyPrincipal>("typed-api-key-header") {
headerName = "X-Custom-Api-Key"
validate { apiKey ->
if (apiKey == "custom") ApiKeyPrincipal(apiKey) else null
}
}

routing {
authenticateWith(scheme) {
get("/protected") {
call.respondText(principal.key)
}
}
}

val response = client.get("/protected") {
header("X-Custom-Api-Key", "custom")
}
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("custom", response.bodyAsText())
}

@Test
fun `api key onUnauthorized can be configured per scheme and route`() = testApplication {
val scheme = typedApiKey<ApiKeyPrincipal>("typed-api-key-unauthorized") {
onUnauthorized = { call, cause ->
call.respondText("scheme:${cause::class.simpleName}", status = HttpStatusCode.Unauthorized)
}
validate { apiKey ->
if (apiKey == "valid") ApiKeyPrincipal(apiKey) else null
}
}

routing {
authenticateWith(scheme) {
get("/scheme") {
call.respondText(principal.key)
}
}
authenticateWith(
scheme,
onUnauthorized = { call, cause ->
call.respondText("route:${cause::class.simpleName}", status = HttpStatusCode.Unauthorized)
}
) {
get("/route") {
call.respondText(principal.key)
}
}
}

val schemeResponse = client.get("/scheme")
assertEquals(HttpStatusCode.Unauthorized, schemeResponse.status)
assertEquals("scheme:NoCredentials", schemeResponse.bodyAsText())

val routeResponse = client.get("/route") {
header(ApiKeyAuth.DEFAULT_HEADER_NAME, "invalid")
}
assertEquals(HttpStatusCode.Unauthorized, routeResponse.status)
assertEquals("route:InvalidCredentials", routeResponse.bodyAsText())
}

@Test
fun `api key any-of failures are tracked per typed scheme name`() = testApplication {
val primary = typedApiKey<ApiKeyPrincipal>("primary-api-key") {
headerName = "X-Primary-Api-Key"
validate { apiKey ->
if (apiKey == "primary") ApiKeyPrincipal(apiKey) else null
}
}
val secondary = typedApiKey<ApiKeyPrincipal>("secondary-api-key") {
headerName = "X-Secondary-Api-Key"
validate { apiKey ->
if (apiKey == "secondary") ApiKeyPrincipal(apiKey) else null
}
}

routing {
authenticateWithAnyOf(
primary,
secondary,
onUnauthorized = { call, failures ->
val text = failures.entries.joinToString(";") { (name, cause) ->
"$name=${cause::class.simpleName}"
}
call.respondText(text, status = HttpStatusCode.Unauthorized)
}
) {
get("/protected") {
call.respondText(principal.key)
}
}
}

val rejected = client.get("/protected") {
header("X-Secondary-Api-Key", "wrong")
}
assertEquals(HttpStatusCode.Unauthorized, rejected.status)
assertEquals("primary-api-key=NoCredentials;secondary-api-key=InvalidCredentials", rejected.bodyAsText())

val accepted = client.get("/protected") {
header("X-Secondary-Api-Key", "secondary")
}
assertEquals(HttpStatusCode.OK, accepted.status)
assertEquals("secondary", accepted.bodyAsText())
}

private data class ApiKeyPrincipal(val key: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,28 @@ public final class io/ktor/server/auth/jwt/JWTPrincipal : io/ktor/server/auth/jw
public fun <init> (Lcom/auth0/jwt/interfaces/Payload;)V
}

public final class io/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig {
public fun <init> ()V
public final fun authHeader (Lkotlin/jvm/functions/Function1;)V
public final fun authSchemes (Ljava/lang/String;[Ljava/lang/String;)V
public static synthetic fun authSchemes$default (Lio/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig;Ljava/lang/String;[Ljava/lang/String;ILjava/lang/Object;)V
public final fun buildProvider (Ljava/lang/String;)Lio/ktor/server/auth/jwt/JWTAuthenticationProvider;
public final fun getDescription ()Ljava/lang/String;
public final fun getOnUnauthorized ()Lkotlin/jvm/functions/Function3;
public final fun getRealm ()Ljava/lang/String;
public final fun setDescription (Ljava/lang/String;)V
public final fun setOnUnauthorized (Lkotlin/jvm/functions/Function3;)V
public final fun setRealm (Ljava/lang/String;)V
public final fun validate (Lkotlin/jvm/functions/Function3;)V
public final fun verifier (Lcom/auth0/jwk/JwkProvider;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun verifier (Lcom/auth0/jwk/JwkProvider;Lkotlin/jvm/functions/Function1;)V
public final fun verifier (Lcom/auth0/jwt/JWTVerifier;)V
public final fun verifier (Ljava/lang/String;Ljava/lang/String;Lcom/auth0/jwt/algorithms/Algorithm;Lkotlin/jvm/functions/Function1;)V
public final fun verifier (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun verifier (Lkotlin/jvm/functions/Function2;)V
public static synthetic fun verifier$default (Lio/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig;Lcom/auth0/jwk/JwkProvider;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun verifier$default (Lio/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig;Lcom/auth0/jwk/JwkProvider;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun verifier$default (Lio/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig;Ljava/lang/String;Ljava/lang/String;Lcom/auth0/jwt/algorithms/Algorithm;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun verifier$default (Lio/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
}

Loading
Loading