From d0767aa74e2fc6bafe3e35188ce643c792e07ddc Mon Sep 17 00:00:00 2001 From: dpnolte Date: Wed, 21 Apr 2021 19:51:42 +0200 Subject: [PATCH] init security scheme (#29) --- CHANGELOG.md | 6 + README.md | 50 ++++- gradle.properties | 2 +- gradle/libs.versions.toml | 4 + kompendium-auth/build.gradle.kts | 40 ++++ .../kompendium/auth/AuthPathCalculator.kt | 19 ++ .../kompendium/auth/KompendiumAuth.kt | 50 +++++ .../kompendium/auth/KompendiumAuthTest.kt | 197 ++++++++++++++++++ .../kompendium/auth/util/TestData.kt | 18 ++ .../kompendium/auth/util/TestModels.kt | 19 ++ .../notarized_basic_authenticated_get.json | 74 +++++++ .../notarized_jwt_authenticated_get.json | 74 +++++++ ...d_jwt_custom_header_authenticated_get.json | 75 +++++++ ...d_jwt_custom_scheme_authenticated_get.json | 74 +++++++ ...arized_multiple_jwt_authenticated_get.json | 81 +++++++ .../org/leafygreens/kompendium/Kompendium.kt | 6 +- .../kompendium/models/meta/MethodInfo.kt | 3 +- .../models/oas/OpenApiSpecComponents.kt | 2 +- kompendium-playground/build.gradle.kts | 2 + .../leafygreens/kompendium/playground/Main.kt | 47 ++++- settings.gradle.kts | 1 + 21 files changed, 835 insertions(+), 9 deletions(-) create mode 100644 kompendium-auth/build.gradle.kts create mode 100644 kompendium-auth/src/main/kotlin/org/leafygreens/kompendium/auth/AuthPathCalculator.kt create mode 100644 kompendium-auth/src/main/kotlin/org/leafygreens/kompendium/auth/KompendiumAuth.kt create mode 100644 kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/KompendiumAuthTest.kt create mode 100644 kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/util/TestData.kt create mode 100644 kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/util/TestModels.kt create mode 100644 kompendium-auth/src/test/resources/notarized_basic_authenticated_get.json create mode 100644 kompendium-auth/src/test/resources/notarized_jwt_authenticated_get.json create mode 100644 kompendium-auth/src/test/resources/notarized_jwt_custom_header_authenticated_get.json create mode 100644 kompendium-auth/src/test/resources/notarized_jwt_custom_scheme_authenticated_get.json create mode 100644 kompendium-auth/src/test/resources/notarized_multiple_jwt_authenticated_get.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e1d6ddca1..22235f000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.6.0] - April 21st, 2021 + +### Added + +- Added basic and jwt security scheme support with the new module kompendium-auth + ## [0.5.2] - April 19th, 2021 ### Removed diff --git a/README.md b/README.md index bbfd14f57..ecaec000e 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,6 @@ dependencies { Kompendium is still under active development ⚠️ There are a number of yet-to-be-implemented features, including - Multiple Responses 📜 -- Security Schemas 🔏 - Sealed Class / Polymorphic Support 😬 - Validation / Enforcement (❓👀❓) @@ -135,6 +134,55 @@ When run in the playground, this would output the following at `/openapi.json` https://gist.github.com/rgbrizzlehizzle/b9544922f2e99a2815177f8bdbf80668 +### Kompendium Auth and security schemes + +There is a seperate library to handle security schemes: `kompendium-auth`. +This needs to be added to your project as dependency. + +At the moment, the basic and jwt authentication is only supported. + +A minimal example would be: +```kotlin + install(Authentication) { + notarizedBasic("basic") { + realm = "Ktor realm 1" + // ... + } + notarizedJwt("jwt") { + realm = "Ktor realm 2" + // ... + } + } + routing { + authenticate("basic") { + route("/basic_auth") { + notarizedGet( + MethodInfo( + // securitySchemes needs to be set + "Another get test", "testing more", testGetResponse, securitySchemes = setOf("basic") + ) + ) { + call.respondText { "basic auth" } + } + } + } + authenticate("jwt") { + route("/jwt") { + notarizedGet( + MethodInfo( + // securitySchemes needs to be set + "Another get test", "testing more", testGetResponse, securitySchemes = setOf("jwt") + ) + ) { + call.respondText { "jwt" } + } + } + } + } +``` + + + ## Limitations ### Kompendium as a singleton diff --git a/gradle.properties b/gradle.properties index 886b0251c..29a7c3018 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=0.5.2 +project.version=0.6.0 # Kotlin kotlin.code.style=official # Gradle diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 972e51064..7cc091145 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,9 @@ ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" } ktor-jackson = { group = "io.ktor", name = "ktor-jackson", version.ref = "ktor" } ktor-html-builder = { group = "io.ktor", name = "ktor-html-builder", version.ref = "ktor" } +ktor-auth-lib = { group = "io.ktor", name = "ktor-auth", version.ref = "ktor" } +ktor-auth-jwt = { group = "io.ktor", name = "ktor-auth-jwt", version.ref = "ktor" } + # Logging slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } @@ -17,4 +20,5 @@ logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = [bundles] ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ] +ktorAuth = [ "ktor-auth-lib", "ktor-auth-jwt" ] logging = [ "slf4j", "logback-classic", "logback-core" ] diff --git a/kompendium-auth/build.gradle.kts b/kompendium-auth/build.gradle.kts new file mode 100644 index 000000000..572fc3d8b --- /dev/null +++ b/kompendium-auth/build.gradle.kts @@ -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("kompendium") { + from(components["kotlin"]) + artifact(tasks.sourcesJar) + } + } +} diff --git a/kompendium-auth/src/main/kotlin/org/leafygreens/kompendium/auth/AuthPathCalculator.kt b/kompendium-auth/src/main/kotlin/org/leafygreens/kompendium/auth/AuthPathCalculator.kt new file mode 100644 index 000000000..e649497ed --- /dev/null +++ b/kompendium-auth/src/main/kotlin/org/leafygreens/kompendium/auth/AuthPathCalculator.kt @@ -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) + } +} diff --git a/kompendium-auth/src/main/kotlin/org/leafygreens/kompendium/auth/KompendiumAuth.kt b/kompendium-auth/src/main/kotlin/org/leafygreens/kompendium/auth/KompendiumAuth.kt new file mode 100644 index 000000000..a7a113bed --- /dev/null +++ b/kompendium-auth/src/main/kotlin/org/leafygreens/kompendium/auth/KompendiumAuth.kt @@ -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)? +} diff --git a/kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/KompendiumAuthTest.kt b/kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/KompendiumAuthTest.kt new file mode 100644 index 000000000..5277b4aae --- /dev/null +++ b/kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/KompendiumAuthTest.kt @@ -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(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()) + } +} diff --git a/kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/util/TestData.kt b/kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/util/TestData.kt new file mode 100644 index 000000000..a6683cfac --- /dev/null +++ b/kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/util/TestData.kt @@ -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() + } +} diff --git a/kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/util/TestModels.kt b/kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/util/TestModels.kt new file mode 100644 index 000000000..8d172c510 --- /dev/null +++ b/kompendium-auth/src/test/kotlin/org/leafygreens/kompendium/auth/util/TestModels.kt @@ -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 +) + +data class TestResponse(val c: String) + diff --git a/kompendium-auth/src/test/resources/notarized_basic_authenticated_get.json b/kompendium-auth/src/test/resources/notarized_basic_authenticated_get.json new file mode 100644 index 000000000..808d1936a --- /dev/null +++ b/kompendium-auth/src/test/resources/notarized_basic_authenticated_get.json @@ -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" : [ ] +} diff --git a/kompendium-auth/src/test/resources/notarized_jwt_authenticated_get.json b/kompendium-auth/src/test/resources/notarized_jwt_authenticated_get.json new file mode 100644 index 000000000..1bf1389e8 --- /dev/null +++ b/kompendium-auth/src/test/resources/notarized_jwt_authenticated_get.json @@ -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" : [ ] +} diff --git a/kompendium-auth/src/test/resources/notarized_jwt_custom_header_authenticated_get.json b/kompendium-auth/src/test/resources/notarized_jwt_custom_header_authenticated_get.json new file mode 100644 index 000000000..971473bd1 --- /dev/null +++ b/kompendium-auth/src/test/resources/notarized_jwt_custom_header_authenticated_get.json @@ -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" : [ ] +} diff --git a/kompendium-auth/src/test/resources/notarized_jwt_custom_scheme_authenticated_get.json b/kompendium-auth/src/test/resources/notarized_jwt_custom_scheme_authenticated_get.json new file mode 100644 index 000000000..904bab9fa --- /dev/null +++ b/kompendium-auth/src/test/resources/notarized_jwt_custom_scheme_authenticated_get.json @@ -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" : [ ] +} diff --git a/kompendium-auth/src/test/resources/notarized_multiple_jwt_authenticated_get.json b/kompendium-auth/src/test/resources/notarized_multiple_jwt_authenticated_get.json new file mode 100644 index 000000000..122920e4c --- /dev/null +++ b/kompendium-auth/src/test/resources/notarized_multiple_jwt_authenticated_get.json @@ -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" : [ ] +} diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt index fc6fdf68c..6352afcae 100644 --- a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt +++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt @@ -101,7 +101,11 @@ object Kompendium { deprecated = this.deprecated, parameters = paramType.toParameterSpec(), responses = responseType.toResponseSpec(responseInfo)?.let { mapOf(it) }, - requestBody = if (method != HttpMethod.Get) requestType.toRequestSpec(requestInfo) else null + requestBody = if (method != HttpMethod.Get) requestType.toRequestSpec(requestInfo) else null, + security = if (this.securitySchemes.isNotEmpty()) listOf( + // TODO support scopes + this.securitySchemes.associateWith { listOf() } + ) else null ) @OptIn(ExperimentalStdlibApi::class) diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/meta/MethodInfo.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/meta/MethodInfo.kt index de8207be0..43c8c3c9d 100644 --- a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/meta/MethodInfo.kt +++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/meta/MethodInfo.kt @@ -7,5 +7,6 @@ data class MethodInfo( val responseInfo: ResponseInfo? = null, val requestInfo: RequestInfo? = null, val tags: Set = emptySet(), - val deprecated: Boolean = false + val deprecated: Boolean = false, + val securitySchemes: Set = emptySet() ) diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/oas/OpenApiSpecComponents.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/oas/OpenApiSpecComponents.kt index 69c0c0c1c..e333561cc 100644 --- a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/oas/OpenApiSpecComponents.kt +++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/oas/OpenApiSpecComponents.kt @@ -3,5 +3,5 @@ package org.leafygreens.kompendium.models.oas // TODO I *think* the only thing I need here is the security https://swagger.io/specification/#components-object data class OpenApiSpecComponents( val schemas: MutableMap = mutableMapOf(), - val securitySchemes: MutableMap = mutableMapOf() + val securitySchemes: MutableMap = mutableMapOf() ) diff --git a/kompendium-playground/build.gradle.kts b/kompendium-playground/build.gradle.kts index b6d02a1db..e52e5bc5f 100644 --- a/kompendium-playground/build.gradle.kts +++ b/kompendium-playground/build.gradle.kts @@ -7,8 +7,10 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation(projects.kompendiumCore) + implementation(projects.kompendiumAuth) implementation(libs.bundles.ktor) + implementation(libs.bundles.ktorAuth) implementation(libs.bundles.logging) testImplementation("org.jetbrains.kotlin:kotlin-test") diff --git a/kompendium-playground/src/main/kotlin/org/leafygreens/kompendium/playground/Main.kt b/kompendium-playground/src/main/kotlin/org/leafygreens/kompendium/playground/Main.kt index 78542e196..81305086b 100644 --- a/kompendium-playground/src/main/kotlin/org/leafygreens/kompendium/playground/Main.kt +++ b/kompendium-playground/src/main/kotlin/org/leafygreens/kompendium/playground/Main.kt @@ -5,6 +5,9 @@ import com.fasterxml.jackson.databind.SerializationFeature import io.ktor.application.Application import io.ktor.application.call import io.ktor.application.install +import io.ktor.auth.Authentication +import io.ktor.auth.authenticate +import io.ktor.auth.UserIdPrincipal import io.ktor.features.ContentNegotiation import io.ktor.jackson.jackson import io.ktor.response.respondText @@ -18,6 +21,7 @@ import org.leafygreens.kompendium.Kompendium.notarizedDelete import org.leafygreens.kompendium.Kompendium.notarizedGet import org.leafygreens.kompendium.Kompendium.notarizedPost import org.leafygreens.kompendium.Kompendium.notarizedPut +import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic import org.leafygreens.kompendium.annotations.KompendiumField import org.leafygreens.kompendium.annotations.PathParam import org.leafygreens.kompendium.annotations.QueryParam @@ -28,6 +32,7 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense import org.leafygreens.kompendium.models.oas.OpenApiSpecServer +import org.leafygreens.kompendium.playground.KompendiumTOC.testAuthenticatedSingleGetInfo import org.leafygreens.kompendium.playground.KompendiumTOC.testIdGetInfo import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleDeleteInfo import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleGetInfo @@ -73,12 +78,29 @@ fun main() { ).start(wait = true) } +var featuresInstalled = false fun Application.mainModule() { - install(ContentNegotiation) { - jackson { - enable(SerializationFeature.INDENT_OUTPUT) - setSerializationInclusion(JsonInclude.Include.NON_NULL) + // only install once in case of auto reload + if (!featuresInstalled) { + install(ContentNegotiation) { + jackson { + enable(SerializationFeature.INDENT_OUTPUT) + setSerializationInclusion(JsonInclude.Include.NON_NULL) + } } + install(Authentication) { + notarizedBasic("basic") { + realm = "Ktor Server" + validate { credentials -> + if (credentials.name == credentials.password) { + UserIdPrincipal(credentials.name) + } else { + null + } + } + } + } + featuresInstalled = true } routing { openApi(oas) @@ -103,6 +125,13 @@ fun Application.mainModule() { call.respondText { "heya" } } } + authenticate("basic") { + route("/authenticated/single") { + notarizedGet(testAuthenticatedSingleGetInfo) { + call.respondText("get authentiticated single") + } + } + } } } } @@ -180,4 +209,14 @@ object KompendiumTOC { mediaTypes = emptyList() ) ) + val testAuthenticatedSingleGetInfo = MethodInfo( + summary = "Another get test", + description = "testing more", + tags = setOf("anotherTest", "sample"), + responseInfo = ResponseInfo( + status = KompendiumHttpCodes.OK, + description = "Returns a different sample" + ), + securitySchemes = setOf("basic") + ) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 936601000..dd24d7ca9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,6 @@ rootProject.name = "kompendium" include("kompendium-core") +include("kompendium-auth") include("kompendium-playground") // Feature Previews