Skip to content

Commit ff288c2

Browse files
committed
custom token auth for ktor
1 parent 14d38f0 commit ff288c2

File tree

7 files changed

+189
-16
lines changed

7 files changed

+189
-16
lines changed

build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,23 @@ dependencies {
3131

3232
// Ktor
3333
val ktorVersion = "2.0.1"
34+
implementation("io.ktor:ktor-server-auth:$ktorVersion")
3435
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
3536
implementation("io.ktor:ktor-serialization-jackson:$ktorVersion")
3637

3738
// Testing
3839
testImplementation(kotlin("test"))
40+
testImplementation("io.ktor:ktor-server-test-host:$ktorVersion")
3941
}
4042

4143
tasks.withType<KotlinCompile> {
4244
kotlinOptions.jvmTarget = "17"
4345
}
4446

47+
tasks.test {
48+
useJUnitPlatform()
49+
}
50+
4551
tasks.withType<Jar> {
4652
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
4753
manifest {

src/main/kotlin/no/nav/hjelpemidler/Application.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package no.nav.hjelpemidler
33
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
44
import io.ktor.serialization.jackson.jackson
55
import io.ktor.server.application.install
6+
import io.ktor.server.auth.Authentication
7+
import io.ktor.server.auth.authenticate
68
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
79
import io.ktor.server.routing.routing
810
import mu.KotlinLogging
@@ -51,10 +53,19 @@ fun main() {
5153
registerModule(JavaTimeModule())
5254
}
5355
}
56+
install(Authentication) {
57+
token("oebsToken") {
58+
validate(requireNotNull(Configuration.application["OEBSTOKEN"]) {
59+
"OEBSTOKEN mangler"
60+
})
61+
}
62+
}
5463
val context = Context(rapidApp)
5564
routing {
56-
ordrelinjeAPI(context)
57-
serviceforespørselAPI(context)
65+
authenticate("oebsToken") {
66+
ordrelinjeAPI(context)
67+
serviceforespørselAPI(context)
68+
}
5869
}
5970
}.build()
6071

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package no.nav.hjelpemidler
2+
3+
import io.ktor.http.auth.AuthScheme
4+
import io.ktor.http.auth.HttpAuthHeader
5+
import io.ktor.server.application.ApplicationCall
6+
import io.ktor.server.auth.AuthenticationConfig
7+
import io.ktor.server.auth.AuthenticationContext
8+
import io.ktor.server.auth.AuthenticationFailedCause
9+
import io.ktor.server.auth.AuthenticationFunction
10+
import io.ktor.server.auth.AuthenticationProvider
11+
import io.ktor.server.auth.Principal
12+
import io.ktor.server.auth.UnauthorizedResponse
13+
import io.ktor.server.auth.parseAuthorizationHeader
14+
import io.ktor.server.request.ApplicationRequest
15+
import io.ktor.server.response.respond
16+
17+
fun AuthenticationConfig.token(
18+
name: String? = null,
19+
configure: TokenAuthenticationProvider.Config.() -> Unit,
20+
) {
21+
val provider = TokenAuthenticationProvider(TokenAuthenticationProvider.Config(name).apply(configure))
22+
register(provider)
23+
}
24+
25+
fun ApplicationRequest.tokenCredentials(): TokenCredential? {
26+
when (val authHeader = parseAuthorizationHeader()) {
27+
is HttpAuthHeader.Single -> {
28+
if (!authHeader.authScheme.equals(AuthScheme.Bearer, ignoreCase = true)) {
29+
return null
30+
}
31+
return TokenCredential(authHeader.blob)
32+
}
33+
else -> return null
34+
}
35+
}
36+
37+
class TokenAuthenticationProvider internal constructor(config: Config) : AuthenticationProvider(config) {
38+
internal val authenticationFunction = config.authenticationFunction
39+
40+
override suspend fun onAuthenticate(context: AuthenticationContext) {
41+
val call = context.call
42+
val credentials = call.request.tokenCredentials()
43+
val principal = credentials?.let { authenticationFunction(call, it) }
44+
val cause = when {
45+
credentials == null -> AuthenticationFailedCause.NoCredentials
46+
principal == null -> AuthenticationFailedCause.InvalidCredentials
47+
else -> null
48+
}
49+
if (cause != null) {
50+
@Suppress("NAME_SHADOWING")
51+
context.challenge("", cause) { challenge, call ->
52+
call.respond(
53+
UnauthorizedResponse(
54+
HttpAuthHeader.Parameterized(
55+
AuthScheme.Bearer,
56+
mapOf("realm" to "Ktor Server")
57+
)
58+
)
59+
)
60+
challenge.complete()
61+
}
62+
}
63+
if (principal != null) {
64+
context.principal(principal)
65+
}
66+
}
67+
68+
class Config internal constructor(name: String?) : AuthenticationProvider.Config(name) {
69+
internal var authenticationFunction: AuthenticationFunction<TokenCredential> = {
70+
throw NotImplementedError(
71+
"Token validate function is not specified. Use token { validate { ... } } to fix."
72+
)
73+
}
74+
75+
fun validate(body: suspend ApplicationCall.(TokenCredential) -> Principal?) {
76+
authenticationFunction = body
77+
}
78+
79+
fun validate(expectedToken: String) = validate {
80+
when (it.token) {
81+
expectedToken -> TokenPrincipal(it.token)
82+
else -> null
83+
}
84+
}
85+
}
86+
}
87+
88+
data class TokenCredential(val token: String)
89+
90+
data class TokenPrincipal(val token: String) : Principal

src/main/kotlin/no/nav/hjelpemidler/api/OrdrelinjeAPI.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@ private val mapperXml = XmlMapper().registerModule(JavaTimeModule())
3636
internal fun Route.ordrelinjeAPI(context: Context) {
3737
post("/push") {
3838
logg.info("incoming push")
39-
val authHeader = call.request.header("Authorization").toString()
40-
if (!authHeader.startsWith("Bearer ") || authHeader.substring(7) != Configuration.application["OEBSTOKEN"]!!) {
41-
call.respond(HttpStatusCode.Unauthorized, "unauthorized")
42-
return@post
43-
}
44-
4539
try {
4640
val ordrelinje = parseOrdrelinje(context, call) ?: return@post
4741
sendUvalidertOrdrelinjeTilRapid(context, ordrelinje.toRåOrdrelinje())

src/main/kotlin/no/nav/hjelpemidler/api/ServiceForespørselAPI.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
66
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
77
import io.ktor.http.HttpStatusCode
88
import io.ktor.server.application.call
9-
import io.ktor.server.request.header
109
import io.ktor.server.request.receive
1110
import io.ktor.server.response.respond
1211
import io.ktor.server.routing.Route
1312
import io.ktor.server.routing.post
1413
import mu.KotlinLogging
1514
import no.nav.hjelpemidler.Context
16-
import no.nav.hjelpemidler.configuration.Configuration
1715
import no.nav.hjelpemidler.model.SfMessage
1816
import java.time.LocalDateTime
1917
import java.util.UUID
@@ -25,12 +23,6 @@ private val mapperJson = jacksonMapperBuilder().addModule(JavaTimeModule()).buil
2523
internal fun Route.serviceforespørselAPI(context: Context) {
2624
post("/sf") {
2725
logg.info("incoming sf-oppdatering")
28-
val authHeader = call.request.header("Authorization").toString()
29-
if (!authHeader.startsWith("Bearer ") || authHeader.substring(7) != Configuration.application["OEBSTOKEN"]!!) {
30-
call.respond(HttpStatusCode.Unauthorized, "unauthorized")
31-
return@post
32-
}
33-
3426
try {
3527
val serviceForespørselEndring = call.receive<ServiceForespørselEndring>()
3628
val sfMessage = SfMessage(
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package no.nav.hjelpemidler
2+
3+
import kotlin.test.assertEquals
4+
import kotlin.test.assertNotEquals
5+
6+
infix fun <T> T.shouldBe(expected: T) = assertEquals(expected, this)
7+
infix fun <T> T.shouldNotBe(illegal: T) = assertNotEquals(illegal, this)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package no.nav.hjelpemidler
2+
3+
import io.ktor.client.request.basicAuth
4+
import io.ktor.client.request.bearerAuth
5+
import io.ktor.client.request.get
6+
import io.ktor.client.statement.bodyAsText
7+
import io.ktor.http.HttpStatusCode
8+
import io.ktor.server.application.call
9+
import io.ktor.server.auth.Authentication
10+
import io.ktor.server.auth.authenticate
11+
import io.ktor.server.response.respondText
12+
import io.ktor.server.routing.get
13+
import io.ktor.server.testing.ApplicationTestBuilder
14+
import io.ktor.server.testing.testApplication
15+
import kotlin.test.Test
16+
17+
internal class TokenAuthenticationProviderTest {
18+
19+
@Test
20+
internal fun `authentication fails, missing token`() = testApplication {
21+
configure()
22+
client.get("/secured") {
23+
}.apply {
24+
status shouldBe HttpStatusCode.Unauthorized
25+
}
26+
}
27+
28+
@Test
29+
internal fun `authentication fails, wrong token`() = testApplication {
30+
configure()
31+
client.get("/secured") {
32+
bearerAuth("1234qwer")
33+
}.apply {
34+
status shouldBe HttpStatusCode.Unauthorized
35+
}
36+
}
37+
38+
@Test
39+
internal fun `authentication fails, wrong scheme`() = testApplication {
40+
configure()
41+
client.get("/secured") {
42+
basicAuth("foo", "bar")
43+
}.apply {
44+
status shouldBe HttpStatusCode.Unauthorized
45+
}
46+
}
47+
48+
@Test
49+
internal fun `authentication succeeds`() = testApplication {
50+
configure()
51+
client.get("/secured") {
52+
bearerAuth("qwer1234")
53+
}.apply {
54+
status shouldBe HttpStatusCode.OK
55+
bodyAsText() shouldBe "secret"
56+
}
57+
}
58+
59+
private fun ApplicationTestBuilder.configure() {
60+
install(Authentication) {
61+
token("oebsToken") {
62+
validate("qwer1234")
63+
}
64+
}
65+
routing {
66+
authenticate("oebsToken") {
67+
get("/secured") {
68+
call.respondText("secret")
69+
}
70+
}
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)