diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/api/ktor-server-auth-api-key.api b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/api/ktor-server-auth-api-key.api index ab15d55d43c..1e0bc5234e5 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/api/ktor-server-auth-api-key.api +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/api/ktor-server-auth-api-key.api @@ -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 ()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 +} + diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/api/ktor-server-auth-api-key.klib.api b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/api/ktor-server-auth-api-key.klib.api index af03b908b77..3e031ce98a4 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/api/ktor-server-auth-api-key.klib.api +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/api/ktor-server-auth-api-key.klib.api @@ -6,6 +6,23 @@ // - Show declarations: true // Library unique name: +final class <#A: kotlin/Any> io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig { // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig|null[0] + constructor () // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.|(){}[0] + + final var description // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.description|{}description[0] + final fun (): kotlin/String? // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.description.|(){}[0] + final fun (kotlin/String?) // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.description.|(kotlin.String?){}[0] + final var headerName // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.headerName|{}headerName[0] + final fun (): kotlin/String // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.headerName.|(){}[0] + final fun (kotlin/String) // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.headerName.|(kotlin.String){}[0] + final var onUnauthorized // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.onUnauthorized|{}onUnauthorized[0] + final fun (): kotlin.coroutines/SuspendFunction2? // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.onUnauthorized.|(){}[0] + final fun (kotlin.coroutines/SuspendFunction2?) // io.ktor.server.auth.apikey.typesafe/TypedApiKeyAuthConfig.onUnauthorized.|(kotlin.coroutines.SuspendFunction2?){}[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.auth.apikey.typesafe/TypedApiKeyAuthConfig.validate|validate(kotlin.coroutines.SuspendFunction2){}[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 (): kotlin/String // io.ktor.server.auth.apikey/ApiKeyAuthenticationProvider.headerName.|(){}[0] @@ -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/apiKey|apiKey@io.ktor.server.auth.AuthenticationConfig(kotlin.String?;kotlin.Function1){}[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/apiKey|apiKey@io.ktor.server.auth.AuthenticationConfig(kotlin.String?;kotlin.String?;kotlin.Function1){}[0] +final inline fun <#A: reified kotlin/Any> io.ktor.server.auth.apikey.typesafe/apiKey(kotlin/String, kotlin/Function1, 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,kotlin.Unit>){0§}[0] diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/build.gradle.kts b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/build.gradle.kts index 7d5f5525bdb..c9aa891deb3 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/build.gradle.kts +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/build.gradle.kts @@ -8,6 +8,9 @@ plugins { } kotlin { + compilerOptions { + freeCompilerArgs.add("-Xcontext-parameters") + } sourceSets { commonMain.dependencies { api(projects.ktorServerAuth) diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/ApiKeyAuth.kt b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/ApiKeyAuth.kt index 8b97f46903b..69ab3efdbfc 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/ApiKeyAuth.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/ApiKeyAuth.kt @@ -103,7 +103,7 @@ public class ApiKeyAuthenticationProvider internal constructor( } } if (principal != null) { - context.principal(principal) + context.principal(name, principal) } } diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/typesafe/ApiKeyTypedProvider.kt b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/typesafe/ApiKeyTypedProvider.kt new file mode 100644 index 00000000000..d158805ecf5 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/typesafe/ApiKeyTypedProvider.kt @@ -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 apiKey( + name: String, + configure: TypedApiKeyAuthConfig

.() -> Unit +): DefaultAuthScheme> { + val typedConfig = TypedApiKeyAuthConfig

().apply(configure) + return DefaultAuthScheme.Companion.withDefaultContext( + name, + typedConfig.buildProvider(name), + typedConfig.onUnauthorized + ) +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/typesafe/TypedApiKeyAuthConfig.kt b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/typesafe/TypedApiKeyAuthConfig.kt new file mode 100644 index 00000000000..6ef88bdb87b --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/src/io/ktor/server/auth/apikey/typesafe/TypedApiKeyAuthConfig.kt @@ -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

@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) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/test/io/ktor/server/auth/apikey/ApiKeyAuthTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/test/io/ktor/server/auth/apikey/ApiKeyAuthTest.kt index f94c032fec3..33acf5aa208 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/test/io/ktor/server/auth/apikey/ApiKeyAuthTest.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/test/io/ktor/server/auth/apikey/ApiKeyAuthTest.kt @@ -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 { diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/test/io/ktor/server/auth/apikey/TypedApiKeyAuthTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/test/io/ktor/server/auth/apikey/TypedApiKeyAuthTest.kt new file mode 100644 index 00000000000..e492d9c1a54 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-api-key/common/test/io/ktor/server/auth/apikey/TypedApiKeyAuthTest.kt @@ -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("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("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("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("primary-api-key") { + headerName = "X-Primary-Api-Key" + validate { apiKey -> + if (apiKey == "primary") ApiKeyPrincipal(apiKey) else null + } + } + val secondary = typedApiKey("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) +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/api/ktor-server-auth-jwt.api b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/api/ktor-server-auth-jwt.api index 0d1a9fa8af6..80b01a06be0 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/api/ktor-server-auth-jwt.api +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/api/ktor-server-auth-jwt.api @@ -58,3 +58,28 @@ public final class io/ktor/server/auth/jwt/JWTPrincipal : io/ktor/server/auth/jw public fun (Lcom/auth0/jwt/interfaces/Payload;)V } +public final class io/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig { + public fun ()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 +} + diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/build.gradle.kts b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/build.gradle.kts index bc4afa3f2e8..2d935fe60ec 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/build.gradle.kts +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/build.gradle.kts @@ -7,6 +7,9 @@ plugins { } kotlin { + compilerOptions { + freeCompilerArgs.add("-Xcontext-parameters") + } sourceSets { jvmMain.dependencies { api(projects.ktorServerAuth) @@ -18,4 +21,3 @@ kotlin { } } } - diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/jvm/src/io/ktor/server/auth/jwt/typesafe/JwtTypedProvider.kt b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/jvm/src/io/ktor/server/auth/jwt/typesafe/JwtTypedProvider.kt new file mode 100644 index 00000000000..172167219b9 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/jvm/src/io/ktor/server/auth/jwt/typesafe/JwtTypedProvider.kt @@ -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.jwt.typesafe + +import io.ktor.server.auth.typesafe.DefaultAuthScheme +import io.ktor.server.auth.typesafe.DefaultAuthenticatedContext +import io.ktor.utils.io.* + +/** + * Creates a typed JWT authentication scheme. + * + * The [validate][TypedJwtAuthConfig.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.jwt) + * + * @param name name that identifies the JWT authentication scheme. + * @param configure configures JWT authentication for this scheme. + * @return a typed authentication scheme that produces principals of type [P]. + */ +@ExperimentalKtorApi +public inline fun jwt( + name: String, + configure: TypedJwtAuthConfig

.() -> Unit +): DefaultAuthScheme> { + val typedConfig = TypedJwtAuthConfig

().apply(configure) + return DefaultAuthScheme.Companion.withDefaultContext( + name, + typedConfig.buildProvider(name), + typedConfig.onUnauthorized + ) +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/jvm/src/io/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig.kt b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/jvm/src/io/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig.kt new file mode 100644 index 00000000000..7ed8b90a58c --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/jvm/src/io/ktor/server/auth/jwt/typesafe/TypedJwtAuthConfig.kt @@ -0,0 +1,204 @@ +/* + * 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.jwt.typesafe + +import com.auth0.jwk.JwkProvider +import com.auth0.jwt.JWTVerifier +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.interfaces.Verification +import io.ktor.http.auth.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import io.ktor.utils.io.* + +/** + * Configures a typed JWT authentication scheme. + * + * Unlike [JWTAuthenticationProvider.Config], [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, + * JWT authentication responds to missing or invalid credentials with a `WWW-Authenticate` challenge for the default + * authentication scheme (`Bearer` unless changed by [authSchemes]) and [realm]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig) + * + * @param P the principal type produced by this scheme. + */ +@ExperimentalKtorApi +@KtorDsl +public class TypedJwtAuthConfig

@PublishedApi internal constructor() { + /** + * Human-readable description of this authentication scheme. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.description) + */ + public var description: String? = null + + /** + * JWT realm passed in the `WWW-Authenticate` header. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.realm) + */ + public var realm: String = "Ktor Server" + + /** + * Default handler for authentication failures. + * + * A route-level `onUnauthorized` passed to [io.ktor.server.auth.typesafe.authenticateWith] overrides this handler. If both are `null`, JWT + * authentication sends the default challenge described by this configuration. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.onUnauthorized) + */ + public var onUnauthorized: (suspend (ApplicationCall, AuthenticationFailedCause) -> Unit)? = null + + private var validateFn: (suspend ApplicationCall.(JWTCredential) -> P?)? = null + private var authHeaderFn: ((ApplicationCall) -> HttpAuthHeader?)? = null + private var authSchemes: AuthSchemes? = null + private var verifierConfig: (JWTAuthenticationProvider.Config.() -> Unit)? = null + + /** + * Configures how to retrieve an HTTP authentication header. + * + * By default, JWT authentication parses the `Authorization` header. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.authHeader) + * + * @param block returns an authentication header for the call, or `null` when no header is available. + */ + public fun authHeader(block: (ApplicationCall) -> HttpAuthHeader?) { + authHeaderFn = block + } + + /** + * Configures accepted authentication schemes. + * + * By default, only the `Bearer` scheme is accepted. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.authSchemes) + * + * @param defaultScheme scheme used in the default challenge. + * @param additionalSchemes additional schemes accepted when validating the request. + */ + public fun authSchemes(defaultScheme: String = "Bearer", vararg additionalSchemes: String) { + this.authSchemes = AuthSchemes(defaultScheme, additionalSchemes.toList()) + } + + /** + * Sets the [JWTVerifier] used to verify token format and signature. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.verifier) + * + * @param verifier verifies token format and signature. + */ + public fun verifier(verifier: JWTVerifier) { + verifierConfig = { verifier(verifier) } + } + + /** + * Sets a suspend function that selects the [JWTVerifier] for a token. + * + * Return `null` when no verifier can be created for the provided header. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.verifier) + * + * @param verifier resolves a verifier for the authentication header. + */ + public fun verifier(verifier: suspend (HttpAuthHeader) -> JWTVerifier?) { + verifierConfig = { verifier(verifier) } + } + + /** + * Creates a [JWTVerifier] from [jwkProvider] and [issuer]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.verifier) + * + * @param jwkProvider provides JSON Web Keys. + * @param issuer expected token issuer. + * @param configure configures JWT verification. + */ + public fun verifier(jwkProvider: JwkProvider, issuer: String, configure: JWTConfigureFunction = {}) { + verifierConfig = { verifier(jwkProvider, issuer, configure) } + } + + /** + * Creates a [JWTVerifier] from [jwkProvider]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.verifier) + * + * @param jwkProvider provides JSON Web Keys. + * @param configure configures JWT verification. + */ + public fun verifier(jwkProvider: JwkProvider, configure: JWTConfigureFunction = {}) { + verifierConfig = { verifier(jwkProvider, configure) } + } + + /** + * Creates a [JWTVerifier] for the given [issuer], [audience], and [algorithm]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.verifier) + * + * @param issuer expected token issuer. + * @param audience expected token audience. + * @param algorithm algorithm used to verify token signatures. + * @param block customizes the underlying JWT verification. + */ + public fun verifier( + issuer: String, + audience: String, + algorithm: Algorithm, + block: Verification.() -> Unit = {} + ) { + verifierConfig = { verifier(issuer, audience, algorithm, block) } + } + + /** + * Creates a [JWTVerifier] using JSON Web Keys discovered from [issuer]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.verifier) + * + * @param issuer expected token issuer and JWK provider base URL. + * @param block configures JWT verification. + */ + public fun verifier(issuer: String, block: JWTConfigureFunction = {}) { + verifierConfig = { verifier(issuer, block) } + } + + /** + * Sets a validation function for [JWTCredential]. + * + * Return a principal of type [P] when authentication succeeds, or `null` when the verified JWT should not be + * accepted. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedJwtAuthConfig.validate) + * + * @param body validation function called after the token is verified. + */ + public fun validate(body: suspend ApplicationCall.(JWTCredential) -> P?) { + validateFn = body + } + + @PublishedApi + internal fun buildProvider(name: String): JWTAuthenticationProvider { + val config = JWTAuthenticationProvider.Config(name, description) + config.realm = realm + authHeaderFn?.let { config.authHeader(it) } + authSchemes?.let { schemes -> + config.authSchemes(schemes.defaultScheme, *schemes.additionalSchemes.toTypedArray()) + } + verifierConfig?.invoke(config) + validateFn?.let { fn -> config.validate { credential -> fn(credential) } } + return JWTAuthenticationProvider(config) + } + + private class AuthSchemes( + val defaultScheme: String, + val additionalSchemes: List + ) +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/jvm/test/io/ktor/server/auth/jwt/TypedJwtAuthTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/jvm/test/io/ktor/server/auth/jwt/TypedJwtAuthTest.kt new file mode 100644 index 00000000000..953d1f1eab4 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth-jwt/jvm/test/io/ktor/server/auth/jwt/TypedJwtAuthTest.kt @@ -0,0 +1,164 @@ +/* + * 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.jwt + +import com.auth0.jwt.* +import com.auth0.jwt.algorithms.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.http.auth.* +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.jwt.typesafe.jwt as typedJwt + +class TypedJwtAuthTest { + + @Test + fun `jwt scheme authenticates and rejects`() = testApplication { + val scheme = typedJwt("typed-jwt") { + verifier(ISSUER, AUDIENCE, ALGORITHM) + validate { credential -> + if (credential.audience.contains(AUDIENCE)) { + JwtUser(credential.payload.subject) + } else { + null + } + } + } + + routing { + authenticateWith(scheme) { + get("/profile") { + call.respondText(principal.name) + } + } + } + + val ok = client.get("/profile") { + header(HttpHeaders.Authorization, token(subject = "alice")) + } + assertEquals(HttpStatusCode.OK, ok.status) + assertEquals("alice", ok.bodyAsText()) + + assertEquals(HttpStatusCode.Unauthorized, client.get("/profile").status) + + val invalid = client.get("/profile") { + header(HttpHeaders.Authorization, token(audience = "wrong")) + } + assertEquals(HttpStatusCode.Unauthorized, invalid.status) + } + + @Test + fun `jwt scheme accepts configured auth schemes`() = testApplication { + val scheme = typedJwt("typed-jwt-scheme") { + authSchemes("Bearer", "Token") + verifier(ISSUER, AUDIENCE, ALGORITHM) + validate { credential -> JwtUser(credential.payload.subject) } + } + + routing { + authenticateWith(scheme) { + get("/profile") { + call.respondText(principal.name) + } + } + } + + val response = client.get("/profile") { + header(HttpHeaders.Authorization, token(scheme = "Token", subject = "token-user")) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("token-user", response.bodyAsText()) + } + + @Test + fun `jwt scheme accepts configured auth header`() = testApplication { + val scheme = typedJwt("typed-jwt-header") { + authHeader { call -> + call.request.headers["X-Auth"]?.let { parseAuthorizationHeader(it) } + } + verifier(ISSUER, AUDIENCE, ALGORITHM) + validate { credential -> JwtUser(credential.payload.subject) } + } + + routing { + authenticateWith(scheme) { + get("/profile") { + call.respondText(principal.name) + } + } + } + + val response = client.get("/profile") { + header("X-Auth", token(subject = "header-user")) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("header-user", response.bodyAsText()) + } + + @Test + fun `jwt onUnauthorized can be configured per scheme and route`() = testApplication { + val scheme = typedJwt("typed-jwt-unauthorized") { + onUnauthorized = { call, cause -> + call.respondText("scheme:${cause::class.simpleName}", status = HttpStatusCode.Unauthorized) + } + verifier(ISSUER, AUDIENCE, ALGORITHM) + validate { credential -> JwtUser(credential.payload.subject) } + } + + routing { + authenticateWith(scheme) { + get("/scheme") { + call.respondText(principal.name) + } + } + authenticateWith( + scheme, + onUnauthorized = { call, cause -> + call.respondText("route:${cause::class.simpleName}", status = HttpStatusCode.Unauthorized) + } + ) { + get("/route") { + call.respondText(principal.name) + } + } + } + + val schemeResponse = client.get("/scheme") + assertEquals(HttpStatusCode.Unauthorized, schemeResponse.status) + assertEquals("scheme:NoCredentials", schemeResponse.bodyAsText()) + + val routeResponse = client.get("/route") { + header(HttpHeaders.Authorization, token(audience = "wrong")) + } + assertEquals(HttpStatusCode.Unauthorized, routeResponse.status) + assertEquals("route:InvalidCredentials", routeResponse.bodyAsText()) + } + + private data class JwtUser(val name: String) + + private fun token( + scheme: String = "Bearer", + subject: String = "user", + audience: String = AUDIENCE + ): String = "$scheme " + JWT.create() + .withAudience(audience) + .withIssuer(ISSUER) + .withSubject(subject) + .sign(ALGORITHM) + + private companion object { + private const val ISSUER = "https://jwt-provider-domain/" + private const val AUDIENCE = "jwt-audience" + private val ALGORITHM = Algorithm.HMAC256("secret") + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.api b/ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.api index 80c84112ff0..64f55d4a55b 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.api +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.api @@ -637,3 +637,200 @@ public final class io/ktor/server/auth/UserPasswordCredential { public fun toString ()Ljava/lang/String; } +public final class io/ktor/server/auth/typesafe/AnonymousAuthScheme : io/ktor/server/auth/typesafe/AuthScheme { + public fun (Lio/ktor/server/auth/typesafe/DefaultAuthScheme;Lkotlin/jvm/functions/Function2;)V + public synthetic fun createAuthenticatedContext (Lio/ktor/server/routing/Route;)Lio/ktor/server/auth/typesafe/AuthenticatedContext; + public fun createAuthenticatedContext (Lio/ktor/server/routing/Route;)Lio/ktor/server/auth/typesafe/DefaultAuthenticatedContext; + public final fun getBase ()Lio/ktor/server/auth/typesafe/DefaultAuthScheme; + public fun getName ()Ljava/lang/String; +} + +public abstract interface class io/ktor/server/auth/typesafe/AuthRole { + public abstract fun getName ()Ljava/lang/String; +} + +public abstract interface class io/ktor/server/auth/typesafe/AuthScheme { + public abstract fun createAuthenticatedContext (Lio/ktor/server/routing/Route;)Lio/ktor/server/auth/typesafe/AuthenticatedContext; + public abstract fun getName ()Ljava/lang/String; +} + +public abstract interface class io/ktor/server/auth/typesafe/AuthenticatedContext { + public abstract fun principal (Lio/ktor/server/routing/RoutingContext;)Ljava/lang/Object; +} + +public final class io/ktor/server/auth/typesafe/BasicTypedProviderKt { + public static final fun buildSessionAuthScheme (Ljava/lang/String;Lio/ktor/util/reflect/TypeInfo;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)Lio/ktor/server/auth/typesafe/SessionAuthScheme; +} + +public class io/ktor/server/auth/typesafe/DefaultAuthScheme : io/ktor/server/auth/typesafe/AuthScheme { + public static final field Companion Lio/ktor/server/auth/typesafe/DefaultAuthScheme$Companion; + public fun (Ljava/lang/String;Lkotlin/reflect/KClass;Lio/ktor/server/auth/AuthenticationProvider;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;)V + public fun createAuthenticatedContext (Lio/ktor/server/routing/Route;)Lio/ktor/server/auth/typesafe/AuthenticatedContext; + public fun getName ()Ljava/lang/String; +} + +public final class io/ktor/server/auth/typesafe/DefaultAuthScheme$Companion { +} + +public class io/ktor/server/auth/typesafe/DefaultAuthenticatedContext : io/ktor/server/auth/typesafe/AuthenticatedContext { + public fun (Lio/ktor/util/AttributeKey;)V + public final fun getPrincipalKey ()Lio/ktor/util/AttributeKey; + public fun principal (Lio/ktor/server/routing/RoutingContext;)Ljava/lang/Object; +} + +public final class io/ktor/server/auth/typesafe/OptionalAuthScheme : io/ktor/server/auth/typesafe/AuthScheme { + public fun (Lio/ktor/server/auth/typesafe/DefaultAuthScheme;)V + public synthetic fun createAuthenticatedContext (Lio/ktor/server/routing/Route;)Lio/ktor/server/auth/typesafe/AuthenticatedContext; + public fun createAuthenticatedContext (Lio/ktor/server/routing/Route;)Lio/ktor/server/auth/typesafe/OptionalAuthenticatedContext; + public final fun getBase ()Lio/ktor/server/auth/typesafe/DefaultAuthScheme; + public fun getName ()Ljava/lang/String; +} + +public final class io/ktor/server/auth/typesafe/OptionalAuthSchemeKt { + public static final fun optional (Lio/ktor/server/auth/typesafe/DefaultAuthScheme;)Lio/ktor/server/auth/typesafe/OptionalAuthScheme; + public static final fun optional (Lio/ktor/server/auth/typesafe/DefaultAuthScheme;Lkotlin/jvm/functions/Function2;)Lio/ktor/server/auth/typesafe/AnonymousAuthScheme; +} + +public final class io/ktor/server/auth/typesafe/OptionalAuthenticatedContext : io/ktor/server/auth/typesafe/AuthenticatedContext { + public fun principal (Lio/ktor/server/routing/RoutingContext;)Ljava/lang/Object; +} + +public final class io/ktor/server/auth/typesafe/RoleBasedAuthScheme : io/ktor/server/auth/typesafe/AuthScheme { + public synthetic fun createAuthenticatedContext (Lio/ktor/server/routing/Route;)Lio/ktor/server/auth/typesafe/AuthenticatedContext; + public fun createAuthenticatedContext (Lio/ktor/server/routing/Route;)Lio/ktor/server/auth/typesafe/RoleBasedContext; + public final fun getBase ()Lio/ktor/server/auth/typesafe/DefaultAuthScheme; + public fun getName ()Ljava/lang/String; +} + +public final class io/ktor/server/auth/typesafe/RoleBasedAuthSchemeKt { + public static final fun withRoles (Lio/ktor/server/auth/typesafe/DefaultAuthScheme;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;)Lio/ktor/server/auth/typesafe/RoleBasedAuthScheme; + public static synthetic fun withRoles$default (Lio/ktor/server/auth/typesafe/DefaultAuthScheme;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/ktor/server/auth/typesafe/RoleBasedAuthScheme; +} + +public final class io/ktor/server/auth/typesafe/RoleBasedContext : io/ktor/server/auth/typesafe/DefaultAuthenticatedContext { + public final fun roles (Lio/ktor/server/routing/RoutingContext;)Ljava/util/Set; +} + +public final class io/ktor/server/auth/typesafe/RouteBuildersKt { + public static final fun authenticateWith (Lio/ktor/server/routing/Route;Lio/ktor/server/auth/typesafe/AnonymousAuthScheme;Lkotlin/jvm/functions/Function2;)Lio/ktor/server/routing/Route; + public static final fun authenticateWith (Lio/ktor/server/routing/Route;Lio/ktor/server/auth/typesafe/DefaultAuthScheme;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;)Lio/ktor/server/routing/Route; + public static final fun authenticateWith (Lio/ktor/server/routing/Route;Lio/ktor/server/auth/typesafe/OptionalAuthScheme;Lkotlin/jvm/functions/Function2;)Lio/ktor/server/routing/Route; + public static final fun authenticateWith (Lio/ktor/server/routing/Route;Lio/ktor/server/auth/typesafe/RoleBasedAuthScheme;Ljava/util/Set;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;)Lio/ktor/server/routing/Route; + public static synthetic fun authenticateWith$default (Lio/ktor/server/routing/Route;Lio/ktor/server/auth/typesafe/DefaultAuthScheme;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/ktor/server/routing/Route; + public static synthetic fun authenticateWith$default (Lio/ktor/server/routing/Route;Lio/ktor/server/auth/typesafe/RoleBasedAuthScheme;Ljava/util/Set;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/ktor/server/routing/Route; + public static final fun authenticateWithAnyOf (Lio/ktor/server/routing/Route;Ljava/util/List;Lio/ktor/util/reflect/TypeInfo;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;)Lio/ktor/server/routing/Route; + public static synthetic fun authenticateWithAnyOf$default (Lio/ktor/server/routing/Route;Ljava/util/List;Lio/ktor/util/reflect/TypeInfo;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/ktor/server/routing/Route; +} + +public final class io/ktor/server/auth/typesafe/ScopesKt { + public static final fun authenticatedContext (Lio/ktor/server/auth/typesafe/AuthenticatedContext;)Lio/ktor/server/auth/typesafe/AuthenticatedContext; + public static final fun clearSession (Lio/ktor/server/auth/typesafe/SessionAuthenticatedContext;Lio/ktor/server/routing/RoutingContext;)V + public static final fun getPrincipal (Lio/ktor/server/auth/typesafe/AuthenticatedContext;Lio/ktor/server/routing/RoutingContext;)Ljava/lang/Object; + public static final fun getRoles (Lio/ktor/server/auth/typesafe/RoleBasedContext;Lio/ktor/server/routing/RoutingContext;)Ljava/util/Set; + public static final fun getSession (Lio/ktor/server/auth/typesafe/SessionAuthenticatedContext;Lio/ktor/server/routing/RoutingContext;)Ljava/lang/Object; + public static final fun setAuthenticatedSession (Lio/ktor/server/auth/typesafe/SessionAuthenticatedContext;Lio/ktor/server/routing/RoutingContext;Ljava/lang/Object;)V + public static final fun setSession (Lio/ktor/server/auth/typesafe/SessionAuthenticatedContext;Lio/ktor/server/routing/RoutingContext;Ljava/lang/Object;)V + public static final fun updateSession (Lio/ktor/server/auth/typesafe/SessionAuthenticatedContext;Lio/ktor/server/routing/RoutingContext;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public final class io/ktor/server/auth/typesafe/SessionAuthScheme : io/ktor/server/auth/typesafe/DefaultAuthScheme { +} + +public final class io/ktor/server/auth/typesafe/SessionAuthenticatedContext : io/ktor/server/auth/typesafe/AuthenticatedContext { + public fun (Lio/ktor/server/auth/typesafe/DefaultAuthenticatedContext;Lio/ktor/util/AttributeKey;Ljava/lang/String;)V + public final fun clearSession (Lio/ktor/server/routing/RoutingContext;)V + public fun principal (Lio/ktor/server/routing/RoutingContext;)Ljava/lang/Object; + public final fun session (Lio/ktor/server/routing/RoutingContext;)Ljava/lang/Object; + public final fun setSession (Lio/ktor/server/routing/RoutingContext;Ljava/lang/Object;)V + public final fun updateSession (Lio/ktor/server/routing/RoutingContext;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public final class io/ktor/server/auth/typesafe/SessionsConfigTypesafeExtensionsKt { + public static final fun cookie (Lio/ktor/server/sessions/SessionsConfig;Lio/ktor/server/auth/typesafe/SessionAuthScheme;)V + public static final fun cookie (Lio/ktor/server/sessions/SessionsConfig;Lio/ktor/server/auth/typesafe/SessionAuthScheme;Lio/ktor/server/sessions/SessionStorage;)V + public static final fun cookie (Lio/ktor/server/sessions/SessionsConfig;Lio/ktor/server/auth/typesafe/SessionAuthScheme;Lio/ktor/server/sessions/SessionStorage;Lkotlin/jvm/functions/Function1;)V + public static final fun cookie (Lio/ktor/server/sessions/SessionsConfig;Lio/ktor/server/auth/typesafe/SessionAuthScheme;Lkotlin/jvm/functions/Function1;)V + public static final fun header (Lio/ktor/server/sessions/SessionsConfig;Lio/ktor/server/auth/typesafe/SessionAuthScheme;)V + public static final fun header (Lio/ktor/server/sessions/SessionsConfig;Lio/ktor/server/auth/typesafe/SessionAuthScheme;Lio/ktor/server/sessions/SessionStorage;)V + public static final fun header (Lio/ktor/server/sessions/SessionsConfig;Lio/ktor/server/auth/typesafe/SessionAuthScheme;Lio/ktor/server/sessions/SessionStorage;Lkotlin/jvm/functions/Function1;)V + public static final fun header (Lio/ktor/server/sessions/SessionsConfig;Lio/ktor/server/auth/typesafe/SessionAuthScheme;Lkotlin/jvm/functions/Function1;)V + public static final fun set (Lio/ktor/server/sessions/CurrentSession;Lio/ktor/server/auth/typesafe/SessionAuthScheme;Ljava/lang/Object;)V +} + +public final class io/ktor/server/auth/typesafe/TypedBasicAuthConfig { + public fun ()V + public final fun buildProvider (Ljava/lang/String;)Lio/ktor/server/auth/BasicAuthenticationProvider; + public final fun getCharset ()Ljava/nio/charset/Charset; + 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 setCharset (Ljava/nio/charset/Charset;)V + 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 class io/ktor/server/auth/typesafe/TypedBearerAuthConfig { + public fun ()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/typesafe/TypedBearerAuthConfig;Ljava/lang/String;[Ljava/lang/String;ILjava/lang/Object;)V + public final fun authenticate (Lkotlin/jvm/functions/Function3;)V + public final fun buildProvider (Ljava/lang/String;)Lio/ktor/server/auth/BearerAuthenticationProvider; + 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 class io/ktor/server/auth/typesafe/TypedDigestAuthConfig { + public fun ()V + public final fun buildProvider (Ljava/lang/String;)Lio/ktor/server/auth/DigestAuthenticationProvider; + public final fun digestProvider (Lkotlin/jvm/functions/Function3;)V + public final fun digestProvider (Lkotlin/jvm/functions/Function4;)V + public final fun getAlgorithms ()Ljava/util/List; + public final fun getCharset ()Ljava/nio/charset/Charset; + public final fun getDescription ()Ljava/lang/String; + public final fun getNonceManager ()Lio/ktor/util/NonceManager; + public final fun getOnUnauthorized ()Lkotlin/jvm/functions/Function3; + public final fun getRealm ()Ljava/lang/String; + public final fun getSupportedQop ()Ljava/util/List; + public final fun setAlgorithms (Ljava/util/List;)V + public final fun setCharset (Ljava/nio/charset/Charset;)V + public final fun setDescription (Ljava/lang/String;)V + public final fun setNonceManager (Lio/ktor/util/NonceManager;)V + public final fun setOnUnauthorized (Lkotlin/jvm/functions/Function3;)V + public final fun setRealm (Ljava/lang/String;)V + public final fun setSupportedQop (Ljava/util/List;)V + public final fun strictRfc7616Mode ()V + public final fun userHashResolver (Lkotlin/jvm/functions/Function4;)V + public final fun validate (Lkotlin/jvm/functions/Function3;)V +} + +public final class io/ktor/server/auth/typesafe/TypedFormAuthConfig { + public fun ()V + public final fun buildProvider (Ljava/lang/String;)Lio/ktor/server/auth/FormAuthenticationProvider; + public final fun getDescription ()Ljava/lang/String; + public final fun getOnUnauthorized ()Lkotlin/jvm/functions/Function3; + public final fun getPasswordParamName ()Ljava/lang/String; + public final fun getUserParamName ()Ljava/lang/String; + public final fun setDescription (Ljava/lang/String;)V + public final fun setOnUnauthorized (Lkotlin/jvm/functions/Function3;)V + public final fun setPasswordParamName (Ljava/lang/String;)V + public final fun setUserParamName (Ljava/lang/String;)V + public final fun validate (Lkotlin/jvm/functions/Function3;)V +} + +public final class io/ktor/server/auth/typesafe/TypedSessionAuthConfig { + public fun ()V + public final fun buildProvider (Ljava/lang/String;Lkotlin/reflect/KClass;Lio/ktor/util/AttributeKey;)Lio/ktor/server/auth/SessionAuthenticationProvider; + public final fun getDescription ()Ljava/lang/String; + public final fun getOnUnauthorized ()Lkotlin/jvm/functions/Function3; + public final fun setDescription (Ljava/lang/String;)V + public final fun setOnUnauthorized (Lkotlin/jvm/functions/Function3;)V + public final fun validate (Lkotlin/jvm/functions/Function3;)V +} + diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.klib.api b/ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.klib.api index b1c0a458fb4..3cfeec3894e 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.klib.api +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.klib.api @@ -29,6 +29,22 @@ final enum class io.ktor.server.auth/OAuthVersion : kotlin/Enum // io.ktor.server.auth/OAuthVersion.values|values#static(){}[0] } +abstract interface <#A: kotlin/Any, #B: io.ktor.server.auth.typesafe/AuthenticatedContext<*>> io.ktor.server.auth.typesafe/AuthScheme { // io.ktor.server.auth.typesafe/AuthScheme|null[0] + abstract val name // io.ktor.server.auth.typesafe/AuthScheme.name|{}name[0] + abstract fun (): kotlin/String // io.ktor.server.auth.typesafe/AuthScheme.name.|(){}[0] + + abstract fun createAuthenticatedContext(io.ktor.server.routing/Route): #B // io.ktor.server.auth.typesafe/AuthScheme.createAuthenticatedContext|createAuthenticatedContext(io.ktor.server.routing.Route){}[0] +} + +abstract interface <#A: kotlin/Any?> io.ktor.server.auth.typesafe/AuthenticatedContext { // io.ktor.server.auth.typesafe/AuthenticatedContext|null[0] + abstract fun principal(io.ktor.server.routing/RoutingContext): #A // io.ktor.server.auth.typesafe/AuthenticatedContext.principal|principal(io.ktor.server.routing.RoutingContext){}[0] +} + +abstract interface io.ktor.server.auth.typesafe/AuthRole { // io.ktor.server.auth.typesafe/AuthRole|null[0] + abstract val name // io.ktor.server.auth.typesafe/AuthRole.name|{}name[0] + abstract fun (): kotlin/String // io.ktor.server.auth.typesafe/AuthRole.name.|(){}[0] +} + abstract interface io.ktor.server.auth/Credential // io.ktor.server.auth/Credential|null[0] abstract interface io.ktor.server.auth/Principal // io.ktor.server.auth/Principal|null[0] @@ -58,6 +74,130 @@ abstract class io.ktor.server.auth/AuthenticationProvider { // io.ktor.server.au } } +final class <#A: kotlin/Any, #B: #A, #C: #A> io.ktor.server.auth.typesafe/AnonymousAuthScheme : io.ktor.server.auth.typesafe/AuthScheme<#B, io.ktor.server.auth.typesafe/DefaultAuthenticatedContext<#A>> { // io.ktor.server.auth.typesafe/AnonymousAuthScheme|null[0] + constructor (io.ktor.server.auth.typesafe/DefaultAuthScheme<#B, *>, kotlin.coroutines/SuspendFunction1) // io.ktor.server.auth.typesafe/AnonymousAuthScheme.|(io.ktor.server.auth.typesafe.DefaultAuthScheme<1:1,*>;kotlin.coroutines.SuspendFunction1){}[0] + + final val base // io.ktor.server.auth.typesafe/AnonymousAuthScheme.base|{}base[0] + final fun (): io.ktor.server.auth.typesafe/DefaultAuthScheme<#B, *> // io.ktor.server.auth.typesafe/AnonymousAuthScheme.base.|(){}[0] + final val name // io.ktor.server.auth.typesafe/AnonymousAuthScheme.name|{}name[0] + final fun (): kotlin/String // io.ktor.server.auth.typesafe/AnonymousAuthScheme.name.|(){}[0] + + final fun createAuthenticatedContext(io.ktor.server.routing/Route): io.ktor.server.auth.typesafe/DefaultAuthenticatedContext<#A> // io.ktor.server.auth.typesafe/AnonymousAuthScheme.createAuthenticatedContext|createAuthenticatedContext(io.ktor.server.routing.Route){}[0] +} + +final class <#A: kotlin/Any, #B: io.ktor.server.auth.typesafe/AuthRole> io.ktor.server.auth.typesafe/RoleBasedAuthScheme : io.ktor.server.auth.typesafe/AuthScheme<#A, io.ktor.server.auth.typesafe/RoleBasedContext<#A, #B>> { // io.ktor.server.auth.typesafe/RoleBasedAuthScheme|null[0] + final val base // io.ktor.server.auth.typesafe/RoleBasedAuthScheme.base|{}base[0] + final fun (): io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, *> // io.ktor.server.auth.typesafe/RoleBasedAuthScheme.base.|(){}[0] + final val name // io.ktor.server.auth.typesafe/RoleBasedAuthScheme.name|{}name[0] + final fun (): kotlin/String // io.ktor.server.auth.typesafe/RoleBasedAuthScheme.name.|(){}[0] + + final fun createAuthenticatedContext(io.ktor.server.routing/Route): io.ktor.server.auth.typesafe/RoleBasedContext<#A, #B> // io.ktor.server.auth.typesafe/RoleBasedAuthScheme.createAuthenticatedContext|createAuthenticatedContext(io.ktor.server.routing.Route){}[0] +} + +final class <#A: kotlin/Any, #B: io.ktor.server.auth.typesafe/AuthRole> io.ktor.server.auth.typesafe/RoleBasedContext : io.ktor.server.auth.typesafe/DefaultAuthenticatedContext<#A> { // io.ktor.server.auth.typesafe/RoleBasedContext|null[0] + final fun roles(io.ktor.server.routing/RoutingContext): kotlin.collections/Set<#B> // io.ktor.server.auth.typesafe/RoleBasedContext.roles|roles(io.ktor.server.routing.RoutingContext){}[0] +} + +final class <#A: kotlin/Any, #B: kotlin/Any> io.ktor.server.auth.typesafe/SessionAuthScheme : io.ktor.server.auth.typesafe/DefaultAuthScheme<#B, io.ktor.server.auth.typesafe/SessionAuthenticatedContext<#A, #B>> // io.ktor.server.auth.typesafe/SessionAuthScheme|null[0] + +final class <#A: kotlin/Any, #B: kotlin/Any> io.ktor.server.auth.typesafe/SessionAuthenticatedContext : io.ktor.server.auth.typesafe/AuthenticatedContext<#B> { // io.ktor.server.auth.typesafe/SessionAuthenticatedContext|null[0] + constructor (io.ktor.server.auth.typesafe/DefaultAuthenticatedContext<#B>, io.ktor.util/AttributeKey<#A>, kotlin/String) // io.ktor.server.auth.typesafe/SessionAuthenticatedContext.|(io.ktor.server.auth.typesafe.DefaultAuthenticatedContext<1:1>;io.ktor.util.AttributeKey<1:0>;kotlin.String){}[0] + + final fun clearSession(io.ktor.server.routing/RoutingContext) // io.ktor.server.auth.typesafe/SessionAuthenticatedContext.clearSession|clearSession(io.ktor.server.routing.RoutingContext){}[0] + final fun principal(io.ktor.server.routing/RoutingContext): #B // io.ktor.server.auth.typesafe/SessionAuthenticatedContext.principal|principal(io.ktor.server.routing.RoutingContext){}[0] + final fun session(io.ktor.server.routing/RoutingContext): #A // io.ktor.server.auth.typesafe/SessionAuthenticatedContext.session|session(io.ktor.server.routing.RoutingContext){}[0] + final fun setSession(io.ktor.server.routing/RoutingContext, #A) // io.ktor.server.auth.typesafe/SessionAuthenticatedContext.setSession|setSession(io.ktor.server.routing.RoutingContext;1:0){}[0] + final fun updateSession(io.ktor.server.routing/RoutingContext, kotlin/Function1<#A, #A>): #A // io.ktor.server.auth.typesafe/SessionAuthenticatedContext.updateSession|updateSession(io.ktor.server.routing.RoutingContext;kotlin.Function1<1:0,1:0>){}[0] +} + +final class <#A: kotlin/Any, #B: kotlin/Any> io.ktor.server.auth.typesafe/TypedSessionAuthConfig { // io.ktor.server.auth.typesafe/TypedSessionAuthConfig|null[0] + constructor () // io.ktor.server.auth.typesafe/TypedSessionAuthConfig.|(){}[0] + + final var description // io.ktor.server.auth.typesafe/TypedSessionAuthConfig.description|{}description[0] + final fun (): kotlin/String? // io.ktor.server.auth.typesafe/TypedSessionAuthConfig.description.|(){}[0] + final fun (kotlin/String?) // io.ktor.server.auth.typesafe/TypedSessionAuthConfig.description.|(kotlin.String?){}[0] + final var onUnauthorized // io.ktor.server.auth.typesafe/TypedSessionAuthConfig.onUnauthorized|{}onUnauthorized[0] + final fun (): kotlin.coroutines/SuspendFunction2? // io.ktor.server.auth.typesafe/TypedSessionAuthConfig.onUnauthorized.|(){}[0] + final fun (kotlin.coroutines/SuspendFunction2?) // io.ktor.server.auth.typesafe/TypedSessionAuthConfig.onUnauthorized.|(kotlin.coroutines.SuspendFunction2?){}[0] + + final fun buildProvider(kotlin/String, kotlin.reflect/KClass<#A>, io.ktor.util/AttributeKey<#A>): io.ktor.server.auth/SessionAuthenticationProvider<#A> // io.ktor.server.auth.typesafe/TypedSessionAuthConfig.buildProvider|buildProvider(kotlin.String;kotlin.reflect.KClass<1:0>;io.ktor.util.AttributeKey<1:0>){}[0] + final fun validate(kotlin.coroutines/SuspendFunction2) // io.ktor.server.auth.typesafe/TypedSessionAuthConfig.validate|validate(kotlin.coroutines.SuspendFunction2){}[0] +} + +final class <#A: kotlin/Any> io.ktor.server.auth.typesafe/OptionalAuthScheme : io.ktor.server.auth.typesafe/AuthScheme<#A, io.ktor.server.auth.typesafe/OptionalAuthenticatedContext<#A>> { // io.ktor.server.auth.typesafe/OptionalAuthScheme|null[0] + constructor (io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, *>) // io.ktor.server.auth.typesafe/OptionalAuthScheme.|(io.ktor.server.auth.typesafe.DefaultAuthScheme<1:0,*>){}[0] + + final val base // io.ktor.server.auth.typesafe/OptionalAuthScheme.base|{}base[0] + final fun (): io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, *> // io.ktor.server.auth.typesafe/OptionalAuthScheme.base.|(){}[0] + final val name // io.ktor.server.auth.typesafe/OptionalAuthScheme.name|{}name[0] + final fun (): kotlin/String // io.ktor.server.auth.typesafe/OptionalAuthScheme.name.|(){}[0] + + final fun createAuthenticatedContext(io.ktor.server.routing/Route): io.ktor.server.auth.typesafe/OptionalAuthenticatedContext<#A> // io.ktor.server.auth.typesafe/OptionalAuthScheme.createAuthenticatedContext|createAuthenticatedContext(io.ktor.server.routing.Route){}[0] +} + +final class <#A: kotlin/Any> io.ktor.server.auth.typesafe/OptionalAuthenticatedContext : io.ktor.server.auth.typesafe/AuthenticatedContext<#A?> { // io.ktor.server.auth.typesafe/OptionalAuthenticatedContext|null[0] + final fun principal(io.ktor.server.routing/RoutingContext): #A? // io.ktor.server.auth.typesafe/OptionalAuthenticatedContext.principal|principal(io.ktor.server.routing.RoutingContext){}[0] +} + +final class <#A: kotlin/Any> io.ktor.server.auth.typesafe/TypedBasicAuthConfig { // io.ktor.server.auth.typesafe/TypedBasicAuthConfig|null[0] + constructor () // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.|(){}[0] + + final var charset // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.charset|{}charset[0] + final fun (): io.ktor.utils.io.charsets/Charset? // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.charset.|(){}[0] + final fun (io.ktor.utils.io.charsets/Charset?) // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.charset.|(io.ktor.utils.io.charsets.Charset?){}[0] + final var description // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.description|{}description[0] + final fun (): kotlin/String? // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.description.|(){}[0] + final fun (kotlin/String?) // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.description.|(kotlin.String?){}[0] + final var onUnauthorized // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.onUnauthorized|{}onUnauthorized[0] + final fun (): kotlin.coroutines/SuspendFunction2? // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.onUnauthorized.|(){}[0] + final fun (kotlin.coroutines/SuspendFunction2?) // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.onUnauthorized.|(kotlin.coroutines.SuspendFunction2?){}[0] + final var realm // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.realm|{}realm[0] + final fun (): kotlin/String // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.realm.|(){}[0] + final fun (kotlin/String) // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.realm.|(kotlin.String){}[0] + + final fun buildProvider(kotlin/String): io.ktor.server.auth/BasicAuthenticationProvider // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.buildProvider|buildProvider(kotlin.String){}[0] + final fun validate(kotlin.coroutines/SuspendFunction2) // io.ktor.server.auth.typesafe/TypedBasicAuthConfig.validate|validate(kotlin.coroutines.SuspendFunction2){}[0] +} + +final class <#A: kotlin/Any> io.ktor.server.auth.typesafe/TypedBearerAuthConfig { // io.ktor.server.auth.typesafe/TypedBearerAuthConfig|null[0] + constructor () // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.|(){}[0] + + final var description // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.description|{}description[0] + final fun (): kotlin/String? // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.description.|(){}[0] + final fun (kotlin/String?) // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.description.|(kotlin.String?){}[0] + final var onUnauthorized // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.onUnauthorized|{}onUnauthorized[0] + final fun (): kotlin.coroutines/SuspendFunction2? // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.onUnauthorized.|(){}[0] + final fun (kotlin.coroutines/SuspendFunction2?) // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.onUnauthorized.|(kotlin.coroutines.SuspendFunction2?){}[0] + final var realm // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.realm|{}realm[0] + final fun (): kotlin/String? // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.realm.|(){}[0] + final fun (kotlin/String?) // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.realm.|(kotlin.String?){}[0] + + final fun authHeader(kotlin/Function1) // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.authHeader|authHeader(kotlin.Function1){}[0] + final fun authSchemes(kotlin/String = ..., kotlin/Array...) // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.authSchemes|authSchemes(kotlin.String;kotlin.Array...){}[0] + final fun authenticate(kotlin.coroutines/SuspendFunction2) // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.authenticate|authenticate(kotlin.coroutines.SuspendFunction2){}[0] + final fun buildProvider(kotlin/String): io.ktor.server.auth/BearerAuthenticationProvider // io.ktor.server.auth.typesafe/TypedBearerAuthConfig.buildProvider|buildProvider(kotlin.String){}[0] +} + +final class <#A: kotlin/Any> io.ktor.server.auth.typesafe/TypedFormAuthConfig { // io.ktor.server.auth.typesafe/TypedFormAuthConfig|null[0] + constructor () // io.ktor.server.auth.typesafe/TypedFormAuthConfig.|(){}[0] + + final var description // io.ktor.server.auth.typesafe/TypedFormAuthConfig.description|{}description[0] + final fun (): kotlin/String? // io.ktor.server.auth.typesafe/TypedFormAuthConfig.description.|(){}[0] + final fun (kotlin/String?) // io.ktor.server.auth.typesafe/TypedFormAuthConfig.description.|(kotlin.String?){}[0] + final var onUnauthorized // io.ktor.server.auth.typesafe/TypedFormAuthConfig.onUnauthorized|{}onUnauthorized[0] + final fun (): kotlin.coroutines/SuspendFunction2? // io.ktor.server.auth.typesafe/TypedFormAuthConfig.onUnauthorized.|(){}[0] + final fun (kotlin.coroutines/SuspendFunction2?) // io.ktor.server.auth.typesafe/TypedFormAuthConfig.onUnauthorized.|(kotlin.coroutines.SuspendFunction2?){}[0] + final var passwordParamName // io.ktor.server.auth.typesafe/TypedFormAuthConfig.passwordParamName|{}passwordParamName[0] + final fun (): kotlin/String // io.ktor.server.auth.typesafe/TypedFormAuthConfig.passwordParamName.|(){}[0] + final fun (kotlin/String) // io.ktor.server.auth.typesafe/TypedFormAuthConfig.passwordParamName.|(kotlin.String){}[0] + final var userParamName // io.ktor.server.auth.typesafe/TypedFormAuthConfig.userParamName|{}userParamName[0] + final fun (): kotlin/String // io.ktor.server.auth.typesafe/TypedFormAuthConfig.userParamName.|(){}[0] + final fun (kotlin/String) // io.ktor.server.auth.typesafe/TypedFormAuthConfig.userParamName.|(kotlin.String){}[0] + + final fun buildProvider(kotlin/String): io.ktor.server.auth/FormAuthenticationProvider // io.ktor.server.auth.typesafe/TypedFormAuthConfig.buildProvider|buildProvider(kotlin.String){}[0] + final fun validate(kotlin.coroutines/SuspendFunction2) // io.ktor.server.auth.typesafe/TypedFormAuthConfig.validate|validate(kotlin.coroutines.SuspendFunction2){}[0] +} + final class <#A: kotlin/Any> io.ktor.server.auth/SessionAuthenticationProvider : io.ktor.server.auth/AuthenticationProvider { // io.ktor.server.auth/SessionAuthenticationProvider|null[0] final val type // io.ktor.server.auth/SessionAuthenticationProvider.type|{}type[0] final fun (): kotlin.reflect/KClass<#A> // io.ktor.server.auth/SessionAuthenticationProvider.type.|(){}[0] @@ -344,6 +484,28 @@ final class io.ktor.server.auth/UserPasswordCredential { // io.ktor.server.auth/ final fun toString(): kotlin/String // io.ktor.server.auth/UserPasswordCredential.toString|toString(){}[0] } +open class <#A: kotlin/Any, #B: io.ktor.server.auth.typesafe/AuthenticatedContext<#A>> io.ktor.server.auth.typesafe/DefaultAuthScheme : io.ktor.server.auth.typesafe/AuthScheme<#A, #B> { // io.ktor.server.auth.typesafe/DefaultAuthScheme|null[0] + constructor (kotlin/String, kotlin.reflect/KClass<#A>, io.ktor.server.auth/AuthenticationProvider, kotlin.coroutines/SuspendFunction2?, kotlin/Function1, #B>) // io.ktor.server.auth.typesafe/DefaultAuthScheme.|(kotlin.String;kotlin.reflect.KClass<1:0>;io.ktor.server.auth.AuthenticationProvider;kotlin.coroutines.SuspendFunction2?;kotlin.Function1,1:1>){}[0] + + open val name // io.ktor.server.auth.typesafe/DefaultAuthScheme.name|{}name[0] + open fun (): kotlin/String // io.ktor.server.auth.typesafe/DefaultAuthScheme.name.|(){}[0] + + open fun createAuthenticatedContext(io.ktor.server.routing/Route): #B // io.ktor.server.auth.typesafe/DefaultAuthScheme.createAuthenticatedContext|createAuthenticatedContext(io.ktor.server.routing.Route){}[0] + + final object Companion { // io.ktor.server.auth.typesafe/DefaultAuthScheme.Companion|null[0] + final inline fun <#A2: reified kotlin/Any> withDefaultContext(kotlin/String, io.ktor.server.auth/AuthenticationProvider, noinline kotlin.coroutines/SuspendFunction2?): io.ktor.server.auth.typesafe/DefaultAuthScheme<#A2, io.ktor.server.auth.typesafe/DefaultAuthenticatedContext<#A2>> // io.ktor.server.auth.typesafe/DefaultAuthScheme.Companion.withDefaultContext|withDefaultContext(kotlin.String;io.ktor.server.auth.AuthenticationProvider;kotlin.coroutines.SuspendFunction2?){0§}[0] + } +} + +open class <#A: kotlin/Any> io.ktor.server.auth.typesafe/DefaultAuthenticatedContext : io.ktor.server.auth.typesafe/AuthenticatedContext<#A> { // io.ktor.server.auth.typesafe/DefaultAuthenticatedContext|null[0] + constructor (io.ktor.util/AttributeKey<#A>) // io.ktor.server.auth.typesafe/DefaultAuthenticatedContext.|(io.ktor.util.AttributeKey<1:0>){}[0] + + final val principalKey // io.ktor.server.auth.typesafe/DefaultAuthenticatedContext.principalKey|{}principalKey[0] + final fun (): io.ktor.util/AttributeKey<#A> // io.ktor.server.auth.typesafe/DefaultAuthenticatedContext.principalKey.|(){}[0] + + open fun principal(io.ktor.server.routing/RoutingContext): #A // io.ktor.server.auth.typesafe/DefaultAuthenticatedContext.principal|principal(io.ktor.server.routing.RoutingContext){}[0] +} + sealed class io.ktor.server.auth/AuthenticationFailedCause { // io.ktor.server.auth/AuthenticationFailedCause|null[0] open class Error : io.ktor.server.auth/AuthenticationFailedCause { // io.ktor.server.auth/AuthenticationFailedCause.Error|null[0] constructor (kotlin/String) // io.ktor.server.auth/AuthenticationFailedCause.Error.|(kotlin.String){}[0] @@ -615,6 +777,10 @@ final object io.ktor.server.auth/OAuthGrantTypes { // io.ktor.server.auth/OAuthG final const val io.ktor.server.auth/SessionAuthChallengeKey // io.ktor.server.auth/SessionAuthChallengeKey|{}SessionAuthChallengeKey[0] final fun (): kotlin/String // io.ktor.server.auth/SessionAuthChallengeKey.|(){}[0] +final val io.ktor.server.auth.typesafe/principal // io.ktor.server.auth.typesafe/principal|(io.ktor.server.auth.typesafe.AuthenticatedContext<0:0>)@io.ktor.server.routing.RoutingContext{0§}principal[0] + final fun <#A1: kotlin/Any?> (context(io.ktor.server.auth.typesafe/AuthenticatedContext<#A1>), io.ktor.server.routing/RoutingContext).(): #A1 // io.ktor.server.auth.typesafe/principal.|(io.ktor.server.auth.typesafe.AuthenticatedContext<0:0>)@io.ktor.server.routing.RoutingContext(){0§}[0] +final val io.ktor.server.auth.typesafe/roles // io.ktor.server.auth.typesafe/roles|(io.ktor.server.auth.typesafe.RoleBasedContext<*,0:0>)@io.ktor.server.routing.RoutingContext{0§}roles[0] + final fun <#A1: io.ktor.server.auth.typesafe/AuthRole> (context(io.ktor.server.auth.typesafe/RoleBasedContext<*, #A1>), io.ktor.server.routing/RoutingContext).(): kotlin.collections/Set<#A1> // io.ktor.server.auth.typesafe/roles.|(io.ktor.server.auth.typesafe.RoleBasedContext<*,0:0>)@io.ktor.server.routing.RoutingContext(){0§}[0] final val io.ktor.server.auth/AuthenticateProvidersKey // io.ktor.server.auth/AuthenticateProvidersKey|{}AuthenticateProvidersKey[0] final fun (): io.ktor.util/AttributeKey // io.ktor.server.auth/AuthenticateProvidersKey.|(){}[0] final val io.ktor.server.auth/AuthenticationInterceptors // io.ktor.server.auth/AuthenticationInterceptors|{}AuthenticationInterceptors[0] @@ -624,6 +790,10 @@ final val io.ktor.server.auth/OAuthKey // io.ktor.server.auth/OAuthKey|{}OAuthKe final val io.ktor.server.auth/authentication // io.ktor.server.auth/authentication|@io.ktor.server.application.ApplicationCall{}authentication[0] final fun (io.ktor.server.application/ApplicationCall).(): io.ktor.server.auth/AuthenticationContext // io.ktor.server.auth/authentication.|@io.ktor.server.application.ApplicationCall(){}[0] +final var io.ktor.server.auth.typesafe/session // io.ktor.server.auth.typesafe/session|(io.ktor.server.auth.typesafe.SessionAuthenticatedContext<0:0,*>)@io.ktor.server.routing.RoutingContext{0§}session[0] + final fun <#A1: kotlin/Any> (context(io.ktor.server.auth.typesafe/SessionAuthenticatedContext<#A1, *>), io.ktor.server.routing/RoutingContext).(): #A1 // io.ktor.server.auth.typesafe/session.|(io.ktor.server.auth.typesafe.SessionAuthenticatedContext<0:0,*>)@io.ktor.server.routing.RoutingContext(){0§}[0] + final fun <#A1: kotlin/Any> (context(io.ktor.server.auth.typesafe/SessionAuthenticatedContext<#A1, *>), io.ktor.server.routing/RoutingContext).(#A1) // io.ktor.server.auth.typesafe/session.|(io.ktor.server.auth.typesafe.SessionAuthenticatedContext<0:0,*>)@io.ktor.server.routing.RoutingContext(0:0){0§}[0] + final fun (io.ktor.server.application/Application).io.ktor.server.auth/authentication(kotlin/Function1) // io.ktor.server.auth/authentication|authentication@io.ktor.server.application.Application(kotlin.Function1){}[0] final fun (io.ktor.server.auth/Authentication).io.ktor.server.auth/configuration(): io.ktor.server.auth/AuthenticationConfig // io.ktor.server.auth/configuration|configuration@io.ktor.server.auth.Authentication(){}[0] final fun (io.ktor.server.auth/AuthenticationConfig).io.ktor.server.auth/allProviders(): kotlin.collections/Map // io.ktor.server.auth/allProviders|allProviders@io.ktor.server.auth.AuthenticationConfig(){}[0] @@ -640,11 +810,39 @@ final fun (io.ktor.server.request/ApplicationRequest).io.ktor.server.auth/basicA final fun (io.ktor.server.request/ApplicationRequest).io.ktor.server.auth/parseAuthorizationHeader(): io.ktor.http.auth/HttpAuthHeader? // io.ktor.server.auth/parseAuthorizationHeader|parseAuthorizationHeader@io.ktor.server.request.ApplicationRequest(){}[0] final fun (io.ktor.server.routing/Route).io.ktor.server.auth/authenticate(kotlin/Array... = ..., io.ktor.server.auth/AuthenticationStrategy, kotlin/Function1): io.ktor.server.routing/Route // io.ktor.server.auth/authenticate|authenticate@io.ktor.server.routing.Route(kotlin.Array...;io.ktor.server.auth.AuthenticationStrategy;kotlin.Function1){}[0] final fun (io.ktor.server.routing/Route).io.ktor.server.auth/authenticate(kotlin/Array... = ..., kotlin/Boolean = ..., kotlin/Function1): io.ktor.server.routing/Route // io.ktor.server.auth/authenticate|authenticate@io.ktor.server.routing.Route(kotlin.Array...;kotlin.Boolean;kotlin.Function1){}[0] +final fun <#A: kotlin/Any, #B: #A, #C: #A> (io.ktor.server.auth.typesafe/DefaultAuthScheme<#B, *>).io.ktor.server.auth.typesafe/optional(kotlin.coroutines/SuspendFunction1): io.ktor.server.auth.typesafe/AnonymousAuthScheme<#A, #B, #C> // io.ktor.server.auth.typesafe/optional|optional@io.ktor.server.auth.typesafe.DefaultAuthScheme<0:1,*>(kotlin.coroutines.SuspendFunction1){0§;1§<0:0>;2§<0:0>}[0] +final fun <#A: kotlin/Any, #B: #A, #C: #A> (io.ktor.server.routing/Route).io.ktor.server.auth.typesafe/authenticateWith(io.ktor.server.auth.typesafe/AnonymousAuthScheme<#A, #B, #C>, kotlin/Function2, io.ktor.server.routing/Route, kotlin/Unit>): io.ktor.server.routing/Route // io.ktor.server.auth.typesafe/authenticateWith|authenticateWith@io.ktor.server.routing.Route(io.ktor.server.auth.typesafe.AnonymousAuthScheme<0:0,0:1,0:2>;kotlin.Function2,io.ktor.server.routing.Route,kotlin.Unit>){0§;1§<0:0>;2§<0:0>}[0] +final fun <#A: kotlin/Any, #B: io.ktor.server.auth.typesafe/AuthRole> (io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, *>).io.ktor.server.auth.typesafe/withRoles(kotlin.coroutines/SuspendFunction2, kotlin/Unit> = ..., kotlin.coroutines/SuspendFunction2>): io.ktor.server.auth.typesafe/RoleBasedAuthScheme<#A, #B> // io.ktor.server.auth.typesafe/withRoles|withRoles@io.ktor.server.auth.typesafe.DefaultAuthScheme<0:0,*>(kotlin.coroutines.SuspendFunction2,kotlin.Unit>;kotlin.coroutines.SuspendFunction2>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: io.ktor.server.auth.typesafe/AuthRole> (io.ktor.server.routing/Route).io.ktor.server.auth.typesafe/authenticateWith(io.ktor.server.auth.typesafe/RoleBasedAuthScheme<#A, #B>, kotlin.collections/Set<#B>, kotlin.coroutines/SuspendFunction2? = ..., kotlin.coroutines/SuspendFunction2, kotlin/Unit>? = ..., kotlin/Function2, io.ktor.server.routing/Route, kotlin/Unit>): io.ktor.server.routing/Route // io.ktor.server.auth.typesafe/authenticateWith|authenticateWith@io.ktor.server.routing.Route(io.ktor.server.auth.typesafe.RoleBasedAuthScheme<0:0,0:1>;kotlin.collections.Set<0:1>;kotlin.coroutines.SuspendFunction2?;kotlin.coroutines.SuspendFunction2,kotlin.Unit>?;kotlin.Function2,io.ktor.server.routing.Route,kotlin.Unit>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: io.ktor.server.auth.typesafe/AuthenticatedContext<#A>> (io.ktor.server.routing/Route).io.ktor.server.auth.typesafe/authenticateWith(io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, #B>, kotlin.coroutines/SuspendFunction2? = ..., kotlin/Function2<#B, io.ktor.server.routing/Route, kotlin/Unit>): io.ktor.server.routing/Route // io.ktor.server.auth.typesafe/authenticateWith|authenticateWith@io.ktor.server.routing.Route(io.ktor.server.auth.typesafe.DefaultAuthScheme<0:0,0:1>;kotlin.coroutines.SuspendFunction2?;kotlin.Function2<0:1,io.ktor.server.routing.Route,kotlin.Unit>){0§;1§>}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> (io.ktor.server.sessions/CurrentSession).io.ktor.server.auth.typesafe/set(io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B>, #A) // io.ktor.server.auth.typesafe/set|set@io.ktor.server.sessions.CurrentSession(io.ktor.server.auth.typesafe.SessionAuthScheme<0:0,0:1>;0:0){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> (io.ktor.server.sessions/SessionsConfig).io.ktor.server.auth.typesafe/cookie(io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B>) // io.ktor.server.auth.typesafe/cookie|cookie@io.ktor.server.sessions.SessionsConfig(io.ktor.server.auth.typesafe.SessionAuthScheme<0:0,0:1>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> (io.ktor.server.sessions/SessionsConfig).io.ktor.server.auth.typesafe/cookie(io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B>, io.ktor.server.sessions/SessionStorage) // io.ktor.server.auth.typesafe/cookie|cookie@io.ktor.server.sessions.SessionsConfig(io.ktor.server.auth.typesafe.SessionAuthScheme<0:0,0:1>;io.ktor.server.sessions.SessionStorage){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> (io.ktor.server.sessions/SessionsConfig).io.ktor.server.auth.typesafe/cookie(io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B>, io.ktor.server.sessions/SessionStorage, kotlin/Function1, kotlin/Unit>) // io.ktor.server.auth.typesafe/cookie|cookie@io.ktor.server.sessions.SessionsConfig(io.ktor.server.auth.typesafe.SessionAuthScheme<0:0,0:1>;io.ktor.server.sessions.SessionStorage;kotlin.Function1,kotlin.Unit>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> (io.ktor.server.sessions/SessionsConfig).io.ktor.server.auth.typesafe/cookie(io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B>, kotlin/Function1, kotlin/Unit>) // io.ktor.server.auth.typesafe/cookie|cookie@io.ktor.server.sessions.SessionsConfig(io.ktor.server.auth.typesafe.SessionAuthScheme<0:0,0:1>;kotlin.Function1,kotlin.Unit>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> (io.ktor.server.sessions/SessionsConfig).io.ktor.server.auth.typesafe/header(io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B>) // io.ktor.server.auth.typesafe/header|header@io.ktor.server.sessions.SessionsConfig(io.ktor.server.auth.typesafe.SessionAuthScheme<0:0,0:1>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> (io.ktor.server.sessions/SessionsConfig).io.ktor.server.auth.typesafe/header(io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B>, io.ktor.server.sessions/SessionStorage) // io.ktor.server.auth.typesafe/header|header@io.ktor.server.sessions.SessionsConfig(io.ktor.server.auth.typesafe.SessionAuthScheme<0:0,0:1>;io.ktor.server.sessions.SessionStorage){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> (io.ktor.server.sessions/SessionsConfig).io.ktor.server.auth.typesafe/header(io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B>, io.ktor.server.sessions/SessionStorage, kotlin/Function1, kotlin/Unit>) // io.ktor.server.auth.typesafe/header|header@io.ktor.server.sessions.SessionsConfig(io.ktor.server.auth.typesafe.SessionAuthScheme<0:0,0:1>;io.ktor.server.sessions.SessionStorage;kotlin.Function1,kotlin.Unit>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> (io.ktor.server.sessions/SessionsConfig).io.ktor.server.auth.typesafe/header(io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B>, kotlin/Function1, kotlin/Unit>) // io.ktor.server.auth.typesafe/header|header@io.ktor.server.sessions.SessionsConfig(io.ktor.server.auth.typesafe.SessionAuthScheme<0:0,0:1>;kotlin.Function1,kotlin.Unit>){0§;1§}[0] +final fun <#A: kotlin/Any, #B: kotlin/Any> io.ktor.server.auth.typesafe/buildSessionAuthScheme(kotlin/String, io.ktor.util.reflect/TypeInfo, kotlin.reflect/KClass<#B>, kotlin/Function1, kotlin/Unit>): io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B> // io.ktor.server.auth.typesafe/buildSessionAuthScheme|buildSessionAuthScheme(kotlin.String;io.ktor.util.reflect.TypeInfo;kotlin.reflect.KClass<0:1>;kotlin.Function1,kotlin.Unit>){0§;1§}[0] +final fun <#A: kotlin/Any> (context(io.ktor.server.auth.typesafe/SessionAuthenticatedContext<#A, *>), io.ktor.server.routing/RoutingContext).io.ktor.server.auth.typesafe/clearSession() // io.ktor.server.auth.typesafe/clearSession|clearSession(io.ktor.server.auth.typesafe.SessionAuthenticatedContext<0:0,*>)@io.ktor.server.routing.RoutingContext(){0§}[0] +final fun <#A: kotlin/Any> (context(io.ktor.server.auth.typesafe/SessionAuthenticatedContext<#A, *>), io.ktor.server.routing/RoutingContext).io.ktor.server.auth.typesafe/setSession(#A) // io.ktor.server.auth.typesafe/setSession|setSession(io.ktor.server.auth.typesafe.SessionAuthenticatedContext<0:0,*>)@io.ktor.server.routing.RoutingContext(0:0){0§}[0] +final fun <#A: kotlin/Any> (context(io.ktor.server.auth.typesafe/SessionAuthenticatedContext<#A, *>), io.ktor.server.routing/RoutingContext).io.ktor.server.auth.typesafe/updateSession(kotlin/Function1<#A, #A>): #A // io.ktor.server.auth.typesafe/updateSession|updateSession(io.ktor.server.auth.typesafe.SessionAuthenticatedContext<0:0,*>)@io.ktor.server.routing.RoutingContext(kotlin.Function1<0:0,0:0>){0§}[0] +final fun <#A: kotlin/Any> (io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, *>).io.ktor.server.auth.typesafe/optional(): io.ktor.server.auth.typesafe/OptionalAuthScheme<#A> // io.ktor.server.auth.typesafe/optional|optional@io.ktor.server.auth.typesafe.DefaultAuthScheme<0:0,*>(){0§}[0] final fun <#A: kotlin/Any> (io.ktor.server.auth/AuthenticationConfig).io.ktor.server.auth/session(kotlin/String? = ..., kotlin.reflect/KClass<#A>) // io.ktor.server.auth/session|session@io.ktor.server.auth.AuthenticationConfig(kotlin.String?;kotlin.reflect.KClass<0:0>){0§}[0] final fun <#A: kotlin/Any> (io.ktor.server.auth/AuthenticationConfig).io.ktor.server.auth/session(kotlin/String?, kotlin.reflect/KClass<#A>, kotlin/Function1, kotlin/Unit>) // io.ktor.server.auth/session|session@io.ktor.server.auth.AuthenticationConfig(kotlin.String?;kotlin.reflect.KClass<0:0>;kotlin.Function1,kotlin.Unit>){0§}[0] +final fun <#A: kotlin/Any> (io.ktor.server.routing/Route).io.ktor.server.auth.typesafe/authenticateWith(io.ktor.server.auth.typesafe/OptionalAuthScheme<#A>, kotlin/Function2, io.ktor.server.routing/Route, kotlin/Unit>): io.ktor.server.routing/Route // io.ktor.server.auth.typesafe/authenticateWith|authenticateWith@io.ktor.server.routing.Route(io.ktor.server.auth.typesafe.OptionalAuthScheme<0:0>;kotlin.Function2,io.ktor.server.routing.Route,kotlin.Unit>){0§}[0] +final fun <#A: kotlin/Any> (io.ktor.server.routing/Route).io.ktor.server.auth.typesafe/authenticateWithAnyOf(kotlin.collections/List>, io.ktor.util.reflect/TypeInfo, kotlin.coroutines/SuspendFunction2, kotlin/Unit>? = ..., kotlin/Function2, io.ktor.server.routing/Route, kotlin/Unit>): io.ktor.server.routing/Route // io.ktor.server.auth.typesafe/authenticateWithAnyOf|authenticateWithAnyOf@io.ktor.server.routing.Route(kotlin.collections.List>;io.ktor.util.reflect.TypeInfo;kotlin.coroutines.SuspendFunction2,kotlin.Unit>?;kotlin.Function2,io.ktor.server.routing.Route,kotlin.Unit>){0§}[0] +final fun <#A: kotlin/Any?, #B: io.ktor.server.auth.typesafe/AuthenticatedContext<#A>> (context(#B)).io.ktor.server.auth.typesafe/authenticatedContext(): #B // io.ktor.server.auth.typesafe/authenticatedContext|authenticatedContext(0:1)(){0§;1§>}[0] final inline fun <#A: kotlin/Any> (io.ktor.server.auth/AuthenticationConfig).io.ktor.server.auth/session(kotlin/String?, kotlin/String?, kotlin.reflect/KClass<#A>, kotlin/Function1, kotlin/Unit>) // io.ktor.server.auth/session|session@io.ktor.server.auth.AuthenticationConfig(kotlin.String?;kotlin.String?;kotlin.reflect.KClass<0:0>;kotlin.Function1,kotlin.Unit>){0§}[0] +final inline fun <#A: reified kotlin/Any, #B: reified kotlin/Any> io.ktor.server.auth.typesafe/session(kotlin/String, noinline kotlin/Function1, kotlin/Unit>): io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #B> // io.ktor.server.auth.typesafe/session|session(kotlin.String;kotlin.Function1,kotlin.Unit>){0§;1§}[0] final inline fun <#A: reified kotlin/Any> (io.ktor.server.application/ApplicationCall).io.ktor.server.auth/principal(): #A? // io.ktor.server.auth/principal|principal@io.ktor.server.application.ApplicationCall(){0§}[0] final inline fun <#A: reified kotlin/Any> (io.ktor.server.application/ApplicationCall).io.ktor.server.auth/principal(kotlin/String?): #A? // io.ktor.server.auth/principal|principal@io.ktor.server.application.ApplicationCall(kotlin.String?){0§}[0] final inline fun <#A: reified kotlin/Any> (io.ktor.server.auth/AuthenticationConfig).io.ktor.server.auth/session(kotlin/String? = ...) // io.ktor.server.auth/session|session@io.ktor.server.auth.AuthenticationConfig(kotlin.String?){0§}[0] final inline fun <#A: reified kotlin/Any> (io.ktor.server.auth/AuthenticationConfig).io.ktor.server.auth/session(kotlin/String? = ..., noinline kotlin/Function1, kotlin/Unit>) // io.ktor.server.auth/session|session@io.ktor.server.auth.AuthenticationConfig(kotlin.String?;kotlin.Function1,kotlin.Unit>){0§}[0] +final inline fun <#A: reified kotlin/Any> (io.ktor.server.routing/Route).io.ktor.server.auth.typesafe/authenticateWithAnyOf(kotlin/Array>..., noinline kotlin.coroutines/SuspendFunction2, kotlin/Unit>? = ..., noinline kotlin/Function2, io.ktor.server.routing/Route, kotlin/Unit>): io.ktor.server.routing/Route // io.ktor.server.auth.typesafe/authenticateWithAnyOf|authenticateWithAnyOf@io.ktor.server.routing.Route(kotlin.Array>...;kotlin.coroutines.SuspendFunction2,kotlin.Unit>?;kotlin.Function2,io.ktor.server.routing.Route,kotlin.Unit>){0§}[0] +final inline fun <#A: reified kotlin/Any> io.ktor.server.auth.typesafe/basic(kotlin/String, kotlin/Function1, kotlin/Unit>): io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, io.ktor.server.auth.typesafe/DefaultAuthenticatedContext<#A>> // io.ktor.server.auth.typesafe/basic|basic(kotlin.String;kotlin.Function1,kotlin.Unit>){0§}[0] +final inline fun <#A: reified kotlin/Any> io.ktor.server.auth.typesafe/bearer(kotlin/String, kotlin/Function1, kotlin/Unit>): io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, io.ktor.server.auth.typesafe/DefaultAuthenticatedContext<#A>> // io.ktor.server.auth.typesafe/bearer|bearer(kotlin.String;kotlin.Function1,kotlin.Unit>){0§}[0] +final inline fun <#A: reified kotlin/Any> io.ktor.server.auth.typesafe/form(kotlin/String, kotlin/Function1, kotlin/Unit>): io.ktor.server.auth.typesafe/DefaultAuthScheme<#A, io.ktor.server.auth.typesafe/DefaultAuthenticatedContext<#A>> // io.ktor.server.auth.typesafe/form|form(kotlin.String;kotlin.Function1,kotlin.Unit>){0§}[0] +final inline fun <#A: reified kotlin/Any> io.ktor.server.auth.typesafe/session(kotlin/String, noinline kotlin/Function1, kotlin/Unit>): io.ktor.server.auth.typesafe/SessionAuthScheme<#A, #A> // io.ktor.server.auth.typesafe/session|session(kotlin.String;kotlin.Function1,kotlin.Unit>){0§}[0] final suspend fun io.ktor.server.auth/verifyWithOAuth2(io.ktor.server.auth/UserPasswordCredential, io.ktor.client/HttpClient, io.ktor.server.auth/OAuthServerSettings.OAuth2ServerSettings): io.ktor.server.auth/OAuthAccessTokenResponse.OAuth2 // io.ktor.server.auth/verifyWithOAuth2|verifyWithOAuth2(io.ktor.server.auth.UserPasswordCredential;io.ktor.client.HttpClient;io.ktor.server.auth.OAuthServerSettings.OAuth2ServerSettings){}[0] diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/build.gradle.kts b/ktor-server/ktor-server-plugins/ktor-server-auth/build.gradle.kts index cca680e4c55..7d8f1157ea5 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/build.gradle.kts +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/build.gradle.kts @@ -10,12 +10,18 @@ plugins { } kotlin { + compilerOptions { + freeCompilerArgs.add("-Xcontext-parameters") + } sourceSets { commonMain.dependencies { api(projects.ktorClientCore) api(projects.ktorServerSessions) api(libs.kotlinx.serialization.json) } + commonTest.dependencies { + implementation(projects.ktorServerTestHost) + } jvmTest.dependencies { implementation(projects.ktorServerContentNegotiation) implementation(projects.ktorSerializationJackson) diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/AuthenticationInterceptors.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/AuthenticationInterceptors.kt index 1de52325c87..55e2430d418 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/AuthenticationInterceptors.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/AuthenticationInterceptors.kt @@ -173,7 +173,7 @@ private object ReceiveBytes : Hook Any> { } } -private suspend fun AuthenticationContext.executeChallenges(call: ApplicationCall) { +internal suspend fun AuthenticationContext.executeChallenges(call: ApplicationCall) { val challenges = challenge.challenges if (this.executeChallenges(challenges, call)) return @@ -192,7 +192,7 @@ private suspend fun AuthenticationContext.executeChallenges(call: ApplicationCal } } -private suspend fun AuthenticationContext.executeChallenges( +internal suspend fun AuthenticationContext.executeChallenges( challenges: List, call: ApplicationCall ): Boolean { diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuth2.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuth2.kt index 9e0ce614b0a..73b4b52c41c 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuth2.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuth2.kt @@ -21,8 +21,7 @@ import io.ktor.utils.io.core.* import kotlinx.coroutines.* import kotlinx.io.* import kotlinx.serialization.json.* - -private val Logger: Logger = KtorSimpleLogger("io.ktor.auth.oauth") +import kotlin.io.encoding.Base64 internal suspend fun ApplicationCall.oauth2HandleCallback(): OAuthCallback? { val params = when (request.contentType()) { @@ -189,7 +188,7 @@ private suspend fun oauth2RequestAccessToken( HttpHeaders.Authorization, HttpAuthHeader.Single( AuthScheme.Basic, - "$clientId:$clientSecret".toByteArray(Charsets.ISO_8859_1).encodeBase64() + Base64.encode("$clientId:$clientSecret".toByteArray(Charsets.ISO_8859_1)) ).render() ) } diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/SessionAuth.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/SessionAuth.kt index 9b3e5e04f71..d74e9e14adb 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/SessionAuth.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/SessionAuth.kt @@ -26,13 +26,20 @@ public class SessionAuthenticationProvider private constructor( ) : AuthenticationProvider(config) { public val type: KClass = config.type + private val sessionName: String? = config.sessionName + private val challengeFunction: SessionAuthChallengeFunction = config.challengeFunction private val validator: AuthenticationFunction = config.validator override suspend fun onAuthenticate(context: AuthenticationContext) { val call = context.call - val session = call.sessions.get(type) + val sessionName = sessionName + val session = if (sessionName == null) { + call.sessions.get(type) + } else { + call.sessions.getSessionByName(sessionName) + } val principal = session?.let { validator(call, it) } if (principal != null) { @@ -55,6 +62,15 @@ public class SessionAuthenticationProvider private constructor( } } + private fun CurrentSession.getSessionByName(sessionName: String): T? { + val session = get(sessionName) ?: return null + check(type.isInstance(session)) { + "Session provider `$sessionName` returned `$session`, but session type `$type` was expected." + } + @Suppress("UNCHECKED_CAST") + return session as T + } + /** * A configuration for the [session] authentication provider. * @@ -65,6 +81,8 @@ public class SessionAuthenticationProvider private constructor( description: String?, internal val type: KClass ) : AuthenticationProvider.Config(name, description) { + internal var sessionName: String? = null + internal var validator: AuthenticationFunction = UninitializedValidator internal var challengeFunction: SessionAuthChallengeFunction = { call.respond(UnauthorizedResponse()) } diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/AuthScheme.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/AuthScheme.kt new file mode 100644 index 00000000000..b11a29aa988 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/AuthScheme.kt @@ -0,0 +1,159 @@ +/* + * 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.typesafe + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.routing.* +import io.ktor.util.* +import io.ktor.util.reflect.* +import io.ktor.utils.io.* +import kotlin.reflect.KClass + +/** + * Handles an authentication failure for a typed authentication scheme. + * + * The handler receives the current [ApplicationCall] and the [AuthenticationFailedCause] for the failed + * authentication attempt. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.UnauthorizedHandler) + */ +public typealias UnauthorizedHandler = suspend (ApplicationCall, AuthenticationFailedCause) -> Unit + +/** + * Represents a typed authentication scheme that can provide an authenticated route context. + * + * A scheme has a stable name and creates the context used by [authenticateWith] route bodies. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.AuthScheme) + * + * @param P the principal type produced by this scheme. + * @param C the context type available inside authenticated routes. + */ +@ExperimentalKtorApi +public interface AuthScheme

> { + /** + * Name that identifies this authentication scheme. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.AuthScheme.name) + */ + public val name: String + + /** + * Creates a context that exposes authentication data for [route]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.AuthScheme.createAuthenticatedContext) + * + * @return the authenticated context used by route handlers. + */ + public fun createAuthenticatedContext(route: Route): C +} + +private val RegisteredSchemesKey = AttributeKey>("TypesafeAuthRegisteredSchemes") + +/** + * Default [AuthScheme] implementation created by typed authentication builders. + * + * The scheme is registered lazily when it is used by [authenticateWith]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.DefaultAuthScheme) + * + * @param P the principal type produced by this scheme. + * @param C the context type available inside authenticated routes. + * @property name name that identifies this authentication scheme. + */ +@ExperimentalKtorApi +public open class DefaultAuthScheme

>( + override val name: String, + internal val principalType: KClass

, + internal val provider: AuthenticationProvider, + internal val onUnauthorized: UnauthorizedHandler?, + internal val contextFactory: (DefaultAuthenticatedContext

) -> C +) : AuthScheme { + internal val principalKey = AttributeKey

("TypesafeAuth:$name:Principal", TypeInfo(principalType)) + + /** + * Registers this scheme in an [AuthenticationConfig]. + * + * Typed route builders call this automatically when a scheme is used with [authenticateWith]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.DefaultAuthScheme.setup) + * + * @param config authentication configuration where the scheme should be registered. + */ + internal fun setup(config: AuthenticationConfig) { + config.register(provider) + } + + override fun createAuthenticatedContext(route: Route): C { + return contextFactory(DefaultAuthenticatedContext(principalKey)) + } + + private fun Application.registerSchemeIfNeeded() { + val registered = attributes.computeIfAbsent(RegisteredSchemesKey) { mutableSetOf() } + if (name in registered) return + authentication { setup(this) } + registered.add(name) + } + + internal open fun preinstall(route: Route) { + route.application.registerSchemeIfNeeded() + } + + internal fun install( + route: Route, + onUnauthorized: UnauthorizedHandler? = null, + kind: String = "Scheme", + optional: Boolean = false, + onAccepted: (suspend (ApplicationCall) -> Unit)? = null + ): C { + preinstall(route) + val plugin = createTypedAuthPlugin( + route = route, + kind = kind, + onUnauthorized = onUnauthorized ?: this@DefaultAuthScheme.onUnauthorized, + optional = optional, + onAccepted = onAccepted, + ) + route.install(plugin) + return createAuthenticatedContext(route) + } + + internal fun principalFrom(ctx: AuthenticationContext): P? { + return ctx.principal(provider = name, klass = principalType) + } + + internal fun capture(call: ApplicationCall, principal: Any) { + @Suppress("UNCHECKED_CAST") + call.attributes.put(principalKey, principal as P) + } + + public companion object { + /** + * Creates a [DefaultAuthScheme] that exposes the authenticated principal through [DefaultAuthenticatedContext]. + * + * Typed provider builders use this helper when they do not need a custom route context. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.DefaultAuthScheme.withDefaultContext) + * + * @param name name that identifies the authentication scheme. + * @param provider provider implementation that authenticates requests for this scheme. + * @param onUnauthorized default failure handler for routes that use the scheme. + */ + public inline fun withDefaultContext( + name: String, + provider: AuthenticationProvider, + noinline onUnauthorized: UnauthorizedHandler? + ): DefaultAuthScheme> { + return DefaultAuthScheme( + name = name, + provider = provider, + principalType = P::class, + onUnauthorized = onUnauthorized, + contextFactory = { it } + ) + } + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/BasicTypedProvider.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/BasicTypedProvider.kt new file mode 100644 index 00000000000..e50b5247514 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/BasicTypedProvider.kt @@ -0,0 +1,149 @@ +/* + * 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.typesafe + +import io.ktor.util.* +import io.ktor.util.reflect.* +import io.ktor.utils.io.* +import kotlin.jvm.JvmName +import kotlin.reflect.* + +/** + * Creates a typed Basic authentication scheme. + * + * The [validate][TypedBasicAuthConfig.validate] callback returns a principal of type [P]. Use the returned scheme + * with [authenticateWith] to protect routes and access [principal] without casts. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.basic) + * + * @param name name that identifies the Basic authentication scheme. + * @param configure configures Basic authentication for this scheme. + * @return a typed authentication scheme that produces principals of type [P]. + */ +@ExperimentalKtorApi +public inline fun basic( + name: String, + configure: TypedBasicAuthConfig

.() -> Unit +): DefaultAuthScheme> { + val typedConfig = TypedBasicAuthConfig

().apply(configure) + return DefaultAuthScheme.withDefaultContext(name, typedConfig.buildProvider(name), typedConfig.onUnauthorized) +} + +/** + * Creates a typed Bearer authentication scheme. + * + * The [authenticate][TypedBearerAuthConfig.authenticate] callback returns a principal of type [P]. Use the returned + * scheme with [authenticateWith] to protect routes and access [principal] without casts. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.bearer) + * + * @param name name that identifies the Bearer authentication scheme. + * @param configure configures Bearer authentication for this scheme. + * @return a typed authentication scheme that produces principals of type [P]. + */ +@ExperimentalKtorApi +public inline fun bearer( + name: String, + configure: TypedBearerAuthConfig

.() -> Unit +): DefaultAuthScheme> { + val typedConfig = TypedBearerAuthConfig

().apply(configure) + return DefaultAuthScheme.withDefaultContext(name, typedConfig.buildProvider(name), typedConfig.onUnauthorized) +} + +/** + * Creates a typed Form authentication scheme. + * + * The [validate][TypedFormAuthConfig.validate] callback returns a principal of type [P]. Use the returned scheme with + * [authenticateWith] to protect routes and access [principal] without casts. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.form) + * + * @param name name that identifies the Form authentication scheme. + * @param configure configures Form authentication for this scheme. + * @return a typed authentication scheme that produces principals of type [P]. + */ +@ExperimentalKtorApi +public inline fun form( + name: String, + configure: TypedFormAuthConfig

.() -> Unit +): DefaultAuthScheme> { + val typedConfig = TypedFormAuthConfig

().apply(configure) + return DefaultAuthScheme.withDefaultContext(name, typedConfig.buildProvider(name), typedConfig.onUnauthorized) +} + +/** + * Creates a typed Session authentication scheme. + * + * The session value [S] is validated and mapped to a route principal [P]. Use this scheme with [authenticateWith] + * after installing and configuring [io.ktor.server.sessions.Sessions]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.session) + * + * @param name name that identifies the Session authentication scheme. + * @param configure configures Session authentication for this scheme. + * @return a typed authentication scheme that produces principals of type [P]. + */ +@ExperimentalKtorApi +@JvmName("sessionWithPrincipal") +public inline fun session( + name: String, + noinline configure: TypedSessionAuthConfig.() -> Unit +): SessionAuthScheme { + return buildSessionAuthScheme( + name = name, + sessionTypeInfo = typeInfo(), + principalType = P::class, + configure = configure + ) +} + +@PublishedApi +@OptIn(ExperimentalKtorApi::class) +internal fun buildSessionAuthScheme( + name: String, + sessionTypeInfo: TypeInfo, + principalType: KClass

, + configure: TypedSessionAuthConfig.() -> Unit +): SessionAuthScheme { + val typedConfig = TypedSessionAuthConfig().apply(configure) + + @Suppress("UNCHECKED_CAST") + val sessionType = sessionTypeInfo.type as KClass + val sessionKey = AttributeKey("TypesafeAuth:$name:Session", sessionTypeInfo) + val provider = typedConfig.buildProvider( + name = name, + sessionType = sessionType, + sessionKey = sessionKey + ) + return SessionAuthScheme( + name = name, + sessionType = sessionType, + sessionTypeInfo = sessionTypeInfo, + principalType = principalType, + provider = provider, + onUnauthorized = typedConfig.onUnauthorized, + sessionKey = sessionKey + ) +} + +/** + * Creates a typed Session authentication scheme where the stored session is also the route principal. + * + * Use the returned route context to read, update, or clear the authenticated [session] value without calling + * `call.sessions` directly. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.session) + * + * @param name name that identifies the Session authentication scheme. + * @param configure configures Session authentication for this scheme. + * @return a typed authentication scheme that produces principals of type [P]. + */ +@ExperimentalKtorApi +public inline fun session( + name: String, + noinline configure: TypedSessionAuthConfig.() -> Unit +): SessionAuthScheme { + return session(name, configure) +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/OptionalAuthScheme.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/OptionalAuthScheme.kt new file mode 100644 index 00000000000..a1cb3812bce --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/OptionalAuthScheme.kt @@ -0,0 +1,135 @@ +/* + * 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.typesafe + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.routing.* +import io.ktor.util.* +import io.ktor.util.reflect.* +import io.ktor.utils.io.* + +/** + * Converts this scheme into an optional scheme. + * + * Requests without credentials continue through the route with `principal == null`. Requests with invalid credentials + * are still rejected by the original authentication scheme. + * + * ```kotlin + * val optionalAuth = userAuth.optional() + * + * routing { + * authenticateWith(optionalAuth) { + * get("/me") { + * call.respondText(principal?.name ?: "anonymous") + * } + * } + * } + * ``` + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.optional) + * + * @return a typed scheme that exposes a nullable principal. + */ +@ExperimentalKtorApi +public fun

DefaultAuthScheme.optional(): OptionalAuthScheme

= OptionalAuthScheme(base = this) + +/** + * Converts this scheme into an optional scheme with an anonymous fallback. + * + * Requests without credentials continue through the route with a principal created by [fallback]. Requests with + * invalid credentials are still rejected by the original authentication scheme. The route principal type is the common + * supertype [B] of authenticated principals [P] and anonymous principals [AP]. + * + * ```kotlin + * val publicAuth: AnonymousAuthScheme = userAuth.optional { Guest() } + * + * routing { + * authenticateWith(publicAuth) { + * get("/me") { + * call.respondText(principal.displayName) + * } + * } + * } + * ``` + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.optional) + * + * @param fallback creates a fallback principal when credentials are missing. + * @return a typed scheme that exposes an authenticated or anonymous principal. + */ +@ExperimentalKtorApi +public fun DefaultAuthScheme.optional( + fallback: suspend (ApplicationCall) -> AP +): AnonymousAuthScheme { + return AnonymousAuthScheme(base = this, anonymousFactory = fallback) +} + +/** + * Optional typed authentication scheme where missing credentials produce a nullable principal. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.OptionalAuthScheme) + * + * @param P the principal type produced when authentication succeeds. + * @property base scheme used to authenticate requests that include credentials. + */ +@ExperimentalKtorApi +public class OptionalAuthScheme

( + public val base: DefaultAuthScheme +) : AuthScheme> { + override val name: String = base.name + + override fun createAuthenticatedContext(route: Route): OptionalAuthenticatedContext

= + OptionalAuthenticatedContext(base.principalKey) + + internal fun install(route: Route): OptionalAuthenticatedContext

{ + base.install( + route = route, + kind = "Optional", + optional = true + ) + return createAuthenticatedContext(route) + } +} + +/** + * Optional typed authentication scheme where missing credentials produce an anonymous principal. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.AnonymousAuthScheme) + * + * @param B the common supertype of authenticated and anonymous principals. + * @param P the authenticated principal type. + * @param AP the anonymous principal type. + * @property base scheme used to authenticate requests that include credentials. + */ +@ExperimentalKtorApi +public class AnonymousAuthScheme( + public val base: DefaultAuthScheme, + internal val anonymousFactory: suspend (ApplicationCall) -> AP +) : AuthScheme> { + override val name: String = base.name + private val principalKey: AttributeKey = + AttributeKey("TypesafeAuth:$name:AnonymousPrincipal", TypeInfo(Any::class)) + + override fun createAuthenticatedContext(route: Route): DefaultAuthenticatedContext = + DefaultAuthenticatedContext(principalKey) + + internal fun install(route: Route): DefaultAuthenticatedContext { + base.install( + route = route, + kind = "Anonymous", + optional = true, + onAccepted = { call -> + val ctx = call.authentication + val principal: B = base.principalFrom(ctx) ?: anonymousFactory(call).also { anonymous -> + // Use ctx.principal directly; anonymous type AP may differ from base type P. + ctx.principal(provider = base.name, principal = anonymous) + } + call.attributes.put(principalKey, principal) + } + ) + return createAuthenticatedContext(route) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/RoleBasedAuthScheme.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/RoleBasedAuthScheme.kt new file mode 100644 index 00000000000..b23a5ca4be8 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/RoleBasedAuthScheme.kt @@ -0,0 +1,136 @@ +/* + * 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.typesafe + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.response.* +import io.ktor.server.routing.Route +import io.ktor.util.* +import io.ktor.utils.io.* +import kotlin.reflect.KClass + +/** + * Represents a role that can be required by a typed authentication route. + * + * Implement this interface on an enum or another role type used by your application. + * + * ```kotlin + * enum class Role : AuthRole { + * User, Admin, Moderator + * } + * ``` + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.AuthRole) + */ +@ExperimentalKtorApi +public interface AuthRole { + /** + * Name of this role. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.AuthRole.name) + */ + public val name: String +} + +/** + * Creates a role-based scheme from this typed authentication scheme. + * + * Roles are resolved for each authenticated call. Use the returned scheme with [authenticateWith] and pass the roles + * required by a route. + * + * ```kotlin + * val adminAuth = userAuth.withRoles { user -> user.roles } + * + * routing { + * authenticateWith(adminAuth, roles = setOf(Role.Admin)) { + * get("/admin") { + * call.respondText(principal.name) + * } + * } + * } + * ``` + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.withRoles) + * + * @param onForbidden default handler invoked when a principal does not have the required roles. + * @param resolve resolves roles for the authenticated principal. + * @return a typed authentication scheme that also exposes [roles]. + */ +@ExperimentalKtorApi +public fun

DefaultAuthScheme.withRoles( + onForbidden: suspend (ApplicationCall, Set) -> Unit = { call, _ -> call.respond(HttpStatusCode.Forbidden) }, + resolve: suspend ApplicationCall.(P) -> Set +): RoleBasedAuthScheme { + return RoleBasedAuthScheme( + base = this, + principalType = principalType, + onForbidden = onForbidden, + resolveRoles = resolve + ) +} + +/** + * Typed authentication scheme that checks resolved roles after authentication succeeds. + * + * Routes protected by this scheme receive a [RoleBasedContext] with both [principal] and [roles]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.RoleBasedAuthScheme) + * + * @param P the principal type. + * @param R the role type. + * @property base authentication scheme that provides the principal. + */ +@ExperimentalKtorApi +public class RoleBasedAuthScheme

internal constructor( + public val base: DefaultAuthScheme, + internal val principalType: KClass

, + private val onForbidden: suspend (ApplicationCall, Set) -> Unit, + private val resolveRoles: suspend ApplicationCall.(P) -> Set +) : AuthScheme> { + private val rolesKey: AttributeKey> = AttributeKey("TypesafeAuth:${base.name}:Roles") + + override val name: String = base.name + + override fun createAuthenticatedContext(route: Route): RoleBasedContext = + RoleBasedContext(base.principalKey, rolesKey) + + internal fun install( + route: Route, + roles: Set, + onUnauthorized: UnauthorizedHandler?, + onForbidden: ForbiddenHandler? + ): RoleBasedContext { + base.install( + route = route, + kind = "Roles", + onUnauthorized = onUnauthorized, + onAccepted = { call -> validateRoles(call, roles, onForbidden) } + ) + return createAuthenticatedContext(route) + } + + internal suspend fun validateRoles( + call: ApplicationCall, + requiredRoles: Set, + onForbidden: ForbiddenHandler? + ) { + if (requiredRoles.isEmpty()) { + LOGGER.debug("Skipping role-based authentication because no roles are required") + return + } + + val principal = base.principalFrom(ctx = call.authentication) + ?: return // Typed interceptor already handled unauthorized + + val resolvedRoles = call.resolveRoles(principal) + when { + resolvedRoles.containsAll(requiredRoles) -> call.attributes.put(rolesKey, resolvedRoles) + onForbidden != null -> onForbidden(call, requiredRoles) + else -> this.onForbidden(call, requiredRoles) + } + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/RouteBuilders.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/RouteBuilders.kt new file mode 100644 index 00000000000..3bdd1d521b9 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/RouteBuilders.kt @@ -0,0 +1,221 @@ +/* + * 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.typesafe + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.routing.* +import io.ktor.util.* +import io.ktor.util.reflect.* +import io.ktor.utils.io.* + +/** + * Builds a route with an authenticated context receiver. + * + * Typed route builders use this function type so route handlers can access [principal], [roles], or custom context + * extensions without calling `call.principal()`. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.AuthenticatedRouteBuilder) + * + * @param C the context type available inside the route builder. + */ +public typealias AuthenticatedRouteBuilder = context(C) +Route.() -> Unit + +/** + * Creates a child route protected by [scheme]. + * + * The scheme is registered in [Authentication] when this route is created. Inside [build], use [principal] to access + * the principal as [P] without casting. + * + * ```kotlin + * val userAuth = basic("user-auth") { + * validate { credentials -> findUser(credentials.name, credentials.password) } + * } + * + * routing { + * authenticateWith(userAuth) { + * get("/me") { + * call.respondText(principal.name) + * } + * } + * } + * ``` + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.authenticateWith) + * + * @param scheme typed authentication scheme used for this route. + * @param onUnauthorized optional route-level failure handler. When `null`, the scheme-level handler or provider + * challenge is used. + * @param build route builder with [C] available as a context parameter. + */ +@ExperimentalKtorApi +public fun

> Route.authenticateWith( + scheme: DefaultAuthScheme, + onUnauthorized: UnauthorizedHandler? = null, + build: AuthenticatedRouteBuilder +): Route { + val selector = AuthenticationRouteSelector(listOf(scheme.name)) + val route = createChild(selector) + with(scheme.install(route, onUnauthorized)) { + route.build() + } + return route +} + +/** + * Handles authentication failure for routes protected by [authenticateWithAnyOf]. + * + * The map contains one [AuthenticationFailedCause] for each scheme name that failed to authenticate the call. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.MultiUnauthorizedHandler) + */ +public typealias MultiUnauthorizedHandler = suspend (ApplicationCall, Map) -> Unit + +/** + * Creates a child route that accepts any of the provided typed authentication [schemes]. + * + * The first scheme that authenticates the call supplies the [principal] available inside [build]. All schemes must + * produce principals assignable to [P]. + * + * ```kotlin + * authenticateWithAnyOf(apiKeyAuth, bearerAuth) { + * get("/api/me") { + * call.respondText(principal.id) + * } + * } + * ``` + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.authenticateWithAnyOf) + * + * @param schemes typed schemes accepted by this route. + * @param onUnauthorized optional handler invoked when all schemes fail. + * @param build route builder with [DefaultAuthenticatedContext] available as a context parameter. + */ +@ExperimentalKtorApi +public inline fun Route.authenticateWithAnyOf( + vararg schemes: DefaultAuthScheme, + noinline onUnauthorized: MultiUnauthorizedHandler? = null, + noinline build: AuthenticatedRouteBuilder> +): Route { + return authenticateWithAnyOf(schemes.toList(), principalType = TypeInfo(P::class), onUnauthorized, build) +} + +@OptIn(ExperimentalKtorApi::class) +@PublishedApi +internal fun

Route.authenticateWithAnyOf( + schemes: List>, + principalType: TypeInfo, + onUnauthorized: MultiUnauthorizedHandler? = null, + build: AuthenticatedRouteBuilder> +): Route { + require(schemes.isNotEmpty()) { + "At least one scheme must be specified" + } + val names = schemes.map { it.name } + val route = createChild(selector = AuthenticationRouteSelector(names)) + val principalKeyName = "TypesafeAuth:${names.joinToString(",")}:Principal" + val principalKey = AttributeKey

(principalKeyName, principalType) + with(route.installTypedMultiAuthInterceptor(schemes, principalKey, onUnauthorized)) { + route.build() + } + return route +} + +/** + * Handles authorization failure for a role-protected typed route. + * + * The handler receives the current [ApplicationCall] and the roles required by the route. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.ForbiddenHandler) + */ +@OptIn(ExperimentalKtorApi::class) +public typealias ForbiddenHandler = suspend (ApplicationCall, Set) -> Unit + +/** + * Creates a child route protected by [scheme] and the required [roles]. + * + * Authentication failures are handled as in [authenticateWith]. If authentication succeeds but the resolved roles do + * not include every required role, [onForbidden] or the scheme-level forbidden handler is invoked. + * + * ```kotlin + * val adminAuth = userAuth.withRoles { user -> user.roles } + * + * authenticateWith(adminAuth, roles = setOf(Role.Admin)) { + * get("/admin") { + * call.respondText(principal.name) + * } + * } + * ``` + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.authenticateWith) + * + * @param scheme role-based typed authentication scheme. + * @param roles roles required to enter this route. + * @param onUnauthorized optional route-level handler invoked when authentication fails. + * @param onForbidden optional route-level handler invoked when the principal lacks required roles. + * @param build route builder with [RoleBasedContext] available as a context parameter. + */ +@ExperimentalKtorApi +public fun

Route.authenticateWith( + scheme: RoleBasedAuthScheme, + roles: Set, + onUnauthorized: UnauthorizedHandler? = null, + onForbidden: ForbiddenHandler? = null, + build: AuthenticatedRouteBuilder> +): Route { + val route = createChild(selector = AuthenticationRouteSelector(names = listOf(scheme.base.name))) + val context = scheme.install(route, roles, onUnauthorized, onForbidden) + with(context) { + route.build() + } + return route +} + +/** + * Creates a child route where authentication is optional and [principal] is nullable. + * + * Requests without credentials continue with `principal == null`. Requests with invalid credentials are rejected by + * the original authentication scheme. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.authenticateWith) + * + * @param scheme optional typed authentication scheme. + * @param build route builder with [OptionalAuthenticatedContext] available as a context parameter. + */ +@ExperimentalKtorApi +public fun

Route.authenticateWith( + scheme: OptionalAuthScheme

, + build: AuthenticatedRouteBuilder> +): Route { + val route = createChild(AuthenticationRouteSelector(listOf(scheme.name))) + with(scheme.install(route)) { + route.build() + } + return route +} + +/** + * Creates a child route where missing credentials are replaced by an anonymous principal. + * + * Requests without credentials continue with the fallback principal configured by [optional]. Requests with invalid + * credentials are rejected by the original authentication scheme. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.authenticateWith) + * + * @param scheme anonymous typed authentication scheme with a fallback factory. + * @param build route builder with [DefaultAuthenticatedContext] of the common supertype. + */ +@ExperimentalKtorApi +public fun Route.authenticateWith( + scheme: AnonymousAuthScheme, + build: AuthenticatedRouteBuilder> +): Route { + val route = createChild(AuthenticationRouteSelector(listOf(scheme.name))) + with(scheme.install(route)) { + route.build() + } + return route +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/Scopes.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/Scopes.kt new file mode 100644 index 00000000000..0e88499b112 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/Scopes.kt @@ -0,0 +1,242 @@ +/* + * 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.typesafe + +import io.ktor.server.routing.* +import io.ktor.server.sessions.* +import io.ktor.util.* +import io.ktor.utils.io.* +import kotlin.jvm.JvmName + +/** + * Provides typed access to an authenticated principal captured for a typed authentication route. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.AuthenticatedContext) + * + * @param P the principal type, or a nullable principal type for optional authentication. + */ +@ExperimentalKtorApi +@KtorDsl +public interface AuthenticatedContext

{ + /** + * Returns the principal captured for [context]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.AuthenticatedContext.principal) + */ + public fun principal(context: RoutingContext): P +} + +/** + * Default typed authentication context used by [authenticateWith]. + * + * The context exposes the authenticated [principal] and is used by typed schemes that do not define a custom context. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.DefaultAuthenticatedContext) + * + * @param P the principal type available inside the route. + */ +@ExperimentalKtorApi +@KtorDsl +public open class DefaultAuthenticatedContext

@PublishedApi internal constructor( + @PublishedApi internal val principalKey: AttributeKey

, +) : AuthenticatedContext

{ + override fun principal(context: RoutingContext): P { + return checkNotNull(context.call.attributes.getOrNull(principalKey)) { + "Principal not found. This should not happen inside an authenticateWith block." + } + } +} + +/** + * Typed authentication context that exposes both the authenticated principal and resolved roles. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.RoleBasedContext) + * + * @param P the principal type. + * @param R the role type. + */ +@ExperimentalKtorApi +@KtorDsl +public class RoleBasedContext

internal constructor( + principalKey: AttributeKey

, + private val rolesKey: AttributeKey>, +) : DefaultAuthenticatedContext

(principalKey) { + /** + * Returns the roles resolved for [context]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.RoleBasedContext.roles) + */ + public fun roles(context: RoutingContext): Set { + return context.call.attributes[rolesKey] + } +} + +/** + * Typed authentication context used by Session authentication. + * + * The context exposes the authenticated [principal], the session value that passed authentication, and helpers to + * update or clear that session in a type-safe way. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.SessionAuthenticatedContext) + * + * @param S the stored session type. + * @param P the principal type. + */ +@ExperimentalKtorApi +@KtorDsl +public class SessionAuthenticatedContext @PublishedApi internal constructor( + defaultContext: DefaultAuthenticatedContext

, + private val sessionKey: AttributeKey, + private val sessionProviderName: String, +) : AuthenticatedContext

by defaultContext { + /** + * Returns the session value that passed authentication for [context]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.SessionAuthenticatedContext.session) + */ + public fun session(context: RoutingContext): S { + return checkNotNull(context.call.attributes.getOrNull(sessionKey)) { + "Session not found. This should not happen inside a session authenticateWith block." + } + } + + /** + * Sets a new session value for [context]. + * + * The captured route session is updated as well, so subsequent reads of [session] in the same handler see [value]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.SessionAuthenticatedContext.setSession) + */ + public fun setSession(context: RoutingContext, value: S) { + context.call.sessions.set(sessionProviderName, value) + context.call.attributes.put(sessionKey, value) + } + + /** + * Clears the authenticated session for [context]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.SessionAuthenticatedContext.clearSession) + */ + public fun clearSession(context: RoutingContext) { + context.call.sessions.clear(sessionProviderName) + context.call.attributes.remove(sessionKey) + } + + /** + * Replaces the authenticated session with the value returned by [transform]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.SessionAuthenticatedContext.updateSession) + * + * @return the updated session value. + */ + public fun updateSession(context: RoutingContext, transform: (S) -> S): S { + val updated = transform(session(context)) + setSession(context, updated) + return updated + } +} + +/** + * Typed authentication context used when authentication is optional. + * + * The [principal] is `null` when the request has no credentials. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.OptionalAuthenticatedContext) + * + * @param P the principal type produced when authentication succeeds. + */ +@ExperimentalKtorApi +@KtorDsl +public class OptionalAuthenticatedContext

internal constructor( + private val principalKey: AttributeKey

, +) : AuthenticatedContext { + override fun principal(context: RoutingContext): P? { + return context.call.attributes.getOrNull(principalKey) + } +} + +/** + * Returns the authenticated context available in the current typed route builder scope. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.authenticatedContext) + */ +@ExperimentalKtorApi +context(auth: A) +public fun > authenticatedContext(): A = auth + +/** + * Authenticated principal captured for the current typed route. + * + * The property is available inside route handlers nested in [authenticateWith]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.principal) + */ +@ExperimentalKtorApi +context(auth: AuthenticatedContext

) +public val

RoutingContext.principal: P + get() = auth.principal(context = this) + +/** + * Authenticated session captured for the current session-protected typed route. + * + * Assigning this property updates the stored session and the value exposed in the current route handler. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.session) + */ +@ExperimentalKtorApi +context(auth: SessionAuthenticatedContext) +public var RoutingContext.session: S + get() = auth.session(context = this) + set(value) { + auth.setSession(context = this, value) + } + +/** + * Sets the authenticated session for the current session-protected typed route. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.setSession) + */ +@ExperimentalKtorApi +@JvmName("setAuthenticatedSession") +context(auth: SessionAuthenticatedContext) +public fun RoutingContext.setSession(value: S) { + auth.setSession(context = this, value) +} + +/** + * Replaces the authenticated session with the value returned by [transform]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.updateSession) + * + * @return the updated session value. + */ +@ExperimentalKtorApi +context(auth: SessionAuthenticatedContext) +public fun RoutingContext.updateSession(transform: (S) -> S): S { + return auth.updateSession(context = this, transform) +} + +/** + * Clears the authenticated session for the current session-protected typed route. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.clearSession) + */ +@ExperimentalKtorApi +context(auth: SessionAuthenticatedContext) +public fun RoutingContext.clearSession() { + auth.clearSession(context = this) +} + +/** + * Roles resolved for the current role-protected typed route. + * + * The property is available inside route handlers nested in [authenticateWith] with a [RoleBasedAuthScheme]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.roles) + */ +@ExperimentalKtorApi +context(auth: RoleBasedContext<*, R>) +public val RoutingContext.roles: Set + get() = auth.roles(context = this) diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/SessionAuthScheme.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/SessionAuthScheme.kt new file mode 100644 index 00000000000..eebbe7a7467 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/SessionAuthScheme.kt @@ -0,0 +1,75 @@ +/* + * 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.typesafe + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.routing.* +import io.ktor.server.sessions.* +import io.ktor.util.* +import io.ktor.util.reflect.* +import io.ktor.utils.io.* +import kotlin.reflect.* + +/** + * A typed Session authentication scheme. + * + * Use [io.ktor.server.sessions.Sessions] to configure how the session is transported or stored, for example with + * `install(Sessions) { cookie(auth) }`. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.SessionAuthScheme) + * + * @param S the stored session type. + * @param P the principal type exposed to authenticated routes. + */ +@ExperimentalKtorApi +public class SessionAuthScheme internal constructor( + name: String, + internal val sessionType: KClass, + internal val sessionTypeInfo: TypeInfo, + principalType: KClass

, + provider: AuthenticationProvider, + onUnauthorized: UnauthorizedHandler?, + sessionKey: AttributeKey, +) : DefaultAuthScheme>( + name = name, + principalType = principalType, + provider = provider, + onUnauthorized = onUnauthorized, + contextFactory = { defaultContext -> + SessionAuthenticatedContext( + defaultContext = defaultContext, + sessionKey = sessionKey, + sessionProviderName = name + ) + } +) { + + override fun preinstall(route: Route) { + try { + route.plugin(Sessions) + } catch (_: MissingApplicationPluginException) { + val message = "Typed session auth scheme `$name` requires Sessions to be installed before " + + "authenticateWith. Install it with install(Sessions) { cookie(auth) } before the typed route." + error(message) + } + + @OptIn(InternalAPI::class) + val providers = route.application.attributes.getOrNull(SessionProvidersKey).orEmpty() + val provider = providers.firstOrNull { it.name == name } + ?: error( + "Typed session auth scheme `$name` requires a Sessions provider named `$name` " + + "for session type `$sessionType`. Install it with install(Sessions) { cookie(auth) } " + + "before authenticateWith." + ) + + check(provider.type == sessionType) { + "Typed session auth scheme `$name` expects session type `$sessionType`, but Sessions " + + "provider `$name` is registered for `${provider.type}`." + } + + super.preinstall(route) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/SessionsConfigTypesafeExtensions.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/SessionsConfigTypesafeExtensions.kt new file mode 100644 index 00000000000..d16d1570217 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/SessionsConfigTypesafeExtensions.kt @@ -0,0 +1,158 @@ +/* + * 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.typesafe + +import io.ktor.server.sessions.* +import io.ktor.utils.io.* + +/** + * Configures [Sessions] to pass the typed authentication session in a cookie. + * + * The cookie and Sessions provider use [scheme]'s name. The same name is used by [authenticateWith] to read, update, + * or clear the authenticated session. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.cookie) + * + * @param scheme typed Session authentication scheme. + */ +@ExperimentalKtorApi +public fun SessionsConfig.cookie(scheme: SessionAuthScheme) { + cookie(scheme.name, scheme.sessionTypeInfo) +} + +/** + * Configures [Sessions] to pass the typed authentication session in a cookie. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.cookie) + * + * @param scheme typed Session authentication scheme. + * @param block configures cookie settings, serialization, and transformations. + */ +@ExperimentalKtorApi +public fun SessionsConfig.cookie( + scheme: SessionAuthScheme, + block: CookieSessionBuilder.() -> Unit +) { + cookie(scheme.name, scheme.sessionTypeInfo, block) +} + +/** + * Configures [Sessions] to pass the typed authentication session ID in a cookie and store session data on the server. + * + * The cookie and Sessions provider use [scheme]'s name. The same name is used by [authenticateWith] to read, update, + * or clear the authenticated session. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.cookie) + * + * @param scheme typed Session authentication scheme. + * @param storage server-side session storage. + */ +@ExperimentalKtorApi +public fun SessionsConfig.cookie( + scheme: SessionAuthScheme, + storage: SessionStorage +) { + cookie(scheme.name, scheme.sessionTypeInfo, storage) +} + +/** + * Configures [Sessions] to pass the typed authentication session ID in a cookie and store session data on the server. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.cookie) + * + * @param scheme typed Session authentication scheme. + * @param storage server-side session storage. + * @param block configures cookie settings, serialization, transformations, and ID generation. + */ +@ExperimentalKtorApi +public fun SessionsConfig.cookie( + scheme: SessionAuthScheme, + storage: SessionStorage, + block: CookieIdSessionBuilder.() -> Unit +) { + cookie(scheme.name, scheme.sessionTypeInfo, storage, block) +} + +/** + * Configures [Sessions] to pass the typed authentication session in a header. + * + * The header and Sessions provider use [scheme]'s name. The same name is used by [authenticateWith] to read, update, + * or clear the authenticated session. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.header) + * + * @param scheme typed Session authentication scheme. + */ +@ExperimentalKtorApi +public fun SessionsConfig.header(scheme: SessionAuthScheme) { + header(scheme.name, scheme.sessionTypeInfo) +} + +/** + * Configures [Sessions] to pass the typed authentication session in a header. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.header) + * + * @param scheme typed Session authentication scheme. + * @param block configures header settings, serialization, and transformations. + */ +@ExperimentalKtorApi +public fun SessionsConfig.header( + scheme: SessionAuthScheme, + block: HeaderSessionBuilder.() -> Unit +) { + header(scheme.name, scheme.sessionTypeInfo, block) +} + +/** + * Configures [Sessions] to pass the typed authentication session ID in a header and store session data on the server. + * + * The header and Sessions provider use [scheme]'s name. The same name is used by [authenticateWith] to read, update, + * or clear the authenticated session. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.header) + * + * @param scheme typed Session authentication scheme. + * @param storage server-side session storage. + */ +@ExperimentalKtorApi +public fun SessionsConfig.header( + scheme: SessionAuthScheme, + storage: SessionStorage +) { + header(scheme.name, scheme.sessionTypeInfo, storage) +} + +/** + * Configures [Sessions] to pass the typed authentication session ID in a header and store session data on the server. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.header) + * + * @param scheme typed Session authentication scheme. + * @param storage server-side session storage. + * @param block configures header settings, serialization, transformations, and ID generation. + */ +@ExperimentalKtorApi +public fun SessionsConfig.header( + scheme: SessionAuthScheme, + storage: SessionStorage, + block: HeaderIdSessionBuilder.() -> Unit +) { + header(scheme.name, scheme.sessionTypeInfo, storage, block) +} + +/** + * Sets a session value for [scheme]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.set) + * + * @param scheme typed Session authentication scheme. + * @param value session value to set. + * @throws IllegalStateException if no session provider is registered for [scheme]. + */ +@ExperimentalKtorApi +public fun CurrentSession.set(scheme: SessionAuthScheme, value: S) { + set(scheme.name, value) +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedAuthInterceptors.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedAuthInterceptors.kt new file mode 100644 index 00000000000..3c321f4567a --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedAuthInterceptors.kt @@ -0,0 +1,191 @@ +/* + * 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.typesafe + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.routing.* +import io.ktor.util.* +import io.ktor.utils.io.* + +private val TypedAuthPluginNameGeneratorKey = + AttributeKey("TypesafeAuth:PluginNameGenerator") + +private class TypedAuthPluginNameGenerator { + private var nextId: Int = 0 + + fun next(kind: String, parts: List): String = buildString { + append("TypedAuth:") + append(kind) + parts.forEach { part -> + append(":") + append(part) + } + append(":") + append(nextId++) + } +} + +internal fun Route.typedAuthPluginName(kind: String, vararg parts: String): String { + val root = lineage().last() + val generator = root.attributes.computeIfAbsent(TypedAuthPluginNameGeneratorKey) { + TypedAuthPluginNameGenerator() + } + return generator.next(kind, parts.toList()) +} + +/** + * Creates a route-scoped plugin for one typed authentication layer. + * + * Each layer receives a unique plugin key, which lets nested [authenticateWith] blocks run cumulatively instead of + * replacing one another. The same interceptor supports required, optional, and anonymous fallback modes. + */ +internal fun DefaultAuthScheme<*, *>.createTypedAuthPlugin( + route: Route, + kind: String, + onUnauthorized: UnauthorizedHandler?, + optional: Boolean = false, + onAccepted: (suspend (ApplicationCall) -> Unit)? = null +): RouteScopedPlugin = + createRouteScopedPlugin(name = route.typedAuthPluginName(kind, name)) { + on(AuthenticationHook) { call -> + if (call.isHandled) { + return@on + } + + val context = call.authentication + val existingPrincipal = principalFrom(context) + if (existingPrincipal != null) { + capture(call, existingPrincipal) + return@on + } + + logAuthenticationAttempt(call, provider) + provider.onAuthenticate(context) + if (principalFrom(context) != null) { + logAuthenticationSucceeded(call, provider) + onAccepted?.invoke(call) + principalFrom(context)?.let { principal -> capture(call, principal) } + return@on + } + + logAuthenticationFailed(call, provider) + val cause = context.allFailures.lastOrNull() ?: AuthenticationFailedCause.NoCredentials + + when { + optional && cause == AuthenticationFailedCause.NoCredentials -> { + logOptionalAuthentication(call) + onAccepted?.invoke(call) + principalFrom(context)?.let { principal -> capture(call, principal) } + } + + onUnauthorized != null -> onUnauthorized(call, cause) + else -> context.executeChallenges(call) + } + } + } + +internal fun

Route.installTypedMultiAuthInterceptor( + schemes: List>, + principalKey: AttributeKey

, + onUnauthorized: MultiUnauthorizedHandler? +): DefaultAuthenticatedContext

{ + for (scheme in schemes) { + scheme.preinstall(route = this) + } + install(createTypedMultiAuthInterceptor(schemes, principalKey, onUnauthorized, route = this)) + return DefaultAuthenticatedContext(principalKey) +} + +internal fun

createTypedMultiAuthInterceptor( + schemes: List>, + principalKey: AttributeKey

, + onUnauthorized: MultiUnauthorizedHandler?, + route: Route +): RouteScopedPlugin { + val schemeNames = schemes.joinToString(",") { it.name } + val name = route.typedAuthPluginName("AnyOf", schemeNames) + return createRouteScopedPlugin(name) { + on(AuthenticationHook) { call -> + if (call.isHandled) return@on + + val ctx = call.authentication + + if (onUnauthorized == null) { + for ((index, scheme) in schemes.withIndex()) { + val provider = scheme.provider + logAuthenticationAttempt(call, provider) + provider.onAuthenticate(ctx) + + val principal = scheme.principalFrom(ctx) + if (principal != null) { + logAuthenticationSucceeded(call, provider) + if (index != schemes.lastIndex) { + logSkippingOtherProviders(call) + } + call.attributes.put(principalKey, principal) + return@on + } + logAuthenticationFailed(call, provider) + } + + ctx.executeChallenges(call) + return@on + } + + val failures = mutableMapOf() + for ((index, scheme) in schemes.withIndex()) { + val schemeContext = AuthenticationContext(call) + val provider = scheme.provider + logAuthenticationAttempt(call, provider) + provider.onAuthenticate(schemeContext) + + val principal = scheme.principalFrom(schemeContext) + if (principal != null) { + logAuthenticationSucceeded(call, provider) + if (index != schemes.lastIndex) { + logSkippingOtherProviders(call) + } + ctx.principal(scheme.name, principal) + call.attributes.put(principalKey, principal) + return@on + } + + logAuthenticationFailed(call, provider) + failures[scheme.name] = schemeContext.lastFailureOrNoCredentials() + } + onUnauthorized(call, failures) + } + } +} + +private fun AuthenticationContext.lastFailureOrNoCredentials(): AuthenticationFailedCause { + return challenge.register.lastOrNull()?.first + ?: allFailures.lastOrNull() + ?: AuthenticationFailedCause.NoCredentials +} + +private fun logAuthenticationAttempt(call: ApplicationCall, provider: AuthenticationProvider) { + LOGGER.trace("Trying to authenticate ${call.request.uri} with ${provider.name}") +} + +private fun logAuthenticationSucceeded(call: ApplicationCall, provider: AuthenticationProvider) { + LOGGER.trace("Authentication succeeded for ${call.request.uri} with provider $provider") +} + +private fun logAuthenticationFailed(call: ApplicationCall, provider: AuthenticationProvider) { + LOGGER.trace("Authentication failed for ${call.request.uri} with provider $provider") +} + +private fun logOptionalAuthentication(call: ApplicationCall) { + LOGGER.trace("Authentication is optional and no credentials were provided for ${call.request.uri}") +} + +private fun logSkippingOtherProviders(call: ApplicationCall) { + LOGGER.trace("Authenticate for ${call.request.uri} succeed. Skipping other providers") +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedBasicAuthConfig.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedBasicAuthConfig.kt new file mode 100644 index 00000000000..b3c3cdd958c --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedBasicAuthConfig.kt @@ -0,0 +1,88 @@ +/* + * 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.typesafe + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.utils.io.* +import io.ktor.utils.io.charsets.* + +/** + * Configures a typed Basic authentication scheme. + * + * Unlike [BasicAuthenticationProvider.Config], [validate] returns [P] so routes protected by [authenticateWith] can + * read [principal] as the configured type. + * + * This config does not expose provider-level `challenge`. Set [onUnauthorized] or pass `onUnauthorized` to + * [authenticateWith] to customize failure responses. + * + * Challenge strategy: a route-level `onUnauthorized` is used first, then [onUnauthorized]. If neither is configured, + * Basic authentication responds with a `WWW-Authenticate: Basic` challenge that includes [realm] and [charset]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBasicAuthConfig) + * + * @param P the principal type produced by this scheme. + */ +@ExperimentalKtorApi +@KtorDsl +public class TypedBasicAuthConfig

@PublishedApi internal constructor() { + /** + * Human-readable description of this authentication scheme. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBasicAuthConfig.description) + */ + public var description: String? = null + + /** + * Realm passed in the `WWW-Authenticate` header. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBasicAuthConfig.realm) + */ + public var realm: String = "Ktor Server" + + /** + * Charset used to decode credentials. + * + * It can be either `UTF_8` or `null`. + * Setting `null` turns on a legacy mode (`ISO-8859-1`). + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBasicAuthConfig.charset) + */ + public var charset: Charset? = Charsets.UTF_8 + + /** + * Default handler for authentication failures. + * + * A route-level `onUnauthorized` passed to [authenticateWith] overrides this handler. If both are `null`, Basic + * authentication sends the default challenge described by this configuration. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBasicAuthConfig.onUnauthorized) + */ + public var onUnauthorized: (suspend (ApplicationCall, AuthenticationFailedCause) -> Unit)? = null + + private var validateFn: (suspend ApplicationCall.(UserPasswordCredential) -> P?)? = null + + /** + * Sets a validation function for [UserPasswordCredential]. + * + * Return a principal of type [P] when authentication succeeds, or `null` when credentials are invalid. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBasicAuthConfig.validate) + * + * @param body validation function called for credentials extracted from the request. + */ + public fun validate(body: suspend ApplicationCall.(UserPasswordCredential) -> P?) { + validateFn = body + } + + @PublishedApi + internal fun buildProvider(name: String): BasicAuthenticationProvider { + val config = BasicAuthenticationProvider.Config(name, description) + config.realm = realm + config.charset = charset + validateFn?.let { fn -> config.validate { credential -> fn(credential) } } + return BasicAuthenticationProvider(config) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedBearerAuthConfig.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedBearerAuthConfig.kt new file mode 100644 index 00000000000..8195c05c963 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedBearerAuthConfig.kt @@ -0,0 +1,116 @@ +/* + * 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.typesafe + +import io.ktor.http.auth.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.utils.io.* + +/** + * Configures a typed Bearer authentication scheme. + * + * Unlike [BearerAuthenticationProvider.Config], [authenticate] returns [P] so routes protected by [authenticateWith] + * can read [principal] as the configured type. + * + * This config does not expose provider-level `challenge`. Set [onUnauthorized] or pass `onUnauthorized` to + * [authenticateWith] to customize failure responses. + * + * Challenge strategy: a route-level `onUnauthorized` is used first, then [onUnauthorized]. If neither is configured, + * Bearer authentication responds with a `WWW-Authenticate` challenge for the default authentication scheme (`Bearer` + * unless changed by [authSchemes]) and the optional [realm]. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBearerAuthConfig) + * + * @param P the principal type produced by this scheme. + */ +@ExperimentalKtorApi +@KtorDsl +public class TypedBearerAuthConfig

@PublishedApi internal constructor() { + /** + * Human-readable description of this authentication scheme. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBearerAuthConfig.description) + */ + public var description: String? = null + + /** + * Optional Bearer realm passed in the `WWW-Authenticate` header. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBearerAuthConfig.realm) + */ + public var realm: String? = null + + /** + * Default handler for authentication failures. + * + * A route-level `onUnauthorized` passed to [authenticateWith] overrides this handler. If both are `null`, Bearer + * authentication sends the default challenge described by this configuration. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBearerAuthConfig.onUnauthorized) + */ + public var onUnauthorized: (suspend (ApplicationCall, AuthenticationFailedCause) -> Unit)? = null + + private var authenticateFn: (suspend ApplicationCall.(BearerTokenCredential) -> P?)? = null + private var authHeaderFn: ((ApplicationCall) -> HttpAuthHeader?)? = null + private var defaultScheme: String? = null + private var additionalSchemes: List? = null + + /** + * Exchanges a bearer token for a principal of type [P]. + * + * Return `null` when the token is not accepted. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBearerAuthConfig.authenticate) + * + * @param body authentication function called with the extracted [BearerTokenCredential]. + */ + public fun authenticate(body: suspend ApplicationCall.(BearerTokenCredential) -> P?) { + authenticateFn = body + } + + /** + * Configures how to retrieve an HTTP authentication header. + * + * By default, Bearer authentication parses the `Authorization` header. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBearerAuthConfig.authHeader) + * + * @param block returns an authentication header for the call, or `null` when no header is available. + */ + public fun authHeader(block: (ApplicationCall) -> HttpAuthHeader?) { + authHeaderFn = block + } + + /** + * Configures accepted authentication schemes. + * + * By default, only the `Bearer` scheme is accepted. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedBearerAuthConfig.authSchemes) + * + * @param defaultScheme scheme used in the default challenge. + * @param additionalSchemes additional schemes accepted when validating the request. + */ + public fun authSchemes( + defaultScheme: String = io.ktor.http.auth.AuthScheme.Bearer, + vararg additionalSchemes: String + ) { + this.defaultScheme = defaultScheme + this.additionalSchemes = additionalSchemes.toList() + } + + @PublishedApi + internal fun buildProvider(name: String): BearerAuthenticationProvider { + val config = BearerAuthenticationProvider.Config(name, description) + realm?.let { config.realm = it } + authenticateFn?.let { fn -> config.authenticate { credential -> fn(credential) } } + authHeaderFn?.let { config.authHeader(it) } + defaultScheme?.let { ds -> + config.authSchemes(ds, *additionalSchemes!!.toTypedArray()) + } + return config.build() + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedFormAuthConfig.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedFormAuthConfig.kt new file mode 100644 index 00000000000..939bb1c0c76 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedFormAuthConfig.kt @@ -0,0 +1,84 @@ +/* + * 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.typesafe + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.utils.io.* + +/** + * Configures a typed Form authentication scheme. + * + * Unlike [FormAuthenticationProvider.Config], [validate] returns [P] so routes protected by [authenticateWith] can + * read [principal] as the configured type. + * + * This config does not expose provider-level `challenge`. Set [onUnauthorized] or pass `onUnauthorized` to + * [authenticateWith] to customize failure responses. + * + * Challenge strategy: a route-level `onUnauthorized` is used first, then [onUnauthorized]. If neither is configured, + * Form authentication responds with its default `401 Unauthorized` challenge. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedFormAuthConfig) + * + * @param P the principal type produced by this scheme. + */ +@ExperimentalKtorApi +@KtorDsl +public class TypedFormAuthConfig

@PublishedApi internal constructor() { + /** + * Human-readable description of this authentication scheme. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedFormAuthConfig.description) + */ + public var description: String? = null + + /** + * POST parameter name used to read the username. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedFormAuthConfig.userParamName) + */ + public var userParamName: String = "user" + + /** + * POST parameter name used to read the password. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedFormAuthConfig.passwordParamName) + */ + public var passwordParamName: String = "password" + + /** + * Default handler for authentication failures. + * + * A route-level `onUnauthorized` passed to [authenticateWith] overrides this handler. If both are `null`, Form + * authentication sends the default challenge described by this configuration. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedFormAuthConfig.onUnauthorized) + */ + public var onUnauthorized: (suspend (ApplicationCall, AuthenticationFailedCause) -> Unit)? = null + + private var validateFn: (suspend ApplicationCall.(UserPasswordCredential) -> P?)? = null + + /** + * Sets a validation function for [UserPasswordCredential]. + * + * Return a principal of type [P] when authentication succeeds, or `null` when credentials are invalid. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedFormAuthConfig.validate) + * + * @param body validation function called for credentials extracted from form parameters. + */ + public fun validate(body: suspend ApplicationCall.(UserPasswordCredential) -> P?) { + validateFn = body + } + + @PublishedApi + internal fun buildProvider(name: String): FormAuthenticationProvider { + val config = FormAuthenticationProvider.Config(name, description) + config.userParamName = userParamName + config.passwordParamName = passwordParamName + validateFn?.let { fn -> config.validate { credential -> fn(credential) } } + return config.build() + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedSessionAuthConfig.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedSessionAuthConfig.kt new file mode 100644 index 00000000000..a3ddeab02dc --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/typesafe/TypedSessionAuthConfig.kt @@ -0,0 +1,85 @@ +/* + * 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.typesafe + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.util.* +import io.ktor.utils.io.* +import kotlin.reflect.KClass + +/** + * Configures a typed Session authentication scheme. + * + * Unlike [SessionAuthenticationProvider.Config], [validate] returns [P] from a stored session value [S], so routes + * protected by [authenticateWith] can read [principal] as [P] and [session] as [S]. + * + * This config does not expose provider-level `challenge`. Set [onUnauthorized] or pass `onUnauthorized` to + * [authenticateWith] to customize failure responses. + * + * Challenge strategy: a route-level `onUnauthorized` is used first, then [onUnauthorized]. If neither is configured, + * Session authentication responds with its default `401 Unauthorized` challenge. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedSessionAuthConfig) + * + * @param S the stored session type. + * @param P the principal type exposed to authenticated routes. + */ +@ExperimentalKtorApi +@KtorDsl +public class TypedSessionAuthConfig @PublishedApi internal constructor() { + /** + * Human-readable description of this authentication scheme. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedSessionAuthConfig.description) + */ + public var description: String? = null + + /** + * Default handler for authentication failures. + * + * A route-level `onUnauthorized` passed to [authenticateWith] overrides this handler. If both are `null`, Session + * authentication sends the default challenge described by this configuration. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedSessionAuthConfig.onUnauthorized) + */ + public var onUnauthorized: (suspend (ApplicationCall, AuthenticationFailedCause) -> Unit)? = null + + private var validateFn: (suspend ApplicationCall.(S) -> P?)? = null + + /** + * Sets a validation function for the session value. + * + * Return the principal of type [P] when the session is accepted, or `null` when the session is invalid. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedSessionAuthConfig.validate) + * + * @param body validation function called with the session value read by the [io.ktor.server.sessions.Sessions] + * plugin. + */ + public fun validate(body: suspend ApplicationCall.(S) -> P?) { + validateFn = body + } + + @PublishedApi + internal fun buildProvider( + name: String, + sessionType: KClass, + sessionKey: AttributeKey + ): SessionAuthenticationProvider { + val config = SessionAuthenticationProvider.Config(name, description, sessionType) + config.sessionName = name + validateFn?.let { fn -> + config.validate { session -> + val principal = fn(session) + if (principal != null) { + attributes.put(sessionKey, session) + } + principal + } + } + return config.buildProvider() + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BasicAuthTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BasicAuthTest.kt index 0ca34cb5e18..f6dbaccb3fd 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BasicAuthTest.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BasicAuthTest.kt @@ -14,9 +14,9 @@ import io.ktor.server.plugins.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.testing.* -import io.ktor.util.* import io.ktor.utils.io.charsets.* import io.ktor.utils.io.core.* +import kotlin.io.encoding.Base64 import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals @@ -226,7 +226,7 @@ class BasicAuthTest { charset: Charset = Charsets.ISO_8859_1 ): HttpResponse = client.get(url) { val up = "$user:$pass" - val encoded = up.toByteArray(charset).encodeBase64() + val encoded = Base64.encode(up.toByteArray(charset)) header(HttpHeaders.Authorization, "Basic $encoded") } diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/CryptoTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/CryptoTest.kt index de1b96e0fbc..53fafef1966 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/CryptoTest.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/CryptoTest.kt @@ -13,7 +13,7 @@ import kotlin.test.* class CryptoTest { @Test fun testBase64() { - assertEquals("AAAA", ByteArray(3).encodeBase64()) + assertEquals("AAAA", Base64.encode(ByteArray(3))) assertEquals(ByteArray(3), Base64.decode("AAAA")) } diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/OAuth2Test.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/OAuth2Test.kt index 166ac138e82..ddc177568fb 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/OAuth2Test.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/OAuth2Test.kt @@ -30,6 +30,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import kotlin.io.encoding.Base64 import kotlin.test.* class OAuth2Test { @@ -852,7 +853,7 @@ class OAuth2Test { private suspend fun ApplicationTestBuilder.handleRequestWithBasic(url: String, user: String, pass: String) = client.get(url) { val up = "$user:$pass" - val encoded = up.toByteArray(Charsets.ISO_8859_1).encodeBase64() + val encoded = Base64.encode(up.toByteArray(Charsets.ISO_8859_1)) header(HttpHeaders.Authorization, "Basic $encoded") } diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/BasicSchemesTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/BasicSchemesTest.kt new file mode 100644 index 00000000000..8de58735d06 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/BasicSchemesTest.kt @@ -0,0 +1,417 @@ +/* + * 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.tests.auth.typesafe + +import io.ktor.client.plugins.cookies.* +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.sessions.* +import io.ktor.server.testing.* +import io.ktor.utils.io.* +import kotlinx.serialization.Serializable +import kotlin.test.* + +@Serializable +data class UserSession(val username: String, val visits: Int = 0) + +private class EmailContext( + defaultContext: DefaultAuthenticatedContext +) : AuthenticatedContext by defaultContext { + fun email(context: RoutingContext): String = principal(context).email +} + +context(auth: EmailContext) +private val RoutingContext.email: String + get() = auth.email(this) + +class BasicSchemesTest { + + private val basicScheme = testBasicScheme() + private val bearerScheme = testBearerScheme() + + @Test + fun `basic scheme authenticates and rejects`() = testApplication { + routing { + authenticateWith(basicScheme) { + assertIs>(authenticatedContext()) + + get("/profile") { + call.respondText("${principal.name}:${principal.email}") + } + } + } + + val ok = client.get("/profile") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals(HttpStatusCode.OK, ok.status) + assertEquals("user:user@test.com", ok.bodyAsText()) + + assertEquals(HttpStatusCode.Unauthorized, client.get("/profile").status) + + val invalid = client.get("/profile") { + header(HttpHeaders.Authorization, basicAuthHeader("wrong", "creds")) + } + assertEquals(HttpStatusCode.Unauthorized, invalid.status) + } + + @Test + fun `bearer scheme authenticates and rejects`() = testApplication { + routing { + authenticateWith(bearerScheme) { + get("/api") { + call.respondText("${principal.name}:${principal.email}") + } + } + } + + val ok = client.get("/api") { + header(HttpHeaders.Authorization, bearerAuthHeader("valid")) + } + assertEquals(HttpStatusCode.OK, ok.status) + assertEquals("bearer-user:bearer@test.com", ok.bodyAsText()) + + val invalid = client.get("/api") { + header(HttpHeaders.Authorization, bearerAuthHeader("invalid")) + } + assertEquals(HttpStatusCode.Unauthorized, invalid.status) + } + + @Test + fun `form scheme authenticates and rejects`() = testApplication { + val formScheme = form("test-form") { + validate { credentials -> + if (credentials.name == "user" && credentials.password == "pass") { + TestUser(credentials.name, "user@test.com") + } else { + null + } + } + } + + routing { + authenticateWith(formScheme) { + post("/login") { + call.respondText("${principal.name}:${principal.email}") + } + } + } + + val ok = client.post("/login") { + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) + setBody("user=user&password=pass") + } + assertEquals(HttpStatusCode.OK, ok.status) + assertEquals("user:user@test.com", ok.bodyAsText()) + + val invalid = client.post("/login") { + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) + setBody("user=wrong&password=creds") + } + assertEquals(HttpStatusCode.Unauthorized, invalid.status) + } + + @Test + fun `session scheme authenticates and rejects`() = testApplication { + val sessionScheme = session("test-session") { + validate { session -> session } + } + + install(Sessions) { + cookie(sessionScheme) + } + + routing { + get("/set-session") { + call.sessions.set(sessionScheme, UserSession("Alice")) + call.respondText("ok") + } + authenticateWith(sessionScheme) { + get("/protected") { + call.respondText(principal.username) + } + } + } + + // Missing session → 401 + assertEquals(HttpStatusCode.Unauthorized, client.get("/protected").status) + + // With session → 200 + val cookieClient = createClient { install(HttpCookies) } + cookieClient.get("/set-session") + val response = cookieClient.get("/protected") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("Alice", response.bodyAsText()) + } + + @Test + fun `session scheme exposes stored session and mapped principal`() = testApplication { + val sessionScheme = session("test-session-principal") { + validate { session -> + TestUser(session.username, "${session.username}@test.com") + } + } + + install(Sessions) { + cookie(sessionScheme) + } + + routing { + get("/set-session") { + call.sessions.set(sessionScheme, UserSession("Alice", visits = 3)) + call.respondText("ok") + } + authenticateWith(sessionScheme) { + assertIs>(authenticatedContext()) + + get("/protected") { + call.respondText("${session.username}:${session.visits}:${principal.email}") + } + } + } + + val cookieClient = createClient { install(HttpCookies) } + cookieClient.get("/set-session") + val response = cookieClient.get("/protected") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("Alice:3:Alice@test.com", response.bodyAsText()) + } + + @Test + fun `session scheme updates and clears typed session`() = testApplication { + val sessionScheme = session("test-session-update") { + validate { session -> TestUser(session.username, "${session.username}@test.com") } + } + + install(Sessions) { + cookie(sessionScheme) + } + + routing { + get("/set-session") { + call.sessions.set(sessionScheme, UserSession("Alice")) + call.respondText("ok") + } + authenticateWith(sessionScheme) { + get("/touch") { + val updated = updateSession { current -> current.copy(visits = current.visits + 1) } + call.respondText("${principal.name}:${updated.visits}:${session.visits}") + } + get("/rename") { + session = session.copy(username = "bob") + call.respondText("${principal.name}:${session.username}") + } + get("/logout") { + clearSession() + call.respondText("bye") + } + } + } + + val cookieClient = createClient { install(HttpCookies) } + cookieClient.get("/set-session") + + val firstTouch = cookieClient.get("/touch") + assertEquals(HttpStatusCode.OK, firstTouch.status) + assertEquals("Alice:1:1", firstTouch.bodyAsText()) + + val secondTouch = cookieClient.get("/touch") + assertEquals(HttpStatusCode.OK, secondTouch.status) + assertEquals("Alice:2:2", secondTouch.bodyAsText()) + + val rename = cookieClient.get("/rename") + assertEquals(HttpStatusCode.OK, rename.status) + assertEquals("Alice:bob", rename.bodyAsText()) + + val afterRename = cookieClient.get("/touch") + assertEquals(HttpStatusCode.OK, afterRename.status) + assertEquals("bob:3:3", afterRename.bodyAsText()) + + val logout = cookieClient.get("/logout") + assertEquals(HttpStatusCode.OK, logout.status) + assertEquals("bye", logout.bodyAsText()) + assertEquals(HttpStatusCode.Unauthorized, cookieClient.get("/touch").status) + } + + @Test + fun `session scheme requires Sessions to be installed before typed route`() = testApplication { + val sessionScheme = session("missing-session") { + validate { session -> session } + } + + routing { + authenticateWith(sessionScheme) { + get("/protected") { + call.respondText(principal.username) + } + } + } + + val failure = assertFailsWith { + startApplication() + } + + assertContains(failure.message.orEmpty(), "requires Sessions to be installed before authenticateWith") + } + + @Test + fun `session scheme requires matching Sessions provider before typed route`() = testApplication { + val sessionScheme = session("missing-session-provider") { + validate { session -> session } + } + + install(Sessions) { + cookie("other-session") + } + + routing { + authenticateWith(sessionScheme) { + get("/protected") { + call.respondText(principal.username) + } + } + } + + val failure = assertFailsWith { + startApplication() + } + + assertContains( + failure.message.orEmpty(), + "requires a Sessions provider named `missing-session-provider`" + ) + } + + @Test + fun `session scheme requires Sessions before optional typed route`() = testApplication { + val sessionScheme = session("missing-optional-session") { + validate { session -> session } + } + + routing { + authenticateWith(sessionScheme.optional()) { + get("/protected") { + call.respondText(principal?.username.orEmpty()) + } + } + } + + val failure = assertFailsWith { + startApplication() + } + + assertContains(failure.message.orEmpty(), "requires Sessions to be installed before authenticateWith") + } + + @Test + fun `session scheme requires Sessions before role typed route`() = testApplication { + val sessionScheme = session("missing-role-session") { + validate { session -> session } + }.withRoles { + setOf(TestRole.User) + } + + routing { + authenticateWith(sessionScheme, roles = setOf(TestRole.User)) { + get("/protected") { + call.respondText(principal.username) + } + } + } + + val failure = assertFailsWith { + startApplication() + } + + assertContains(failure.message.orEmpty(), "requires Sessions to be installed before authenticateWith") + } + + @Test + fun `session scheme requires Sessions before any-of typed route`() = testApplication { + val sessionScheme = session("missing-any-of-session") { + validate { session -> session } + } + + routing { + authenticateWithAnyOf(sessionScheme) { + get("/protected") { + call.respondText(principal.username) + } + } + } + + val failure = assertFailsWith { + startApplication() + } + + assertContains(failure.message.orEmpty(), "requires Sessions to be installed before authenticateWith") + } + + @Test + fun `different principal types on different routes are auto-inferred`() = testApplication { + data class AdminPrincipal(val level: Int) + + val userScheme = basic("user-scheme") { + validate { TestUser(it.name, "${it.name}@test.com") } + } + val adminScheme = bearer("admin-scheme") { + authenticate { AdminPrincipal(42) } + } + + routing { + authenticateWith(userScheme) { + get("/user") { call.respondText(principal.email) } + } + authenticateWith(adminScheme) { + get("/admin") { call.respondText("level=${principal.level}") } + } + } + + val userResp = client.get("/user") { + header(HttpHeaders.Authorization, basicAuthHeader("Alice")) + } + assertEquals("Alice@test.com", userResp.bodyAsText()) + + val adminResp = client.get("/admin") { + header(HttpHeaders.Authorization, bearerAuthHeader("token")) + } + assertEquals("level=42", adminResp.bodyAsText()) + } + + @Test + fun `custom auth context is available in authenticated route`() = testApplication { + val config = TypedBasicAuthConfig().apply { + validate { credentials -> TestUser(credentials.name, "${credentials.name}@test.com") } + } + val scheme = DefaultAuthScheme( + name = "custom-context", + principalType = TestUser::class, + provider = config.buildProvider("custom-context"), + onUnauthorized = null, + ) { contextConfig -> EmailContext(contextConfig) } + + routing { + authenticateWith(scheme) { + assertIs(authenticatedContext()) + + get("/custom") { + call.respondText("$email:${principal.name}") + } + } + } + + val response = client.get("/custom") { + header(HttpHeaders.Authorization, basicAuthHeader("Alice")) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("Alice@test.com:Alice", response.bodyAsText()) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/LegacyApiCoexistenceTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/LegacyApiCoexistenceTest.kt new file mode 100644 index 00000000000..7d32d463bd0 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/LegacyApiCoexistenceTest.kt @@ -0,0 +1,205 @@ +/* + * 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.tests.auth.typesafe + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.auth.* +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.ExperimentalKtorApi +import kotlin.test.Test +import kotlin.test.assertEquals + +class LegacyApiCoexistenceTest { + + @Test + fun `typesafe and legacy auth coexist with call principal access`() = testApplication { + install(Authentication) { + basic("old-style") { + validate { TestUser(it.name, "old@test.com") } + } + } + + val newScheme = basic("new-style") { + validate { TestUser(it.name, "new@test.com") } + } + + routing { + authenticate("old-style") { + get("/old") { + val p = call.principal() + call.respondText(p?.email ?: "none") + } + } + authenticateWith(newScheme) { + get("/new") { call.respondText(principal.email) } + get("/new-call") { + // call.principal() also works inside typesafe API + val manual = call.principal() + call.respondText("${principal.name}:${manual?.name}") + } + } + } + + val auth = basicAuthHeader("user") + + val oldResp = client.get("/old") { header(HttpHeaders.Authorization, auth) } + assertEquals("old@test.com", oldResp.bodyAsText()) + + val newResp = client.get("/new") { header(HttpHeaders.Authorization, auth) } + assertEquals("new@test.com", newResp.bodyAsText()) + + val callResp = client.get("/new-call") { header(HttpHeaders.Authorization, auth) } + assertEquals("user:user", callResp.bodyAsText()) + } + + @Test + fun `authentication plugin auto-installed by typesafe API`() = testApplication { + val scheme = basic("auto-install") { + validate { TestUser(it.name, "auto@test.com") } + } + + routing { + authenticateWith(scheme) { + get("/test") { call.respondText(principal.name) } + } + } + + val response = client.get("/test") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("user", response.bodyAsText()) + } + + @Test + fun `authenticateWith inside authenticate`() = testApplication { + install(Authentication) { + form("legacy") { + validate { credentials -> + if (credentials.name == "legacy" && credentials.password == "pass") { + TestUser(credentials.name, "legacy@test.com") + } else { + null + } + } + } + } + + val newScheme = basic("inner-new") { + validate { credentials -> + if (credentials.name == "typed" && credentials.password == "pass") { + TestUser(credentials.name, "new@test.com") + } else { + null + } + } + } + + routing { + authenticate("legacy") { + authenticateWith(newScheme) { + post("/nested") { + val legacy = call.principal("legacy") + call.respondText("${legacy?.name}:${principal.name}") + } + } + } + } + + val onlyLegacy = client.post("/nested") { + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) + setBody("user=legacy&password=pass") + } + assertEquals(HttpStatusCode.Unauthorized, onlyLegacy.status) + + val onlyTyped = client.post("/nested") { + header(HttpHeaders.Authorization, basicAuthHeader("typed")) + } + assertEquals(HttpStatusCode.Unauthorized, onlyTyped.status) + + val both = client.post("/nested") { + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) + header(HttpHeaders.Authorization, basicAuthHeader("typed")) + setBody("user=legacy&password=pass") + } + assertEquals(HttpStatusCode.OK, both.status) + assertEquals("legacy:typed", both.bodyAsText()) + + assertEquals(HttpStatusCode.Unauthorized, client.post("/nested").status) + } + + @Test + fun `authenticate inside authenticateWith`() = testApplication { + install(Authentication) { + form("legacy") { + validate { credentials -> + if (credentials.name == "legacy" && credentials.password == "pass") { + TestUser(credentials.name, "legacy@test.com") + } else { + null + } + } + } + } + + val newScheme = basic("outer-new") { + validate { credentials -> + if (credentials.name == "typed" && credentials.password == "pass") { + TestUser(credentials.name, "typed@test.com") + } else { + null + } + } + } + + routing { + authenticateWith(newScheme) { + get("/outer") { call.respondText(principal.email) } + authenticate("legacy") { + post("/nested") { + val typed = principal + val typedByName = call.principal("outer-new") + val legacy = call.principal("legacy") + call.respondText("${typed.email}:${typedByName?.email}:${legacy?.email}") + } + } + } + } + + val auth = basicAuthHeader("typed") + + // Outer scope works with typesafe principal + val outerResp = client.get("/outer") { header(HttpHeaders.Authorization, auth) } + assertEquals("typed@test.com", outerResp.bodyAsText()) + + // Legacy authenticate keeps Ktor's FirstSuccessful behavior and skips once typed auth set a principal. + val onlyTyped = client.post("/nested") { header(HttpHeaders.Authorization, auth) } + assertEquals(HttpStatusCode.OK, onlyTyped.status) + assertEquals("typed@test.com:typed@test.com:null", onlyTyped.bodyAsText()) + + val onlyLegacy = client.post("/nested") { + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) + setBody("user=legacy&password=pass") + } + assertEquals(HttpStatusCode.Unauthorized, onlyLegacy.status) + + val both = client.post("/nested") { + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) + header(HttpHeaders.Authorization, auth) + setBody("user=legacy&password=pass") + } + assertEquals(HttpStatusCode.OK, both.status) + assertEquals("typed@test.com:typed@test.com:null", both.bodyAsText()) + + assertEquals(HttpStatusCode.Unauthorized, client.post("/nested").status) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/MultipleSchemeTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/MultipleSchemeTest.kt new file mode 100644 index 00000000000..9dd44af4d9b --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/MultipleSchemeTest.kt @@ -0,0 +1,62 @@ +/* + * 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.tests.auth.typesafe + +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.get +import io.ktor.server.testing.* +import io.ktor.utils.io.ExperimentalKtorApi +import kotlin.test.Test +import kotlin.test.assertEquals + +class MultipleSchemeTest { + + interface AppUser { + val email: String + } + + data class BasicUser(override val email: String) : AppUser + data class BearerUser(override val email: String) : AppUser + + private val basicScheme = basic("multi-basic") { + validate { BasicUser("${it.name}@basic.com") } + } + + private val bearerScheme = bearer("multi-bearer") { + authenticate { BearerUser("${it.token}@bearer.com") } + } + + @Test + fun `anyOf accepts matching schemes and rejects when none match`() = testApplication { + routing { + authenticateWithAnyOf(basicScheme, bearerScheme) { + get("/profile") { call.respondText(principal.email) } + } + } + + // Basic credentials → 200 + val basicResp = client.get("/profile") { + header(HttpHeaders.Authorization, basicAuthHeader("alice")) + } + assertEquals(HttpStatusCode.OK, basicResp.status) + assertEquals("alice@basic.com", basicResp.bodyAsText()) + + // Bearer credentials → 200 + val bearerResp = client.get("/profile") { + header(HttpHeaders.Authorization, bearerAuthHeader("tok")) + } + assertEquals(HttpStatusCode.OK, bearerResp.status) + assertEquals("tok@bearer.com", bearerResp.bodyAsText()) + + // No credentials → 401 + assertEquals(HttpStatusCode.Unauthorized, client.get("/profile").status) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/NestedRoutesTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/NestedRoutesTest.kt new file mode 100644 index 00000000000..f4408b10a67 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/NestedRoutesTest.kt @@ -0,0 +1,201 @@ +/* + * 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.tests.auth.typesafe + +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.Test +import kotlin.test.assertEquals + +class NestedRoutesTest { + + private val basicScheme = acceptAllBasicScheme("nested-basic") + private val bearerScheme = testBearerScheme("nested-bearer") + + private val roleScheme = acceptAllBasicScheme("nested-role").withRoles { principal -> + when (principal.name) { + "admin" -> setOf(TestRole.Admin, TestRole.User) + else -> setOf(TestRole.User) + } + } + + @Test + fun `sibling routes with different schemes and scheme reuse`() = testApplication { + routing { + authenticateWith(basicScheme) { + get("/basic") { call.respondText("basic:${principal.name}") } + } + authenticateWith(bearerScheme) { + get("/bearer") { call.respondText("bearer:${principal.name}") } + } + authenticateWith(basicScheme) { + get("/basic2") { call.respondText("basic2:${principal.name}") } + } + } + + val basicResp = client.get("/basic") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals("basic:user", basicResp.bodyAsText()) + + val bearerResp = client.get("/bearer") { + header(HttpHeaders.Authorization, bearerAuthHeader("valid")) + } + assertEquals("bearer:bearer-user", bearerResp.bodyAsText()) + + // Same scheme registered once, works on both routes + val basic2Resp = client.get("/basic2") { + header(HttpHeaders.Authorization, basicAuthHeader("alice")) + } + assertEquals("basic2:alice", basic2Resp.bodyAsText()) + + assertEquals(HttpStatusCode.Unauthorized, client.get("/basic").status) + assertEquals(HttpStatusCode.Unauthorized, client.get("/bearer").status) + assertEquals(HttpStatusCode.Unauthorized, client.get("/basic2").status) + + val basicWithBearerResp = client.get("/basic") { + header(HttpHeaders.Authorization, bearerAuthHeader("valid")) + } + assertEquals(HttpStatusCode.Unauthorized, basicWithBearerResp.status) + + val bearerWithBasicResp = client.get("/bearer") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals(HttpStatusCode.Unauthorized, bearerWithBasicResp.status) + + val basic2WithBearerResp = client.get("/basic2") { + header(HttpHeaders.Authorization, bearerAuthHeader("valid")) + } + assertEquals(HttpStatusCode.Unauthorized, basic2WithBearerResp.status) + } + + @Test + fun `nested and deeply nested routes inherit authentication`() = testApplication { + routing { + authenticateWith(basicScheme) { + route("/api") { + get("/users") { call.respondText("users:${principal.name}") } + get("/items") { call.respondText("items:${principal.name}") } + route("/v2") { + route("/admin") { + get("/deep") { call.respondText("deep:${principal.name}") } + } + } + } + } + } + + val auth = basicAuthHeader("user") + + val usersResp = client.get("/api/users") { header(HttpHeaders.Authorization, auth) } + assertEquals("users:user", usersResp.bodyAsText()) + + val itemsResp = client.get("/api/items") { header(HttpHeaders.Authorization, auth) } + assertEquals("items:user", itemsResp.bodyAsText()) + + val deepResp = client.get("/api/v2/admin/deep") { header(HttpHeaders.Authorization, auth) } + assertEquals("deep:user", deepResp.bodyAsText()) + + // All reject without credentials + assertEquals(HttpStatusCode.Unauthorized, client.get("/api/users").status) + assertEquals(HttpStatusCode.Unauthorized, client.get("/api/v2/admin/deep").status) + } + + @Test + fun `authenticateWith inside authenticateWith requires both layers`() = testApplication { + data class OuterUser(val name: String) + data class InnerUser(val name: String) + + val outerScheme = form("nested-form") { + validate { credentials -> + if (credentials.name == "outer" && credentials.password == "pass") { + OuterUser(credentials.name) + } else { + null + } + } + } + val innerScheme = basic("nested-basic-inner") { + validate { credentials -> + if (credentials.name == "inner" && credentials.password == "pass") { + InnerUser(credentials.name) + } else { + null + } + } + } + + routing { + authenticateWith(outerScheme) { + val outerContext = authenticatedContext() + + authenticateWith(innerScheme) { + post("/nested") { + val outer = outerContext.principal(this) + call.respondText("${outer.name}:${principal.name}") + } + } + } + } + + val onlyOuter = client.post("/nested") { + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) + setBody("user=outer&password=pass") + } + assertEquals(HttpStatusCode.Unauthorized, onlyOuter.status) + + val onlyInner = client.post("/nested") { + header(HttpHeaders.Authorization, basicAuthHeader("inner")) + } + assertEquals(HttpStatusCode.Unauthorized, onlyInner.status) + + val both = client.post("/nested") { + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) + header(HttpHeaders.Authorization, basicAuthHeader("inner")) + setBody("user=outer&password=pass") + } + assertEquals(HttpStatusCode.OK, both.status) + assertEquals("outer:inner", both.bodyAsText()) + + assertEquals(HttpStatusCode.Unauthorized, client.post("/nested").status) + } + + @Test + fun `authenticateWith with roles inside authenticateWith`() = testApplication { + routing { + authenticateWith(basicScheme) { + authenticateWith(roleScheme, roles = setOf(TestRole.Admin)) { + get("/admin") { + call.respondText("admin:${roles.joinToString(",") { it.name }}") + } + } + } + } + + // Admin → 200 + val adminResp = client.get("/admin") { + header(HttpHeaders.Authorization, basicAuthHeader("admin")) + } + assertEquals(HttpStatusCode.OK, adminResp.status) + assertEquals("admin:Admin,User", adminResp.bodyAsText()) + + // Regular user → 403 + val userResp = client.get("/admin") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals(HttpStatusCode.Forbidden, userResp.status) + + // No auth → 401 + assertEquals(HttpStatusCode.Unauthorized, client.get("/admin").status) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/OptionalAndAnonymousAuthTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/OptionalAndAnonymousAuthTest.kt new file mode 100644 index 00000000000..c7c180553fd --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/OptionalAndAnonymousAuthTest.kt @@ -0,0 +1,90 @@ +/* + * 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.tests.auth.typesafe + +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.get +import io.ktor.server.testing.* +import io.ktor.utils.io.ExperimentalKtorApi +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +interface AnonTestIdentity +data class AuthenticatedUser(val id: String) : AnonTestIdentity +data class GuestUser(val label: String = "guest") : AnonTestIdentity + +class OptionalAndAnonymousAuthTest { + + private val baseScheme = testBasicScheme("optional-test") + + @Test + fun `optional auth returns principal or null`() = testApplication { + routing { + authenticateWith(baseScheme.optional()) { + get("/me") { call.respondText(principal?.email ?: "anonymous") } + } + } + + val withCreds = client.get("/me") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals(HttpStatusCode.OK, withCreds.status) + assertEquals("user@test.com", withCreds.bodyAsText()) + + val withoutCreds = client.get("/me") + assertEquals(HttpStatusCode.OK, withoutCreds.status) + assertEquals("anonymous", withoutCreds.bodyAsText()) + + val invalid = client.get("/me") { + header(HttpHeaders.Authorization, basicAuthHeader("wrong", "creds")) + } + assertEquals(HttpStatusCode.Unauthorized, invalid.status) + assertTrue(invalid.headers[HttpHeaders.WWWAuthenticate].orEmpty().contains("Basic")) + } + + @Test + fun `anonymous fallback provides guest or authenticated principal`() = testApplication { + val anonScheme = basic("anon-test") { + validate { credentials -> + if (credentials.name == "user") AuthenticatedUser(credentials.name) else null + } + }.optional { GuestUser() } + + routing { + authenticateWith(anonScheme) { + get("/test") { + when (val p = principal) { + is AuthenticatedUser -> call.respondText("auth:${p.id}") + is GuestUser -> call.respondText("guest:${p.label}") + else -> call.respondText("unknown") + } + } + } + } + + val guest = client.get("/test") + assertEquals(HttpStatusCode.OK, guest.status) + assertEquals("guest:guest", guest.bodyAsText()) + + val authed = client.get("/test") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals(HttpStatusCode.OK, authed.status) + assertEquals("auth:user", authed.bodyAsText()) + + val invalid = client.get("/test") { + header(HttpHeaders.Authorization, basicAuthHeader("wrong", "creds")) + } + assertEquals(HttpStatusCode.Unauthorized, invalid.status) + assertTrue(invalid.headers[HttpHeaders.WWWAuthenticate].orEmpty().contains("Basic")) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/RoleBasedAuthTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/RoleBasedAuthTest.kt new file mode 100644 index 00000000000..151e0c9d918 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/RoleBasedAuthTest.kt @@ -0,0 +1,171 @@ +/* + * 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.tests.auth.typesafe + +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.get +import io.ktor.server.testing.* +import io.ktor.utils.io.ExperimentalKtorApi +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class RoleBasedAuthTest { + + private fun roleScheme(name: String = "role-test") = acceptAllBasicScheme(name).withRoles { principal -> + when (principal.name) { + "admin" -> setOf(TestRole.Admin, TestRole.User) + "mod" -> setOf(TestRole.Moderator, TestRole.User) + "user" -> setOf(TestRole.User) + else -> emptySet() + } + } + + @Test + fun `role-based auth grants forbids and rejects`() = testApplication { + routing { + authenticateWith(roleScheme(), roles = setOf(TestRole.Admin)) { + assertIs>(authenticatedContext()) + + get("/admin") { + call.respondText("${principal.name}:${roles.joinToString(",") { it.name }}") + } + } + } + + // Authorized → 200 with roles + val adminResp = client.get("/admin") { + header(HttpHeaders.Authorization, basicAuthHeader("admin")) + } + assertEquals(HttpStatusCode.OK, adminResp.status) + assertEquals("admin:Admin,User", adminResp.bodyAsText()) + + // Authenticated but wrong role → 403 + val userResp = client.get("/admin") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals(HttpStatusCode.Forbidden, userResp.status) + + // Unauthenticated → 401, not 403 + assertEquals(HttpStatusCode.Unauthorized, client.get("/admin").status) + } + + @Test + fun `custom onForbidden handler receives required roles`() = testApplication { + val scheme = acceptAllBasicScheme("forbidden-test").withRoles( + onForbidden = { call, requiredRoles -> + call.respondText( + "Need: ${requiredRoles.joinToString { it.name }}", + status = HttpStatusCode.Forbidden + ) + } + ) { principal -> + when (principal.name) { + "admin" -> setOf(TestRole.Admin, TestRole.User) + else -> setOf(TestRole.User) + } + } + + routing { + authenticateWith(scheme, roles = setOf(TestRole.Admin)) { + get("/admin") { call.respondText("ok") } + } + } + + val response = client.get("/admin") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals(HttpStatusCode.Forbidden, response.status) + assertEquals("Need: Admin", response.bodyAsText()) + } + + @Test + fun `route-level onForbidden overrides scheme-level`() = testApplication { + val scheme = acceptAllBasicScheme("forbidden-override").withRoles( + onForbidden = { call, _ -> + call.respondText("Scheme forbidden", status = HttpStatusCode.Forbidden) + } + ) { principal -> + when (principal.name) { + "admin" -> setOf(TestRole.Admin, TestRole.User) + else -> setOf(TestRole.User) + } + } + + routing { + authenticateWith(scheme, roles = setOf(TestRole.Admin)) { + get("/default") { call.respondText("ok") } + } + authenticateWith( + scheme, + roles = setOf(TestRole.Admin), + onForbidden = { call, requiredRoles -> + call.respondText( + "Route: need ${requiredRoles.joinToString { it.name }}", + status = HttpStatusCode.Forbidden + ) + } + ) { + get("/custom") { call.respondText("ok") } + } + } + + val defaultResp = client.get("/default") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals("Scheme forbidden", defaultResp.bodyAsText()) + + val customResp = client.get("/custom") { + header(HttpHeaders.Authorization, basicAuthHeader("user")) + } + assertEquals("Route: need Admin", customResp.bodyAsText()) + } + + @Test + fun `route-level onUnauthorized overrides scheme-level for role-based auth`() = testApplication { + val baseScheme = basic("role-unauthorized-override") { + onUnauthorized = { call, _ -> + call.respondText("Scheme unauthorized", status = HttpStatusCode.Unauthorized) + } + validate { credentials -> + if (credentials.name == "admin" && credentials.password == "pass") { + TestUser(credentials.name, "admin@test.com") + } else { + null + } + } + } + val scheme = baseScheme.withRoles { principal -> + if (principal.name == "admin") setOf(TestRole.Admin) else emptySet() + } + + routing { + authenticateWith(scheme, roles = setOf(TestRole.Admin)) { + get("/default") { call.respondText("ok") } + } + authenticateWith( + scheme, + roles = setOf(TestRole.Admin), + onUnauthorized = { call, cause -> + call.respondText( + "Route unauthorized:${cause::class.simpleName}", + status = HttpStatusCode.Unauthorized + ) + } + ) { + get("/custom") { call.respondText("ok") } + } + } + + assertEquals("Scheme unauthorized", client.get("/default").bodyAsText()) + assertEquals("Route unauthorized:NoCredentials", client.get("/custom").bodyAsText()) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/TestUtils.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/TestUtils.kt new file mode 100644 index 00000000000..3e9b9641cc8 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/TestUtils.kt @@ -0,0 +1,59 @@ +/* + * 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.tests.auth.typesafe + +import io.ktor.server.auth.typesafe.* +import io.ktor.utils.io.ExperimentalKtorApi +import kotlinx.serialization.Serializable +import kotlin.io.encoding.Base64 + +data class TestUser(val name: String, val email: String) + +@Serializable +data class TestSession(val name: String) + +enum class TestRole : AuthRole { + User, + Admin, + Moderator +} + +fun basicAuthHeader(user: String, password: String = "pass"): String = + "Basic ${Base64.encode("$user:$password".encodeToByteArray())}" + +fun bearerAuthHeader(token: String): String = "Bearer $token" + +fun testBasicScheme(name: String = "test-basic") = basic(name) { + realm = "test" + validate { credentials -> + if (credentials.name == "user" && credentials.password == "pass") { + TestUser(credentials.name, "user@test.com") + } else { + null + } + } +} + +fun acceptAllBasicScheme(name: String = "accept-all-basic") = basic(name) { + validate { credentials -> + TestUser(credentials.name, "${credentials.name}@test.com") + } +} + +fun testBearerScheme(name: String = "test-bearer") = bearer(name) { + authenticate { credential -> + if (credential.token == "valid") { + TestUser("bearer-user", "bearer@test.com") + } else { + null + } + } +} + +fun testSessionScheme(name: String = "test-session") = session(name) { + validate { it } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/UnauthorizedAndChallengesTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/UnauthorizedAndChallengesTest.kt new file mode 100644 index 00000000000..e134d56329b --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/typesafe/UnauthorizedAndChallengesTest.kt @@ -0,0 +1,189 @@ +/* + * 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.tests.auth.typesafe + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.auth.* +import io.ktor.server.auth.typesafe.* +import io.ktor.server.response.* +import io.ktor.server.routing.get +import io.ktor.server.testing.* +import io.ktor.utils.io.ExperimentalKtorApi +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class UnauthorizedAndChallengesTest { + + private class FailTwiceProvider(name: String) : AuthenticationProvider(Config(name)) { + private class Config(name: String) : AuthenticationProvider.Config(name) + + override suspend fun onAuthenticate(context: AuthenticationContext) { + context.challenge("first", AuthenticationFailedCause.NoCredentials) { _, _ -> } + context.challenge("last", AuthenticationFailedCause.InvalidCredentials) { _, _ -> } + } + } + + @Test + fun `default challenge sends WWW-Authenticate header`() = testApplication { + val scheme = basic("challenge-test") { + realm = "test-realm" + validate { credentials -> + if (credentials.name == "user" && credentials.password == "pass") { + TestUser(credentials.name, "user@test.com") + } else { + null + } + } + } + + routing { + authenticateWith(scheme) { + get("/profile") { call.respondText(principal.name) } + } + } + + val response = client.get("/profile") + assertEquals(HttpStatusCode.Unauthorized, response.status) + val wwwAuth = response.headers[HttpHeaders.WWWAuthenticate] ?: "" + assertTrue(wwwAuth.contains("Basic"), "Expected WWW-Authenticate: Basic") + assertTrue(wwwAuth.contains("test-realm"), "Expected realm in header") + } + + @Test + fun `scheme-level onUnauthorized overrides default challenge`() = testApplication { + val scheme = basic("custom-401") { + onUnauthorized = { call, _ -> + call.respondText("Custom 401", status = HttpStatusCode.Unauthorized) + } + validate { credentials -> + if (credentials.name == "user" && credentials.password == "pass") { + TestUser(credentials.name, "user@test.com") + } else { + null + } + } + } + + routing { + authenticateWith(scheme) { + get("/profile") { call.respondText(principal.name) } + } + } + + val response = client.get("/profile") + assertEquals(HttpStatusCode.Unauthorized, response.status) + assertEquals("Custom 401", response.bodyAsText()) + } + + @Test + fun `route-level onUnauthorized overrides scheme-level`() = testApplication { + val scheme = basic("override-test") { + onUnauthorized = { call, _ -> + call.respondText("Scheme default", status = HttpStatusCode.Unauthorized) + } + validate { credentials -> + if (credentials.name == "user" && credentials.password == "pass") { + TestUser(credentials.name, "user@test.com") + } else { + null + } + } + } + + routing { + authenticateWith(scheme) { + get("/default") { call.respondText(principal.name) } + } + authenticateWith(scheme, onUnauthorized = { call, _ -> + call.respondText("Route override", status = HttpStatusCode.Unauthorized) + }) { + get("/custom") { call.respondText(principal.name) } + } + } + + assertEquals("Scheme default", client.get("/default").bodyAsText()) + assertEquals("Route override", client.get("/custom").bodyAsText()) + } + + @Test + fun `onUnauthorized receives correct failure cause`() = testApplication { + val scheme = testBasicScheme("cause-test") + + routing { + authenticateWith( + scheme, + onUnauthorized = { call, cause -> + call.respondText(cause::class.simpleName!!, status = HttpStatusCode.Unauthorized) + } + ) { + get("/test") { call.respondText(principal.name) } + } + } + + // No credentials → NoCredentials + assertEquals("NoCredentials", client.get("/test").bodyAsText()) + + // Invalid credentials → InvalidCredentials + val invalid = client.get("/test") { + header(HttpHeaders.Authorization, basicAuthHeader("wrong", "creds")) + } + assertEquals("InvalidCredentials", invalid.bodyAsText()) + } + + @Test + fun `authenticateWithAnyOf calls multi onUnauthorized with per-scheme failures`() = testApplication { + val basicScheme = testBasicScheme("anyof-basic") + val bearerScheme = testBearerScheme("anyof-bearer") + + routing { + authenticateWithAnyOf( + basicScheme, + bearerScheme, + onUnauthorized = { call, failures -> + val text = failures.entries + .sortedBy { it.key } + .joinToString(";") { (name, cause) -> "$name=${cause::class.simpleName}" } + call.respondText(text, status = HttpStatusCode.Unauthorized) + } + ) { + get("/data") { call.respondText(principal.email) } + } + } + + val response = client.get("/data") + assertEquals(HttpStatusCode.Unauthorized, response.status) + assertEquals("anyof-basic=NoCredentials;anyof-bearer=NoCredentials", response.bodyAsText()) + } + + @Test + fun `authenticateWithAnyOf reports final failure per scheme`() = testApplication { + val scheme = DefaultAuthScheme.withDefaultContext( + name = "final-failure", + provider = FailTwiceProvider("final-failure"), + onUnauthorized = null + ) + + routing { + authenticateWithAnyOf( + scheme, + onUnauthorized = { call, failures -> + val cause = failures.getValue("final-failure") + call.respondText(cause::class.simpleName!!, status = HttpStatusCode.Unauthorized) + } + ) { + get("/data") { call.respondText(principal.email) } + } + } + + val response = client.get("/data") + assertEquals(HttpStatusCode.Unauthorized, response.status) + assertEquals("InvalidCredentials", response.bodyAsText()) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/src/io/ktor/server/auth/typesafe/DigestTypedProvider.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/src/io/ktor/server/auth/typesafe/DigestTypedProvider.kt new file mode 100644 index 00000000000..ef43cc60e61 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/src/io/ktor/server/auth/typesafe/DigestTypedProvider.kt @@ -0,0 +1,28 @@ +/* + * 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.typesafe + +import io.ktor.utils.io.* + +/** + * Creates a typed Digest authentication scheme. + * + * The [validate][TypedDigestAuthConfig.validate] callback returns a principal of type [P]. Use the returned scheme + * with [authenticateWith] to protect routes and access [principal] without casts. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.digest) + * + * @param name name that identifies the Digest authentication scheme. + * @param configure configures Digest authentication for this scheme. + * @return a typed authentication scheme that produces principals of type [P]. + */ +@ExperimentalKtorApi +public inline fun digest( + name: String, + configure: TypedDigestAuthConfig

.() -> Unit +): DefaultAuthScheme> { + val typedConfig = TypedDigestAuthConfig

().apply(configure) + return DefaultAuthScheme.withDefaultContext(name, typedConfig.buildProvider(name), typedConfig.onUnauthorized) +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/src/io/ktor/server/auth/typesafe/TypedDigestAuthConfig.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/src/io/ktor/server/auth/typesafe/TypedDigestAuthConfig.kt new file mode 100644 index 00000000000..dd57e5786a2 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/src/io/ktor/server/auth/typesafe/TypedDigestAuthConfig.kt @@ -0,0 +1,183 @@ +/* + * 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.typesafe + +import io.ktor.http.auth.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.util.* +import io.ktor.utils.io.* +import io.ktor.utils.io.charsets.* + +/** + * Configures a typed Digest authentication scheme. + * + * Unlike [DigestAuthenticationProvider.Config], [validate] returns [P] so routes protected by [authenticateWith] can + * read [principal] as the configured type. + * + * This config does not expose provider-level `challenge`. Set [onUnauthorized] or pass `onUnauthorized` to + * [authenticateWith] to customize failure responses. + * + * Challenge strategy: a route-level `onUnauthorized` is used first, then [onUnauthorized]. If neither is configured, + * Digest authentication responds with one `WWW-Authenticate: Digest` challenge for each configured algorithm, including + * [realm], a new nonce, supported qop values, UTF-8 charset when configured, and `userhash=true` when a + * [userHashResolver] is configured. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig) + * + * @param P the principal type produced by this scheme. + */ +@ExperimentalKtorApi +@KtorDsl +public class TypedDigestAuthConfig

@PublishedApi internal constructor() { + /** + * Human-readable description of this authentication scheme. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.description) + */ + public var description: String? = null + + /** + * Realm passed in the `WWW-Authenticate` header. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.realm) + */ + public var realm: String = "Ktor Server" + + /** + * Message digest algorithms advertised by the server. + * + * When multiple algorithms are configured, the server sends multiple `WWW-Authenticate` headers and lets the + * client choose one. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.algorithms) + */ + public var algorithms: List = + listOf(DigestAlgorithm.SHA_512_256, @Suppress("DEPRECATION") DigestAlgorithm.MD5) + + /** + * Supported Quality of Protection options. + * + * Include [DigestQop.AUTH_INT] only when the request body can be read during authentication. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.supportedQop) + */ + public var supportedQop: List = listOf(DigestQop.AUTH) + + /** + * Charset used by Digest authentication. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.charset) + */ + public var charset: Charset = Charsets.UTF_8 + + /** + * [NonceManager] used to generate and verify nonce values. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.nonceManager) + */ + public var nonceManager: NonceManager = GenerateOnlyNonceManager + + /** + * Default handler for authentication failures. + * + * A route-level `onUnauthorized` passed to [authenticateWith] overrides this handler. If both are `null`, Digest + * authentication sends the default challenge described by this configuration. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.onUnauthorized) + */ + public var onUnauthorized: (suspend (ApplicationCall, AuthenticationFailedCause) -> Unit)? = null + + private var validateFn: (suspend ApplicationCall.(DigestCredential) -> P?)? = null + private var digestProviderFn: DigestProviderFunctionV2? = null + private var legacyDigestProviderFn: DigestProviderFunction? = null + private var userHashResolverFn: UserHashResolverFunction? = null + private var strictMode: Boolean = false + + /** + * Sets a validation function for [DigestCredential]. + * + * Return a principal of type [P] when authentication succeeds, or `null` when the verified credentials should not + * be accepted. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.validate) + * + * @param body validation function called after Digest credentials are verified. + */ + public fun validate(body: suspend ApplicationCall.(DigestCredential) -> P?) { + validateFn = body + } + + /** + * Configures the digest provider used to look up `H(username:realm:password)`. + * + * This overload does not receive the selected [DigestAlgorithm]. Use the overload that accepts + * [DigestProviderFunctionV2] for full RFC 7616 support. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.digestProvider) + * + * @param digest provides a digest for a username and realm, or `null` when the user is unknown. + */ + public fun digestProvider(digest: DigestProviderFunction) { + digestProviderFn = null + legacyDigestProviderFn = digest + } + + /** + * Configures the digest provider used to look up `H(username:realm:password)` for the selected algorithm. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.digestProvider) + * + * @param digest provides a digest for a username, realm, and algorithm, or `null` when the user is unknown. + */ + public fun digestProvider(digest: DigestProviderFunctionV2) { + legacyDigestProviderFn = null + digestProviderFn = digest + } + + /** + * Configures a resolver for Digest `userhash` support. + * + * When configured, the server can accept hashed usernames and advertise `userhash=true` in challenges. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.userHashResolver) + * + * @param resolver resolves a user hash to the original username. + */ + public fun userHashResolver(resolver: UserHashResolverFunction) { + userHashResolverFn = resolver + } + + /** + * Enables strict RFC 7616 mode for Digest authentication. + * + * Strict mode removes deprecated MD5 algorithms and uses UTF-8. + * + * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.auth.typesafe.TypedDigestAuthConfig.strictRfc7616Mode) + */ + public fun strictRfc7616Mode() { + strictMode = true + } + + @PublishedApi + internal fun buildProvider(name: String): DigestAuthenticationProvider { + val config = DigestAuthenticationProvider.Config(name, description) + config.realm = realm + config.algorithms = algorithms + config.supportedQop = supportedQop + config.charset = charset + config.nonceManager = nonceManager + validateFn?.let { fn -> config.validate { credential -> fn(credential) } } + val digestProvider = digestProviderFn + if (digestProvider != null) { + config.digestProvider(digestProvider) + } else { + legacyDigestProviderFn?.let { config.digestProvider(it) } + } + userHashResolverFn?.let { config.userHashResolver(it) } + if (strictMode) config.strictRfc7616Mode() + return DigestAuthenticationProvider(config) + } +} diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/test/io/ktor/tests/auth/DigestTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/test/io/ktor/tests/auth/DigestTest.kt index 4949b1fab68..eb08fecaf09 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/test/io/ktor/tests/auth/DigestTest.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/test/io/ktor/tests/auth/DigestTest.kt @@ -26,6 +26,7 @@ class DigestTest { intercept(ApplicationCallPipeline.Plugins) { call.respond( UnauthorizedResponse( + @Suppress("DEPRECATION") HttpAuthHeader.digestAuthChallenge( realm = "testrealm@host.com", nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093", diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/test/io/ktor/tests/auth/typesafe/DigestAuthTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/test/io/ktor/tests/auth/typesafe/DigestAuthTest.kt new file mode 100644 index 00000000000..ee919f82102 --- /dev/null +++ b/ktor-server/ktor-server-plugins/ktor-server-auth/jvm/test/io/ktor/tests/auth/typesafe/DigestAuthTest.kt @@ -0,0 +1,121 @@ +/* + * 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.tests.auth.typesafe + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.http.auth.* +import io.ktor.server.auth.typesafe.* +import io.ktor.server.response.* +import io.ktor.server.routing.get +import io.ktor.server.testing.* +import io.ktor.util.GenerateOnlyNonceManager +import io.ktor.utils.io.ExperimentalKtorApi +import kotlin.test.Test +import kotlin.test.assertEquals + +class DigestAuthTest { + + private fun digest(algorithm: DigestAlgorithm, data: String): ByteArray = + algorithm.toDigester().digest(data.toByteArray(Charsets.UTF_8)) + + private fun String.normalize() = trimIndent().replace("\n", " ") + + private fun digestAuthHeader(uri: String, response: String): String = """ + Digest + username="Mufasa", + realm="testrealm@host.com", + nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", + uri="$uri", + qop=auth, + nc=00000001, + cnonce="0a4f113b", + response="$response", + opaque="5ccc069c403ebaf9f0171e9517f40e41" + """.normalize() + + private fun createDigestScheme(name: String) = digest(name) { + realm = "testrealm@host.com" + nonceManager = GenerateOnlyNonceManager + digestProvider { userName, realm, algorithm -> + digest(algorithm, "$userName:$realm:Circle Of Life") + } + validate { credential -> + TestUser(credential.userName, "${credential.userName}@test.com") + } + } + + @Test + fun `digest scheme returns typed principal`() = testApplication { + routing { + authenticateWith(createDigestScheme("test-digest")) { + get("/") { call.respondText("${principal.name}:${principal.email}") } + } + } + + val response = client.get("/") { + header(HttpHeaders.Authorization, digestAuthHeader("/", "d44a9a5b1ac4e32c0587816674183be6")) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("Mufasa:Mufasa@test.com", response.bodyAsText()) + } + + @Test + fun `digest scheme rejects missing credentials`() = testApplication { + routing { + authenticateWith(createDigestScheme("test-digest-reject")) { + get("/") { call.respondText(principal.name) } + } + } + + assertEquals(HttpStatusCode.Unauthorized, client.get("/").status) + } + + @Test + fun `last digest provider overload wins`() = testApplication { + @Suppress("DEPRECATION") + val md5 = DigestAlgorithm.MD5 + + fun badDigest(algorithm: DigestAlgorithm) = digest(algorithm, "bad") + + val legacyThenV2 = digest("digest-legacy-then-v2") { + realm = "testrealm@host.com" + nonceManager = GenerateOnlyNonceManager + digestProvider { _, _ -> badDigest(md5) } + digestProvider { userName, realm, algorithm -> digest(algorithm, "$userName:$realm:Circle Of Life") } + validate { credential -> TestUser(credential.userName, "${credential.userName}@test.com") } + } + + val v2ThenLegacy = digest("digest-v2-then-legacy") { + realm = "testrealm@host.com" + nonceManager = GenerateOnlyNonceManager + digestProvider { _, _, algorithm -> badDigest(algorithm) } + digestProvider { userName, realm -> digest(md5, "$userName:$realm:Circle Of Life") } + validate { credential -> TestUser(credential.userName, "${credential.userName}@test.com") } + } + + routing { + authenticateWith(legacyThenV2) { + get("/v2") { call.respondText(principal.name) } + } + authenticateWith(v2ThenLegacy) { + get("/legacy") { call.respondText(principal.name) } + } + } + + val v2Response = client.get("/v2") { + header(HttpHeaders.Authorization, digestAuthHeader("/v2", "76c83205d831e6638b673f66d4bbe8c8")) + } + assertEquals(HttpStatusCode.OK, v2Response.status) + + val legacyResponse = client.get("/legacy") { + header(HttpHeaders.Authorization, digestAuthHeader("/legacy", "8a6de65c4d8371477d9b057f111f3dbf")) + } + assertEquals(HttpStatusCode.OK, legacyResponse.status) + } +}