init security scheme (#29)

This commit is contained in:
dpnolte
2021-04-21 19:51:42 +02:00
committed by GitHub
parent 8a64925c9d
commit d0767aa74e
21 changed files with 835 additions and 9 deletions

View File

@ -0,0 +1,40 @@
plugins {
`java-library`
`maven-publish`
}
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation(libs.bundles.ktor)
implementation(libs.bundles.ktorAuth)
implementation(projects.kompendiumCore)
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")
testImplementation("io.ktor:ktor-server-test-host:1.5.3")
}
java {
withSourcesJar()
}
publishing {
repositories {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/lg-backbone/kompendium")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("kompendium") {
from(components["kotlin"])
artifact(tasks.sourcesJar)
}
}
}

View File

@ -0,0 +1,19 @@
package org.leafygreens.kompendium.auth
import io.ktor.auth.AuthenticationRouteSelector
import io.ktor.routing.Route
import org.leafygreens.kompendium.path.CorePathCalculator
import org.slf4j.LoggerFactory
class AuthPathCalculator : CorePathCalculator() {
private val logger = LoggerFactory.getLogger(javaClass)
override fun handleCustomSelectors(route: Route, tail: String): String = when (route.selector) {
is AuthenticationRouteSelector -> {
logger.debug("Found authentication route selector ${route.selector}")
super.calculate(route.parent, tail)
}
else -> super.handleCustomSelectors(route, tail)
}
}

View File

@ -0,0 +1,50 @@
package org.leafygreens.kompendium.auth
import io.ktor.auth.Authentication
import io.ktor.auth.basic
import io.ktor.auth.BasicAuthenticationProvider
import io.ktor.auth.jwt.jwt
import io.ktor.auth.jwt.JWTAuthenticationProvider
import org.leafygreens.kompendium.Kompendium
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaSecurity
object KompendiumAuth {
init {
Kompendium.pathCalculator = AuthPathCalculator()
}
fun Authentication.Configuration.notarizedBasic(
name: String? = null,
configure: BasicAuthenticationProvider.Configuration.() -> Unit
) {
Kompendium.openApiSpec.components.securitySchemes[name ?: "default"] = OpenApiSpecSchemaSecurity(
type = "http",
scheme = "basic"
)
basic(name, configure)
}
fun Authentication.Configuration.notarizedJwt(
name: String? = null,
header: String? = null,
scheme: String? = null,
configure: JWTAuthenticationProvider.Configuration.() -> Unit
) {
if (header == null || header == "Authorization") {
Kompendium.openApiSpec.components.securitySchemes[name ?: "default"] = OpenApiSpecSchemaSecurity(
type = "http",
scheme = scheme ?: "bearer"
)
} else {
Kompendium.openApiSpec.components.securitySchemes[name ?: "default"] = OpenApiSpecSchemaSecurity(
type = "apiKey",
name = header,
`in` = "header"
)
}
jwt(name, configure)
}
// TODO support other authentication providers (e.g., oAuth)?
}

View File

@ -0,0 +1,197 @@
package org.leafygreens.kompendium.auth
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.jackson.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.testing.*
import org.junit.Test
import org.leafygreens.kompendium.Kompendium
import org.leafygreens.kompendium.Kompendium.notarizedGet
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedJwt
import org.leafygreens.kompendium.auth.util.TestData
import org.leafygreens.kompendium.auth.util.TestParams
import org.leafygreens.kompendium.auth.util.TestResponse
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.routes.openApi
import org.leafygreens.kompendium.routes.redoc
import org.leafygreens.kompendium.util.KompendiumHttpCodes
import kotlin.test.AfterTest
import kotlin.test.assertEquals
internal class KompendiumAuthTest {
@AfterTest
fun `reset kompendium`() {
Kompendium.openApiSpec = OpenApiSpec(
info = OpenApiSpecInfo(),
servers = mutableListOf(),
paths = mutableMapOf()
)
Kompendium.cache = emptyMap()
}
@Test
fun `Notarized Get with basic authentication records all expected information`() {
withTestApplication({
configModule()
configBasicAuth()
docs()
notarizedAuthenticatedGetModule(TestData.AuthConfigName.Basic)
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_basic_authenticated_get.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Get with jwt authentication records all expected information`() {
withTestApplication({
configModule()
configJwtAuth()
docs()
notarizedAuthenticatedGetModule(TestData.AuthConfigName.JWT)
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_jwt_authenticated_get.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Get with jwt authentication and custom scheme records all expected information`() {
withTestApplication({
configModule()
configJwtAuth(scheme = "oauth")
docs()
notarizedAuthenticatedGetModule(TestData.AuthConfigName.JWT)
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_jwt_custom_scheme_authenticated_get.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Get with jwt authentication and custom header records all expected information`() {
withTestApplication({
configModule()
configJwtAuth(header = "x-api-key")
docs()
notarizedAuthenticatedGetModule(TestData.AuthConfigName.JWT)
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_jwt_custom_header_authenticated_get.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Notarized Get with multiple jwt schemes records all expected information`() {
withTestApplication({
configModule()
install(Authentication) {
notarizedJwt("jwt1", header = "x-api-key-1") {
realm = "Ktor server"
}
notarizedJwt("jwt2", header = "x-api-key-2") {
realm = "Ktor server"
}
}
docs()
notarizedAuthenticatedGetModule("jwt1", "jwt2")
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_multiple_jwt_authenticated_get.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
private fun Application.configModule() {
install(ContentNegotiation) {
jackson(ContentType.Application.Json) {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
}
private fun Application.configBasicAuth() {
install(Authentication) {
notarizedBasic(TestData.AuthConfigName.Basic) {
realm = "Ktor Server"
validate { credentials ->
if (credentials.name == credentials.password) {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
}
private fun Application.configJwtAuth(
header: String? = null,
scheme: String? = null
) {
install(Authentication) {
notarizedJwt(TestData.AuthConfigName.JWT, header, scheme) {
realm = "Ktor server"
}
}
}
private fun Application.notarizedAuthenticatedGetModule(vararg authenticationConfigName: String) {
routing {
authenticate(*authenticationConfigName) {
route(TestData.getRoutePath) {
notarizedGet<TestParams, TestResponse>(testGetInfo(*authenticationConfigName)) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
}
private val oas = Kompendium.openApiSpec.copy()
private fun Application.docs() {
routing {
openApi(oas)
redoc(oas)
}
}
private companion object {
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
fun testGetInfo(vararg security: String) =
MethodInfo("Another get test", "testing more", testGetResponse, securitySchemes = security.toSet())
}
}

View File

@ -0,0 +1,18 @@
package org.leafygreens.kompendium.auth.util
import java.io.File
object TestData {
object AuthConfigName {
val Basic = "basic"
val JWT = "jwt"
}
val getRoutePath = "/test"
fun getFileSnapshot(fileName: String): String {
val snapshotPath = "src/test/resources"
val file = File("$snapshotPath/$fileName")
return file.readText()
}
}

View File

@ -0,0 +1,19 @@
package org.leafygreens.kompendium.auth.util
import org.leafygreens.kompendium.annotations.KompendiumField
import org.leafygreens.kompendium.annotations.PathParam
import org.leafygreens.kompendium.annotations.QueryParam
data class TestParams(
@PathParam val a: String,
@QueryParam val aa: Int
)
data class TestRequest(
@KompendiumField(name = "field_name")
val b: Double,
val aaa: List<Long>
)
data class TestResponse(val c: String)

View File

@ -0,0 +1,74 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"$ref" : "#/components/schemas/String"
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"$ref" : "#/components/schemas/Int"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated" : false,
"security" : [ {
"basic" : [ ]
} ]
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
}
},
"securitySchemes" : {
"basic" : {
"type" : "http",
"scheme" : "basic"
}
}
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,74 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"$ref" : "#/components/schemas/String"
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"$ref" : "#/components/schemas/Int"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated" : false,
"security" : [ {
"jwt" : [ ]
} ]
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
}
},
"securitySchemes" : {
"jwt" : {
"type" : "http",
"scheme" : "bearer"
}
}
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,75 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"$ref" : "#/components/schemas/String"
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"$ref" : "#/components/schemas/Int"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated" : false,
"security" : [ {
"jwt" : [ ]
} ]
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
}
},
"securitySchemes" : {
"jwt" : {
"type" : "apiKey",
"name" : "x-api-key",
"in" : "header"
}
}
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,74 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"$ref" : "#/components/schemas/String"
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"$ref" : "#/components/schemas/Int"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated" : false,
"security" : [ {
"jwt" : [ ]
} ]
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
}
},
"securitySchemes" : {
"jwt" : {
"type" : "http",
"scheme" : "oauth"
}
}
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,81 @@
{
"openapi" : "3.0.3",
"info" : { },
"servers" : [ ],
"paths" : {
"/test" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"$ref" : "#/components/schemas/String"
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"$ref" : "#/components/schemas/Int"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated" : false,
"security" : [ {
"jwt1" : [ ],
"jwt2" : [ ]
} ]
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
}
},
"securitySchemes" : {
"jwt1" : {
"type" : "apiKey",
"name" : "x-api-key-1",
"in" : "header"
},
"jwt2" : {
"type" : "apiKey",
"name" : "x-api-key-2",
"in" : "header"
}
}
},
"security" : [ ],
"tags" : [ ]
}