Skip to content
This repository was archived by the owner on Aug 9, 2022. It is now read-only.

Commit

Permalink
migrate implementation to ktor2
Browse files Browse the repository at this point in the history
  • Loading branch information
LukasForst committed Apr 10, 2022
1 parent a6b10be commit c4e7b74
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 139 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Include following in your `build.gradle.kts`:
implementation("dev.forst", "ktor-api-key", "1.0.0")
```

Versions >= `1.1.0` have implementation for Ktor >= `2.0.0`, use `1.0.0` if you need support for older versions of Ktor.

## Usage

This is minimal implementation of the Ktor app that uses API Key authentication:
Expand Down
13 changes: 8 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import java.net.URL


plugins {
kotlin("jvm") version "1.6.10"
kotlin("jvm") version "1.6.20"

`maven-publish`
signing
Expand All @@ -25,15 +25,18 @@ repositories {
dependencies {
compileOnly(kotlin("stdlib-jdk8"))
// Ktor server dependencies
val ktorVersion = "1.6.7"
val ktorVersion = "2.0.0"
compileOnly("io.ktor", "ktor-server-core", ktorVersion)
compileOnly("io.ktor", "ktor-auth", ktorVersion)
compileOnly("io.ktor", "ktor-server-auth", ktorVersion)

// testing
testImplementation("io.ktor", "ktor-server-core", ktorVersion)
testImplementation("io.ktor", "ktor-server-test-host", ktorVersion)
testImplementation("io.ktor", "ktor-auth", ktorVersion)
testImplementation("io.ktor", "ktor-jackson", ktorVersion)
testImplementation("io.ktor", "ktor-server-auth", ktorVersion)
testImplementation("io.ktor", "ktor-server-content-negotiation", ktorVersion)
testImplementation("io.ktor", "ktor-serialization-jackson", ktorVersion)
testImplementation("io.ktor", "ktor-client-content-negotiation", ktorVersion)

testImplementation(kotlin("test"))
testImplementation(kotlin("stdlib-jdk8"))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,60 @@
package dev.forst.ktor.apikey

import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.auth.Authentication
import io.ktor.auth.AuthenticationContext
import io.ktor.auth.AuthenticationFailedCause
import io.ktor.auth.AuthenticationPipeline
import io.ktor.auth.AuthenticationProcedureChallenge
import io.ktor.auth.AuthenticationProvider
import io.ktor.auth.Principal
import io.ktor.http.HttpStatusCode
import io.ktor.request.header
import io.ktor.response.respond
import io.ktor.util.pipeline.PipelineInterceptor
import io.ktor.server.application.ApplicationCall
import io.ktor.server.auth.AuthenticationConfig
import io.ktor.server.auth.AuthenticationContext
import io.ktor.server.auth.AuthenticationFailedCause
import io.ktor.server.auth.AuthenticationProvider
import io.ktor.server.auth.Principal
import io.ktor.server.request.header
import io.ktor.server.response.respond


/**
* Represents an API Key authentication provider.
* @property name is the name of the provider, or `null` for a default provider.
*/
class ApiKeyAuthenticationProvider internal constructor(
configuration: Configuration
) : AuthenticationProvider(configuration) {
internal val headerName: String = configuration.headerName
private val headerName: String = configuration.headerName

private val authenticationFunction = configuration.authenticationFunction

private val challengeFunction = configuration.challengeFunction

private val authScheme = configuration.authScheme

internal val authenticationFunction = configuration.authenticationFunction
override suspend fun onAuthenticate(context: AuthenticationContext) {
val apiKey = context.call.request.header(headerName)
val principal = apiKey?.let { authenticationFunction(context.call, it) }

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

internal val challengeFunction = configuration.challengeFunction
if (cause != null) {
context.challenge(authScheme, cause) { challenge, call ->
challengeFunction(call)

internal val authScheme = configuration.authScheme
challenge.complete()
}
}
if (principal != null) {
context.principal(principal)
}
}

/**
* Api key auth configuration.
*/
class Configuration internal constructor(name: String?) : AuthenticationProvider.Configuration(name) {
class Configuration internal constructor(name: String?) : Config(name) {

internal lateinit var authenticationFunction: ApiKeyAuthenticationFunction

internal var challengeFunction: ApiKeyAuthChallengeFunction = {
internal var challengeFunction: ApiKeyAuthChallengeFunction = { call ->
call.respond(HttpStatusCode.Unauthorized)
}

Expand Down Expand Up @@ -70,36 +88,11 @@ class ApiKeyAuthenticationProvider internal constructor(
/**
* Installs API Key authentication mechanism.
*/
fun Authentication.Configuration.apiKey(
fun AuthenticationConfig.apiKey(
name: String? = null,
configure: ApiKeyAuthenticationProvider.Configuration.() -> Unit
) {
val provider = ApiKeyAuthenticationProvider(ApiKeyAuthenticationProvider.Configuration(name).apply(configure))
val headerName = provider.headerName
val authenticate = provider.authenticationFunction
val authScheme = provider.authScheme
val challenge = provider.challengeFunction

provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
val apiKey = call.request.header(headerName)
val principal = apiKey?.let { authenticate(call, it) }

val cause = when {
apiKey == null -> AuthenticationFailedCause.NoCredentials
principal == null -> AuthenticationFailedCause.InvalidCredentials
else -> null
}
if (cause != null) {
context.challenge(authScheme, cause) {
challenge.invoke(this, it)
it.complete()
}
}
if (principal != null) {
context.principal(principal)
}
}

register(provider)
}

Expand All @@ -111,4 +104,4 @@ typealias ApiKeyAuthenticationFunction = suspend ApplicationCall.(String) -> Pri
/**
* Alias for function signature that is called when authentication fails.
*/
typealias ApiKeyAuthChallengeFunction = PipelineInterceptor<AuthenticationProcedureChallenge, ApplicationCall>
typealias ApiKeyAuthChallengeFunction = suspend (ApplicationCall) -> Unit
20 changes: 10 additions & 10 deletions src/test/kotlin/dev/forst/ktor/apikey/MinimalExampleApp.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package dev.forst.ktor.apikey

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.auth.Principal
import io.ktor.auth.authenticate
import io.ktor.auth.principal
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.Principal
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.principal
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing

/**
* Minimal Ktor application with API Key authentication.
Expand Down
107 changes: 57 additions & 50 deletions src/test/kotlin/dev/forst/ktor/apikey/TestApiKeyAuth.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package dev.forst.ktor.apikey

import io.ktor.application.call
import io.ktor.auth.Principal
import io.ktor.http.HttpMethod
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.server.testing.handleRequest
import io.ktor.server.testing.withTestApplication
import io.ktor.serialization.jackson.jackson
import io.ktor.server.auth.Principal
import io.ktor.server.response.respond
import io.ktor.server.testing.testApplication
import org.junit.jupiter.api.Test
import java.util.UUID
import kotlin.test.assertEquals
Expand All @@ -25,24 +27,23 @@ class TestApiKeyAuth {
validate { header -> header.takeIf { it == apiKey }?.let { ApiKeyPrincipal(it) } }
}

withTestApplication(module) {
handleRequest(HttpMethod.Get, Routes.open).apply {
assertEquals(HttpStatusCode.OK, response.status())
}
testApplication {
application(module)
val client = createClient { install(ContentNegotiation) { jackson() } }

var response = client.get(Routes.open)
assertEquals(HttpStatusCode.OK, response.status)

handleRequest(HttpMethod.Get, Routes.open) {
addHeader(defaultHeader, apiKey)
}.apply {
assertEquals(HttpStatusCode.OK, response.status())
response = client.get(Routes.open) {
header(defaultHeader, apiKey)
}
assertEquals(HttpStatusCode.OK, response.status)

handleRequest(HttpMethod.Get, Routes.open) {
addHeader(defaultHeader, "$apiKey-wrong")
}.apply {
assertEquals(HttpStatusCode.OK, response.status())
response = client.get(Routes.open) {
header(defaultHeader, "${apiKey}-wrong")
}
assertEquals(HttpStatusCode.OK, response.status)
}

}

@Test
Expand All @@ -53,20 +54,23 @@ class TestApiKeyAuth {
validate { header -> header.takeIf { it == apiKey }?.let { ApiKeyPrincipal(it) } }
}

withTestApplication(module) {
handleRequest(HttpMethod.Get, Routes.authenticated) {
addHeader(defaultHeader, apiKey)
}.apply {
assertEquals(HttpStatusCode.OK, response.status())
val principal = receive<ApiKeyPrincipal>()
assertEquals(principal, ApiKeyPrincipal(apiKey))
testApplication {
application(module)
val client = createClient { install(ContentNegotiation) { jackson() } }

// correct header
val response = client.get(Routes.authenticated) {
header(defaultHeader, apiKey)
}
assertEquals(HttpStatusCode.OK, response.status)
val principal = response.body<ApiKeyPrincipal>()
assertEquals(principal, ApiKeyPrincipal(apiKey))

handleRequest(HttpMethod.Get, Routes.authenticated) {
addHeader(defaultHeader, "$apiKey-wrong")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, response.status())
// incorrect header
val unauthorizedResponse = client.get(Routes.authenticated) {
header(defaultHeader, "${apiKey}-wrong")
}
assertEquals(HttpStatusCode.Unauthorized, unauthorizedResponse.status)
}
}

Expand All @@ -80,18 +84,19 @@ class TestApiKeyAuth {

val module = buildApplicationModule {
headerName = header
challenge { call.respond(errorStatus) }
challenge { call -> call.respond(errorStatus) }
validate { header -> header.takeIf { it == apiKey }?.let { ApiKeyPrincipal(it) } }
}
testApplication {
application(module)
val client = createClient { install(ContentNegotiation) { jackson() } }

withTestApplication(module) {
handleRequest(HttpMethod.Get, Routes.authenticated) {
addHeader(header, apiKey)
}.apply {
assertEquals(HttpStatusCode.OK, response.status())
val principal = receive<ApiKeyPrincipal>()
assertEquals(principal, ApiKeyPrincipal(apiKey))
val response = client.get(Routes.authenticated) {
header(header, apiKey)
}
assertEquals(HttpStatusCode.OK, response.status)
val principal = response.body<ApiKeyPrincipal>()
assertEquals(principal, ApiKeyPrincipal(apiKey))
}
}

Expand All @@ -103,24 +108,26 @@ class TestApiKeyAuth {

val module = buildApplicationModule {
headerName = header
challenge { call.respond(errorStatus) }
challenge { call -> call.respond(errorStatus) }
validate { header -> header.takeIf { it == apiKey }?.let { ApiKeyPrincipal(it) } }
}
testApplication {
application(module)
val client = createClient { install(ContentNegotiation) { jackson() } }

withTestApplication(module) {
handleRequest(HttpMethod.Get, Routes.authenticated) {
addHeader(header, apiKey)
}.apply {
assertEquals(HttpStatusCode.OK, response.status())
val principal = receive<ApiKeyPrincipal>()
assertEquals(principal, ApiKeyPrincipal(apiKey))
// correct header
val response = client.get(Routes.authenticated) {
header(header, apiKey)
}
assertEquals(HttpStatusCode.OK, response.status)
val principal = response.body<ApiKeyPrincipal>()
assertEquals(principal, ApiKeyPrincipal(apiKey))

handleRequest(HttpMethod.Get, Routes.authenticated) {
addHeader(header, "$apiKey-wrong")
}.apply {
assertEquals(errorStatus, response.status())
// incorrect header
val unauthorizedResponse = client.get(Routes.authenticated) {
header(header, "${apiKey}-wrong")
}
assertEquals(errorStatus, unauthorizedResponse.status)
}
}
}
30 changes: 15 additions & 15 deletions src/test/kotlin/dev/forst/ktor/apikey/TestApplication.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package dev.forst.ktor.apikey

import io.ktor.application.Application
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.auth.Principal
import io.ktor.auth.authenticate
import io.ktor.auth.principal
import io.ktor.features.ContentNegotiation
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.serialization.jackson.jackson
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.Principal
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.principal
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.ktor.util.pipeline.PipelineContext

const val apiKeyAuth = "api-key"
Expand Down
Loading

0 comments on commit c4e7b74

Please sign in to comment.