Skip to content

Commit 62052f7

Browse files
authored
feature: multiple server support (#24)
* Add httpserver and "route" abstraction * Support MockWebServer and Netty * Support custom routes, i.e. users of the library may add their own routes to be served by the MockOAuth2Server
1 parent 9c5a204 commit 62052f7

File tree

14 files changed

+475
-80
lines changed

14 files changed

+475
-80
lines changed

build.gradle.kts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ val logbackVersion = "1.2.3"
66
val nimbusSdkVersion = "8.19.1"
77
val mockWebServerVersion = "4.8.1"
88
val jacksonVersion = "2.11.2"
9+
val nettyVersion = "4.1.56.Final"
910
val junitJupiterVersion = "5.7.0-RC1"
10-
val konfigVersion = "1.6.10.0"
1111
val kotlinVersion = "1.4.0"
1212
val freemarkerVersion = "2.3.30"
1313
val kotestVersion = "4.2.5"
@@ -52,9 +52,9 @@ dependencies {
5252
implementation(kotlin("reflect"))
5353
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
5454
implementation("ch.qos.logback:logback-classic:$logbackVersion")
55-
implementation("com.natpryce:konfig:$konfigVersion")
5655
api("com.squareup.okhttp3:mockwebserver:$mockWebServerVersion")
5756
api("com.nimbusds:oauth2-oidc-sdk:$nimbusSdkVersion")
57+
implementation("io.netty:netty-all:$nettyVersion")
5858
implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion")
5959
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
6060
implementation("org.freemarker:freemarker:$freemarkerVersion")
@@ -200,11 +200,11 @@ tasks {
200200
dependsOn("publish")
201201
}
202202

203-
/*withType<Sign>().configureEach {
203+
withType<Sign>().configureEach {
204204
onlyIf {
205205
project.hasProperty("signatory.keyId")
206206
}
207-
}*/
207+
}
208208

209209
withType<Wrapper> {
210210
gradleVersion = "6.6.1"

src/main/kotlin/no/nav/security/mock/oauth2/MockOAuth2Server.kt

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,60 +11,74 @@ import java.io.IOException
1111
import java.net.InetAddress
1212
import java.net.URI
1313
import java.util.UUID
14-
import java.util.concurrent.BlockingQueue
15-
import java.util.concurrent.LinkedBlockingQueue
16-
import no.nav.security.mock.oauth2.extensions.asOAuth2HttpRequest
14+
import mu.KotlinLogging
1715
import no.nav.security.mock.oauth2.extensions.toAuthorizationEndpointUrl
1816
import no.nav.security.mock.oauth2.extensions.toEndSessionEndpointUrl
1917
import no.nav.security.mock.oauth2.extensions.toJwksUrl
2018
import no.nav.security.mock.oauth2.extensions.toTokenEndpointUrl
2119
import no.nav.security.mock.oauth2.extensions.toWellKnownUrl
20+
import no.nav.security.mock.oauth2.http.MockWebServerWrapper
2221
import no.nav.security.mock.oauth2.http.OAuth2HttpRequestHandler
23-
import no.nav.security.mock.oauth2.http.OAuth2HttpResponse
22+
import no.nav.security.mock.oauth2.http.OAuth2HttpRouter
23+
import no.nav.security.mock.oauth2.http.OAuth2HttpRouter.Companion.routes
24+
import no.nav.security.mock.oauth2.http.Route
25+
import no.nav.security.mock.oauth2.http.route
2426
import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback
2527
import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
2628
import okhttp3.HttpUrl
27-
import okhttp3.mockwebserver.Dispatcher
2829
import okhttp3.mockwebserver.MockResponse
29-
import okhttp3.mockwebserver.MockWebServer
3030
import okhttp3.mockwebserver.RecordedRequest
3131

32-
// TODO make open so others can extend?
32+
private val log = KotlinLogging.logger { }
33+
3334
@Suppress("unused", "MemberVisibilityCanBePrivate")
34-
class MockOAuth2Server(
35-
val config: OAuth2Config = OAuth2Config()
35+
open class MockOAuth2Server(
36+
val config: OAuth2Config = OAuth2Config(),
37+
vararg additionalRoutes: Route
3638
) {
37-
private val mockWebServer: MockWebServer = MockWebServer()
39+
constructor(vararg additionalRoutes: Route) : this(config = OAuth2Config(), additionalRoutes = additionalRoutes)
3840

39-
var dispatcher: Dispatcher = MockOAuth2Dispatcher(config)
41+
private val httpServer = config.httpServer
42+
private val defaultRequestHandler: OAuth2HttpRequestHandler = OAuth2HttpRequestHandler(config)
43+
private val router: OAuth2HttpRouter = routes(
44+
*additionalRoutes,
45+
route("") {
46+
defaultRequestHandler.handleRequest(it)
47+
}
48+
)
4049

4150
@JvmOverloads
4251
@Throws(IOException::class)
43-
fun start(
44-
inetAddress: InetAddress = InetAddress.getByName("localhost"),
45-
port: Int = 0
46-
) {
47-
mockWebServer.start(inetAddress, port)
48-
mockWebServer.dispatcher = dispatcher
52+
fun start(inetAddress: InetAddress = InetAddress.getByName("localhost"), port: Int = 0) {
53+
log.debug("attempt to start server on port=$port")
54+
httpServer.start(inetAddress, port, router)
4955
}
5056

5157
@Throws(IOException::class)
5258
fun shutdown() {
53-
mockWebServer.shutdown()
59+
httpServer.stop()
60+
}
61+
62+
fun url(path: String): HttpUrl = httpServer.url(path)
63+
64+
@Deprecated("Use MockWebServer method/function instead", ReplaceWith("MockWebServer.enqueue()"))
65+
fun enqueueResponse(response: MockResponse) {
66+
throw UnsupportedOperationException("cannot enqueue MockResponse, please use the MockWebServer directly with QueueDispatcher")
5467
}
5568

56-
fun url(path: String): HttpUrl = mockWebServer.url(path)
57-
fun enqueueResponse(response: MockResponse) = (dispatcher as MockOAuth2Dispatcher).enqueueResponse(response)
58-
fun enqueueCallback(oAuth2TokenCallback: OAuth2TokenCallback) = (dispatcher as MockOAuth2Dispatcher).enqueueTokenCallback(oAuth2TokenCallback)
59-
fun takeRequest(): RecordedRequest = mockWebServer.takeRequest()
69+
fun enqueueCallback(oAuth2TokenCallback: OAuth2TokenCallback) = defaultRequestHandler.enqueueTokenCallback(oAuth2TokenCallback)
70+
71+
fun takeRequest(): RecordedRequest =
72+
(httpServer as? MockWebServerWrapper)?.mockWebServer?.takeRequest()
73+
?: throw UnsupportedOperationException("can only takeRequest when httpServer is of type MockWebServer")
6074

61-
fun wellKnownUrl(issuerId: String): HttpUrl = mockWebServer.url(issuerId).toWellKnownUrl()
62-
fun tokenEndpointUrl(issuerId: String): HttpUrl = mockWebServer.url(issuerId).toTokenEndpointUrl()
63-
fun jwksUrl(issuerId: String): HttpUrl = mockWebServer.url(issuerId).toJwksUrl()
64-
fun issuerUrl(issuerId: String): HttpUrl = mockWebServer.url(issuerId)
65-
fun authorizationEndpointUrl(issuerId: String): HttpUrl = mockWebServer.url(issuerId).toAuthorizationEndpointUrl()
66-
fun endSessionEndpointUrl(issuerId: String): HttpUrl = mockWebServer.url(issuerId).toEndSessionEndpointUrl()
67-
fun baseUrl(): HttpUrl = mockWebServer.url("")
75+
fun wellKnownUrl(issuerId: String): HttpUrl = url(issuerId).toWellKnownUrl()
76+
fun tokenEndpointUrl(issuerId: String): HttpUrl = url(issuerId).toTokenEndpointUrl()
77+
fun jwksUrl(issuerId: String): HttpUrl = url(issuerId).toJwksUrl()
78+
fun issuerUrl(issuerId: String): HttpUrl = url(issuerId)
79+
fun authorizationEndpointUrl(issuerId: String): HttpUrl = url(issuerId).toAuthorizationEndpointUrl()
80+
fun endSessionEndpointUrl(issuerId: String): HttpUrl = url(issuerId).toEndSessionEndpointUrl()
81+
fun baseUrl(): HttpUrl = url("")
6882

6983
fun issueToken(issuerId: String, clientId: String, tokenCallback: OAuth2TokenCallback): SignedJWT {
7084
val uri = tokenEndpointUrl(issuerId)
@@ -97,29 +111,6 @@ class MockOAuth2Server(
97111
)
98112
}
99113

100-
class MockOAuth2Dispatcher(
101-
config: OAuth2Config
102-
) : Dispatcher() {
103-
private val httpRequestHandler: OAuth2HttpRequestHandler = OAuth2HttpRequestHandler(config)
104-
private val responseQueue: BlockingQueue<MockResponse> = LinkedBlockingQueue()
105-
106-
fun enqueueResponse(mockResponse: MockResponse) = responseQueue.add(mockResponse)
107-
fun enqueueTokenCallback(oAuth2TokenCallback: OAuth2TokenCallback) = httpRequestHandler.enqueueTokenCallback(oAuth2TokenCallback)
108-
109-
override fun dispatch(request: RecordedRequest): MockResponse =
110-
responseQueue.peek()?.let {
111-
responseQueue.take()
112-
} ?: mockResponse(httpRequestHandler.handleRequest(request.asOAuth2HttpRequest()))
113-
114-
private fun mockResponse(response: OAuth2HttpResponse): MockResponse =
115-
MockResponse()
116-
.setHeaders(response.headers)
117-
.setResponseCode(response.status)
118-
.apply {
119-
response.body?.let { this.setBody(it) }
120-
}
121-
}
122-
123114
fun <R> withMockOAuth2Server(
124115
test: MockOAuth2Server.() -> R
125116
): R {
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package no.nav.security.mock.oauth2
22

3+
import no.nav.security.mock.oauth2.http.MockWebServerWrapper
4+
import no.nav.security.mock.oauth2.http.OAuth2HttpServer
35
import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
46
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
57

68
data class OAuth2Config(
79
val interactiveLogin: Boolean = false,
810
val tokenProvider: OAuth2TokenProvider = OAuth2TokenProvider(),
9-
val tokenCallbacks: Set<OAuth2TokenCallback> = emptySet()
11+
val tokenCallbacks: Set<OAuth2TokenCallback> = emptySet(),
12+
val httpServer: OAuth2HttpServer = MockWebServerWrapper()
1013
)
Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
package no.nav.security.mock.oauth2
22

3-
import com.natpryce.konfig.ConfigurationProperties
4-
import com.natpryce.konfig.EnvironmentVariables
5-
import com.natpryce.konfig.Key
6-
import com.natpryce.konfig.intType
7-
import com.natpryce.konfig.overriding
8-
import com.natpryce.konfig.stringType
3+
import java.net.InetAddress
94
import java.net.InetSocketAddress
10-
11-
private val config = ConfigurationProperties.systemProperties() overriding
12-
EnvironmentVariables()
5+
import no.nav.security.mock.oauth2.http.NettyWrapper
6+
import no.nav.security.mock.oauth2.http.OAuth2HttpResponse
7+
import no.nav.security.mock.oauth2.http.route
138

149
data class Configuration(
1510
val server: Server = Server()
1611
) {
1712
data class Server(
18-
val hostname: String = config.getOrElse(Key("server.hostname", stringType), "localhost"),
19-
val port: Int = config.getOrElse(Key("server.port", intType), 8080)
13+
val hostname: InetAddress = "SERVER_HOSTNAME".fromEnv()?.let { InetAddress.getByName(it) } ?: InetSocketAddress(0).address,
14+
val port: Int = "SERVER_PORT".fromEnv()?.toInt() ?: 8080
2015
)
2116
}
2217

2318
fun main() {
2419
val config = Configuration()
2520
MockOAuth2Server(
2621
OAuth2Config(
27-
interactiveLogin = true
28-
)
29-
).start(InetSocketAddress(0).address, config.server.port)
22+
interactiveLogin = true,
23+
httpServer = NettyWrapper()
24+
),
25+
route("/isalive") {
26+
OAuth2HttpResponse(status = 200, body = "alive and well")
27+
}
28+
).start(config.server.hostname, config.server.port)
3029
}
30+
31+
fun String.fromEnv(): String? = System.getenv(this)

src/main/kotlin/no/nav/security/mock/oauth2/extensions/HttpUrlExtensions.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@ fun HttpUrl.issuerId(): String = this.pathSegments.getOrNull(0)
2828
fun HttpUrl.Builder.removeAllEncodedQueryParams(vararg params: String) =
2929
apply { params.forEach { removeAllEncodedQueryParameters(it) } }
3030

31+
fun HttpUrl.match(path: String) =
32+
path.trimPath().let {
33+
this.pathSegments.containsAll(it.split("/"))
34+
}
35+
36+
fun HttpUrl.endsWith(path: String): Boolean = this.pathSegments.joinToString("/").endsWith(path.trimPath())
37+
38+
private fun String.trimPath() = removePrefix("/").removeSuffix("/")
39+
3140
private fun HttpUrl.withoutQuery(): HttpUrl = this.newBuilder().query(null).build()
3241

3342
private fun HttpUrl.resolvePath(path: String): HttpUrl {
34-
3543
return HttpUrl.Builder()
3644
.scheme(this.scheme)
3745
.host(this.host)

src/main/kotlin/no/nav/security/mock/oauth2/extensions/RecordedRequestExtensions.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@ import okhttp3.mockwebserver.RecordedRequest
55

66
fun RecordedRequest.asOAuth2HttpRequest(): OAuth2HttpRequest =
77
OAuth2HttpRequest(this.headers, checkNotNull(this.method), checkNotNull(this.requestUrl), this.body.copy().readUtf8())
8-
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package no.nav.security.mock.oauth2.http
2+
3+
import mu.KotlinLogging
4+
import no.nav.security.mock.oauth2.extensions.endsWith
5+
6+
private val log = KotlinLogging.logger { }
7+
8+
typealias RequestHandler = (OAuth2HttpRequest) -> OAuth2HttpResponse
9+
10+
interface Route : RequestHandler {
11+
fun match(request: OAuth2HttpRequest): Boolean
12+
}
13+
14+
class OAuth2HttpRouter(
15+
private val routes: MutableList<Route> = mutableListOf()
16+
) : RequestHandler {
17+
18+
var notFoundHandler: (OAuth2HttpRequest) -> OAuth2HttpResponse = { OAuth2HttpResponse(status = 404, body = "no route found") }
19+
20+
override fun invoke(request: OAuth2HttpRequest): OAuth2HttpResponse = match(request)
21+
22+
private fun match(request: OAuth2HttpRequest): OAuth2HttpResponse =
23+
routes.also {
24+
log.debug("attempt to route request with url=${request.url}")
25+
}.firstOrNull {
26+
it.match(request)
27+
}?.invoke(request)
28+
?: notFoundHandler.invoke(request)
29+
.also { log.debug("no handler found, using notFoundHandler") }
30+
31+
companion object {
32+
fun routes(vararg route: Route): OAuth2HttpRouter = OAuth2HttpRouter(mutableListOf(*route))
33+
}
34+
}
35+
36+
@JvmOverloads
37+
fun route(path: String, method: String? = null, requestHandler: RequestHandler): Route =
38+
routeFromPathAndMethod(path, method, requestHandler)
39+
40+
fun put(path: String, requestHandler: RequestHandler): Route =
41+
routeFromPathAndMethod(path, "PUT", requestHandler)
42+
43+
fun post(path: String, requestHandler: RequestHandler): Route =
44+
routeFromPathAndMethod(path, "POST", requestHandler)
45+
46+
fun get(path: String, requestHandler: RequestHandler): Route =
47+
routeFromPathAndMethod(path, "GET", requestHandler)
48+
49+
private fun routeFromPathAndMethod(path: String, method: String? = null, requestHandler: RequestHandler): Route =
50+
object : Route {
51+
override fun match(request: OAuth2HttpRequest): Boolean =
52+
if (request.url.endsWith(path)) {
53+
method?.let { it == request.method } ?: true
54+
} else {
55+
false
56+
}
57+
58+
override fun invoke(request: OAuth2HttpRequest): OAuth2HttpResponse = requestHandler.invoke(request)
59+
}

0 commit comments

Comments
 (0)