Skip to content

Commit 9c87faf

Browse files
authored
Added api key auth module to be used between API's (#1)
1 parent e9e15d2 commit 9c87faf

File tree

9 files changed

+288
-30
lines changed

9 files changed

+288
-30
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
66
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
77

8+
# kotlinttest
9+
.kotlintest
10+
811
# User-specific stuff
912
.idea/**/workspace.xml
1013
.idea/**/tasks.xml

build.gradle.kts

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
88
plugins {
99
kotlin("jvm") version "1.3.21"
1010
id("org.jetbrains.dokka") version "0.9.17" apply false
11+
id("com.diffplug.gradle.spotless") version "3.13.0"
1112
`java-library`
1213
`maven-publish`
1314
}
@@ -30,6 +31,7 @@ subprojects {
3031
val scmUrl = "scm:git:https://github.com/navikt/dp-biblioteker.git"
3132

3233
apply(plugin = "org.jetbrains.kotlin.jvm")
34+
apply(plugin = "com.diffplug.gradle.spotless")
3335
apply(plugin = "java-library")
3436
apply(plugin = "org.jetbrains.dokka")
3537
apply(plugin = "maven-publish")
@@ -42,6 +44,9 @@ subprojects {
4244
testCompile("org.junit.jupiter:junit-jupiter-params:$junitJupiterVersion")
4345
testRuntime("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
4446
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:$junitJupiterVersion")
47+
testImplementation("io.kotlintest:kotlintest-runner-junit5:3.3.0")
48+
testImplementation(kotlin("test"))
49+
testImplementation(kotlin("test-junit"))
4550
}
4651

4752

@@ -95,6 +100,17 @@ subprojects {
95100
add("archives", javadocJar)
96101
}
97102

103+
104+
spotless {
105+
kotlin {
106+
ktlint()
107+
}
108+
kotlinGradle {
109+
target("*.gradle.kts", "additionalScripts/*.gradle.kts")
110+
ktlint("0.31.0")
111+
}
112+
}
113+
98114
publishing {
99115
publications {
100116
create<MavenPublication>("maven") {

ktor-utils/build.gradle.kts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
val ktorVersion = "1.2.0"
2+
3+
dependencies {
4+
implementation("io.ktor:ktor-server:$ktorVersion")
5+
implementation("io.ktor:ktor-auth:$ktorVersion")
6+
testImplementation("io.ktor:ktor-server-test-host:$ktorVersion")
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package no.nav.dagpenger.ktor.auth
2+
3+
import io.ktor.application.ApplicationCall
4+
import io.ktor.application.call
5+
import io.ktor.auth.Authentication
6+
import io.ktor.auth.AuthenticationFailedCause
7+
import io.ktor.auth.AuthenticationPipeline
8+
import io.ktor.auth.AuthenticationProvider
9+
import io.ktor.auth.Credential
10+
import io.ktor.auth.Principal
11+
import io.ktor.auth.UnauthorizedResponse
12+
import io.ktor.http.auth.HeaderValueEncoding
13+
import io.ktor.http.auth.HttpAuthHeader
14+
import io.ktor.request.ApplicationRequest
15+
import io.ktor.response.respond
16+
17+
enum class ApiKeyLocation(val location: String) {
18+
QUERY("query"),
19+
HEADER("header")
20+
}
21+
22+
data class ApiKeyCredential(val value: String) : Credential
23+
data class ApiPrincipal(val apiKeyCredential: ApiKeyCredential?) : Principal
24+
25+
/**
26+
* Represents a Api Key authentication provider
27+
* @param name is the name of the provider, or `null` for a default provider
28+
*/
29+
30+
class ApiKeyAuthenticationProvider internal constructor(config: Configuration) : AuthenticationProvider(config) {
31+
32+
internal var apiKeyName: String = config.apiKeyName
33+
internal var apiKeyLocation: ApiKeyLocation = config.apiKeyLocation
34+
internal val authenticationFunction = config.authenticationFunction
35+
36+
class Configuration(name: String?) : AuthenticationProvider.Configuration(name) {
37+
internal var authenticationFunction: suspend ApplicationCall.(ApiKeyCredential) -> Principal? = { null }
38+
39+
var apiKeyName: String = ""
40+
41+
var apiKeyLocation: ApiKeyLocation = ApiKeyLocation.HEADER
42+
43+
fun validate(body: suspend ApplicationCall.(ApiKeyCredential) -> Principal?) {
44+
authenticationFunction = body
45+
}
46+
47+
internal fun build() = ApiKeyAuthenticationProvider(this)
48+
}
49+
}
50+
51+
fun Authentication.Configuration.apiKeyAuth(
52+
name: String? = null,
53+
configure: ApiKeyAuthenticationProvider.Configuration.() -> Unit
54+
) {
55+
val provider = ApiKeyAuthenticationProvider.Configuration(name).apply(configure).build()
56+
val apiKeyName = provider.apiKeyName
57+
val apiKeyLocation = provider.apiKeyLocation
58+
val authenticate = provider.authenticationFunction
59+
60+
provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
61+
val credentials = call.request.apiKeyAuthenticationCredentials(apiKeyName, apiKeyLocation)
62+
val principal = credentials?.let { authenticate(call, it) }
63+
64+
val cause = when {
65+
credentials == null -> AuthenticationFailedCause.NoCredentials
66+
principal == null -> AuthenticationFailedCause.InvalidCredentials
67+
else -> null
68+
}
69+
70+
if (cause != null) {
71+
context.challenge(apiKeyName, cause) {
72+
call.respond(
73+
UnauthorizedResponse(
74+
HttpAuthHeader.Parameterized(
75+
"API_KEY",
76+
mapOf("key" to apiKeyName),
77+
HeaderValueEncoding.QUOTED_ALWAYS
78+
)
79+
)
80+
)
81+
it.complete()
82+
}
83+
}
84+
85+
if (principal != null) {
86+
context.principal(principal)
87+
}
88+
}
89+
90+
register(provider)
91+
}
92+
93+
fun ApplicationRequest.apiKeyAuthenticationCredentials(
94+
apiKeyName: String,
95+
apiKeyLocation: ApiKeyLocation
96+
): ApiKeyCredential? {
97+
return when (val value: String? = when (apiKeyLocation) {
98+
ApiKeyLocation.QUERY -> this.queryParameters[apiKeyName]
99+
ApiKeyLocation.HEADER -> this.headers[apiKeyName]
100+
}) {
101+
null -> null
102+
else -> ApiKeyCredential(value)
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package no.nav.dagpenger.ktor.auth
2+
3+
import io.kotlintest.shouldBe
4+
import io.ktor.application.Application
5+
import io.ktor.application.call
6+
import io.ktor.application.install
7+
import io.ktor.auth.Authentication
8+
import io.ktor.auth.authenticate
9+
import io.ktor.http.HttpMethod
10+
import io.ktor.http.HttpStatusCode
11+
import io.ktor.response.respondText
12+
import io.ktor.routing.get
13+
import io.ktor.routing.route
14+
import io.ktor.routing.routing
15+
import io.ktor.server.testing.handleRequest
16+
import io.ktor.server.testing.withTestApplication
17+
import org.junit.jupiter.api.Test
18+
19+
class ApiKeyAuthTest {
20+
21+
@Test
22+
fun `Request with api key in header ok`() {
23+
withTestApplication({
24+
apiKeyHeader()
25+
}) {
26+
handleRequest(HttpMethod.Get, "/foo") {
27+
addHeader("X-API-KEY", "test")
28+
}.apply {
29+
response.status() shouldBe HttpStatusCode.OK
30+
}
31+
}
32+
}
33+
34+
@Test
35+
fun `Request with no api key in header is unauthorized`() {
36+
withTestApplication({
37+
apiKeyHeader()
38+
}) {
39+
handleRequest(HttpMethod.Get, "/foo") {
40+
}.apply {
41+
response.status() shouldBe HttpStatusCode.Unauthorized
42+
}
43+
}
44+
}
45+
46+
@Test
47+
fun `Request with api key in query ok`() {
48+
withTestApplication({
49+
apiKeyQuery()
50+
}) {
51+
handleRequest(HttpMethod.Get, "/foo?apiKey=test") {
52+
}.apply {
53+
response.status() shouldBe HttpStatusCode.OK
54+
}
55+
}
56+
}
57+
58+
@Test
59+
fun `Request with no api key in query is unauthorized`() {
60+
withTestApplication({
61+
apiKeyQuery()
62+
}) {
63+
handleRequest(HttpMethod.Get, "/foo") {
64+
}.apply {
65+
response.status() shouldBe HttpStatusCode.Unauthorized
66+
}
67+
}
68+
}
69+
}
70+
71+
fun Application.apiKeyHeader() {
72+
install(Authentication) {
73+
apiKeyAuth {
74+
apiKeyName = "X-API-KEY"
75+
validate { apikeyCredential: ApiKeyCredential ->
76+
when {
77+
apikeyCredential.value == "test" -> ApiPrincipal(apikeyCredential)
78+
else -> null
79+
}
80+
}
81+
}
82+
}
83+
84+
routing {
85+
authenticate {
86+
route("/foo") {
87+
get {
88+
call.respondText { "bar" }
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
fun Application.apiKeyQuery() {
96+
install(Authentication) {
97+
apiKeyAuth {
98+
apiKeyName = "apiKey"
99+
apiKeyLocation = ApiKeyLocation.QUERY
100+
validate { apikeyCredential: ApiKeyCredential ->
101+
when {
102+
apikeyCredential.value == "test" -> ApiPrincipal(apikeyCredential)
103+
else -> null
104+
}
105+
}
106+
}
107+
}
108+
109+
routing {
110+
authenticate {
111+
route("/foo") {
112+
get {
113+
call.respondText { "bar" }
114+
}
115+
}
116+
}
117+
}
118+
}

settings.gradle.kts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
rootProject.name = "dp-biblioteker"
22

3-
include("sts-klient")
3+
include("sts-klient")
4+
include("ktor-utils")

sts-klient/build.gradle.kts

-2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,3 @@ dependencies {
55
implementation("com.github.kittinunf.fuel:fuel-gson:$fuelVersion")
66
testImplementation("com.github.tomakehurst:wiremock-standalone:2.19.0")
77
}
8-
9-

sts-klient/src/main/kotlin/no/nav/dagpenger/oidc/StsOidcClient.kt

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package no.nav.dagpenger.oidc
22

3-
43
import com.github.kittinunf.fuel.gson.responseObject
54
import com.github.kittinunf.fuel.httpGet
65
import com.github.kittinunf.result.Result
@@ -12,11 +11,14 @@ import java.time.LocalDateTime.now
1211
*/
1312
class StsOidcClient(stsBaseUrl: String, private val username: String, private val password: String) : OidcClient {
1413
private val timeToRefresh: Long = 60
15-
private val stsTokenUrl: String = if (stsBaseUrl.endsWith("/")) "${stsBaseUrl}rest/v1/sts/token/" else "$stsBaseUrl/rest/v1/sts/token/"
14+
private val stsTokenUrl: String =
15+
if (stsBaseUrl.endsWith("/")) "${stsBaseUrl}rest/v1/sts/token/" else "$stsBaseUrl/rest/v1/sts/token/"
1616

17-
@Volatile private var tokenExpiryTime = now().minus(Duration.ofSeconds(timeToRefresh))
17+
@Volatile
18+
private var tokenExpiryTime = now().minus(Duration.ofSeconds(timeToRefresh))
1819

19-
@Volatile private lateinit var oidcToken: OidcToken
20+
@Volatile
21+
private lateinit var oidcToken: OidcToken
2022

2123
override fun oidcToken(): OidcToken {
2224
return if (now().isBefore(tokenExpiryTime)) {
@@ -44,7 +46,8 @@ class StsOidcClient(stsBaseUrl: String, private val username: String, private va
4446
}
4547
}
4648

47-
class StsOidcClientException(override val message: String, override val cause: Throwable) : RuntimeException(message, cause)
49+
class StsOidcClientException(override val message: String, override val cause: Throwable) :
50+
RuntimeException(message, cause)
4851

4952
data class OidcToken(
5053
val access_token: String,

0 commit comments

Comments
 (0)