Skip to content

Commit e637f3a

Browse files
authored
Merge pull request #67 from sigmanil/security-scheme-generation-test
Adding test to demonstrate failing security scheme generation
2 parents dd1438a + 53006cc commit e637f3a

File tree

3 files changed

+248
-0
lines changed

3 files changed

+248
-0
lines changed

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ dependencies {
2727
testImplementation "io.ktor:ktor-server-netty:$ktor_version"
2828
testImplementation "io.ktor:ktor-server-test-host:$ktor_version"
2929
testImplementation "ch.qos.logback:logback-classic:$logback_version"
30+
testImplementation "io.ktor:ktor-auth:$ktor_version"
31+
testImplementation "io.ktor:ktor-auth-jwt:$ktor_version"
32+
33+
3034
}
3135

3236
compileKotlin {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package origo.booking
2+
3+
import TestServerWithJwtAuth.testServerWithJwtAuth
4+
import io.ktor.http.HttpMethod
5+
import io.ktor.http.HttpStatusCode
6+
import io.ktor.server.testing.handleRequest
7+
import io.ktor.server.testing.withTestApplication
8+
import org.junit.Test
9+
import org.junit.Assert.*
10+
11+
12+
internal class JwtAuthDocumentationGenerationTest {
13+
14+
@Test
15+
fun testRequest() = withTestApplication({
16+
testServerWithJwtAuth()
17+
}) {
18+
with(handleRequest(HttpMethod.Get, "//openapi.json")) {
19+
assertEquals(HttpStatusCode.OK, response.status())
20+
assertTrue(response.content!!.contains("\"securitySchemes\" : {\n" +
21+
" \"JWT\" : {\n" +
22+
" \"bearerFormat\" : \"JWT\",\n" +
23+
" \"name\" : \"JWT\",\n" +
24+
" \"scheme\" : \"bearer\",\n" +
25+
" \"type\" : \"openIdConnect\"\n" +
26+
" }\n" +
27+
" }"))
28+
assertTrue(response.content!!.contains("\"security\" : [ { } ],"))
29+
}
30+
}
31+
32+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import io.ktor.auth.jwt.jwt
2+
import com.auth0.jwk.JwkProvider
3+
import com.auth0.jwk.JwkProviderBuilder
4+
import com.fasterxml.jackson.annotation.JsonInclude
5+
import com.fasterxml.jackson.core.util.DefaultIndenter
6+
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
7+
import com.fasterxml.jackson.databind.DeserializationFeature
8+
import com.fasterxml.jackson.databind.SerializationFeature
9+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
10+
import com.papsign.ktor.openapigen.OpenAPIGen
11+
import com.papsign.ktor.openapigen.annotations.Path
12+
import com.papsign.ktor.openapigen.annotations.Response
13+
import com.papsign.ktor.openapigen.annotations.parameters.PathParam
14+
import com.papsign.ktor.openapigen.annotations.properties.description.Description
15+
import com.papsign.ktor.openapigen.model.Described
16+
import com.papsign.ktor.openapigen.model.security.HttpSecurityScheme
17+
import com.papsign.ktor.openapigen.model.security.SecuritySchemeModel
18+
import com.papsign.ktor.openapigen.model.security.SecuritySchemeType
19+
import com.papsign.ktor.openapigen.model.server.ServerModel
20+
import com.papsign.ktor.openapigen.modules.providers.AuthProvider
21+
import com.papsign.ktor.openapigen.openAPIGen
22+
import com.papsign.ktor.openapigen.route.*
23+
import com.papsign.ktor.openapigen.route.path.auth.OpenAPIAuthenticatedRoute
24+
import com.papsign.ktor.openapigen.route.path.normal.NormalOpenAPIRoute
25+
import com.papsign.ktor.openapigen.route.path.auth.*
26+
import com.papsign.ktor.openapigen.route.response.respond
27+
import com.papsign.ktor.openapigen.schema.namer.DefaultSchemaNamer
28+
import com.papsign.ktor.openapigen.schema.namer.SchemaNamer
29+
import io.ktor.application.*
30+
import io.ktor.auth.Authentication
31+
import io.ktor.auth.Principal
32+
import io.ktor.auth.authenticate
33+
import io.ktor.auth.authentication
34+
import io.ktor.features.ContentNegotiation
35+
import io.ktor.features.origin
36+
import io.ktor.jackson.jackson
37+
import io.ktor.request.host
38+
import io.ktor.request.port
39+
import io.ktor.response.respond
40+
import io.ktor.response.respondRedirect
41+
import io.ktor.routing.get
42+
import io.ktor.routing.routing
43+
import io.ktor.server.engine.embeddedServer
44+
import io.ktor.server.netty.Netty
45+
import io.ktor.util.pipeline.PipelineContext
46+
import java.net.URL
47+
import java.util.concurrent.TimeUnit
48+
import kotlin.reflect.KType
49+
50+
object TestServerWithJwtAuth {
51+
52+
@JvmStatic
53+
fun main(args: Array<String>) {
54+
embeddedServer(Netty, 8080, "localhost") {
55+
testServerWithJwtAuth()
56+
}.start(true)
57+
}
58+
59+
public fun Application.testServerWithJwtAuth() {
60+
//define basic OpenAPI info
61+
val authProvider = JwtProvider();
62+
val api = install(com.papsign.ktor.openapigen.OpenAPIGen) {
63+
info {
64+
version = "0.1"
65+
title = "Test API"
66+
description = "The Test API"
67+
contact {
68+
name = "Support"
69+
70+
}
71+
}
72+
server("https://api.test.com/") {
73+
description = "Main production server"
74+
}
75+
addModules(authProvider)
76+
replaceModule(com.papsign.ktor.openapigen.schema.namer.DefaultSchemaNamer, object: SchemaNamer {
77+
val regex = kotlin.text.Regex("[A-Za-z0-9_.]+")
78+
override fun get(type: KType): String {
79+
return type.toString().replace(regex) { it.value.split(".").last() }.replace(kotlin.text.Regex(">|<|, "), "_")
80+
}
81+
})
82+
}
83+
84+
install(io.ktor.features.ContentNegotiation) {
85+
jackson {
86+
enable(
87+
com.fasterxml.jackson.databind.DeserializationFeature.WRAP_EXCEPTIONS,
88+
com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_INTEGER_FOR_INTS,
89+
com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
90+
)
91+
92+
enable(com.fasterxml.jackson.databind.SerializationFeature.WRAP_EXCEPTIONS, com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT)
93+
94+
setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)
95+
96+
setDefaultPrettyPrinter(com.fasterxml.jackson.core.util.DefaultPrettyPrinter().apply {
97+
indentArraysWith(com.fasterxml.jackson.core.util.DefaultPrettyPrinter.FixedSpaceIndenter.instance)
98+
indentObjectsWith(com.fasterxml.jackson.core.util.DefaultIndenter(" ", "\n"))
99+
})
100+
101+
registerModule(com.fasterxml.jackson.datatype.jsr310.JavaTimeModule())
102+
}
103+
}
104+
105+
install(io.ktor.auth.Authentication) {
106+
installJwt(this)
107+
}
108+
109+
// serve OpenAPI and redirect from root
110+
routing {
111+
get("/openapi.json") {
112+
val host = com.papsign.ktor.openapigen.model.server.ServerModel(
113+
call.request.origin.scheme + "://" + call.request.host() + if (kotlin.collections.setOf(
114+
80,
115+
443
116+
).contains(call.request.port())
117+
) "" else ":${call.request.port()}"
118+
)
119+
application.openAPIGen.api.servers.add(0, host)
120+
call.respond(application.openAPIGen.api.serialize())
121+
application.openAPIGen.api.servers.remove(host)
122+
}
123+
124+
get("/") {
125+
call.respondRedirect("/swagger-ui/index.html?url=/openapi.json", true)
126+
}
127+
}
128+
129+
apiRouting {
130+
auth {
131+
get<StringParam, StringResponse, UserPrincipal>(
132+
com.papsign.ktor.openapigen.route.info("String Param Endpoint", "This is a String Param Endpoint"),
133+
example = StringResponse("Hi")
134+
) { params ->
135+
val (userId, name) = principal()
136+
respond(StringResponse("Hello $name, you submitted ${params.a}"))
137+
}
138+
}
139+
}
140+
}
141+
142+
@Path("string/{a}")
143+
data class StringParam(@PathParam("A simple String Param") val a: String)
144+
145+
@Response("A String Response")
146+
data class StringResponse(@Description("The string value") val str: String)
147+
148+
val authProvider = JwtProvider();
149+
150+
inline fun NormalOpenAPIRoute.auth(route: OpenAPIAuthenticatedRoute<UserPrincipal>.() -> Unit): OpenAPIAuthenticatedRoute<UserPrincipal> {
151+
val authenticatedKtorRoute = this.ktorRoute.authenticate { }
152+
var openAPIAuthenticatedRoute= OpenAPIAuthenticatedRoute(authenticatedKtorRoute, this.provider.child(), authProvider = authProvider);
153+
return openAPIAuthenticatedRoute.apply {
154+
route()
155+
}
156+
}
157+
158+
data class UserPrincipal(val userId: String, val name: String?) : Principal
159+
160+
class JwtProvider : AuthProvider<UserPrincipal> {
161+
override val security: Iterable<Iterable<AuthProvider.Security<*>>> =
162+
listOf(listOf(
163+
AuthProvider.Security(
164+
SecuritySchemeModel(
165+
SecuritySchemeType.openIdConnect,
166+
scheme = HttpSecurityScheme.bearer,
167+
bearerFormat = "JWT",
168+
name = "JWT"
169+
), emptyList<Scopes>()
170+
)
171+
))
172+
173+
override suspend fun getAuth(pipeline: PipelineContext<Unit, ApplicationCall>): UserPrincipal {
174+
return pipeline.context.authentication.principal() ?: throw RuntimeException("No JWTPrincipal")
175+
}
176+
177+
override fun apply(route: NormalOpenAPIRoute): OpenAPIAuthenticatedRoute<UserPrincipal> {
178+
val authenticatedKtorRoute = route.ktorRoute.authenticate { }
179+
return OpenAPIAuthenticatedRoute(authenticatedKtorRoute, route.provider.child(), this)
180+
}
181+
}
182+
183+
enum class Scopes(override val description: String) : Described {
184+
Profile("Some scope")
185+
}
186+
187+
val jwtRealm : String = "example-jwt-realm"
188+
val jwtIssuer: String = "http://localhost:9091/auth/realms/$jwtRealm"
189+
val jwtEndpoint: String = "$jwtIssuer/protocol/openid-connect/certs"
190+
191+
fun installJwt (provider: Authentication.Configuration) {
192+
provider.apply {
193+
jwt {
194+
realm = jwtRealm
195+
verifier(getJwkProvider(jwtEndpoint), jwtIssuer)
196+
validate { credentials ->
197+
UserPrincipal(
198+
credentials.payload.subject,
199+
credentials.payload.claims["name"]?.asString())
200+
}
201+
}
202+
}
203+
}
204+
205+
private fun getJwkProvider(jwkEndpoint: String): JwkProvider {
206+
return JwkProviderBuilder(URL(jwkEndpoint))
207+
.cached(10, 24, TimeUnit.HOURS)
208+
.rateLimited(10, 1, TimeUnit.MINUTES)
209+
.build()
210+
}
211+
212+
}

0 commit comments

Comments
 (0)