From c2f7a28ce8ac8b36816e2aadf051b58257917720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Tue, 24 Mar 2020 15:01:37 +0100 Subject: [PATCH] Add usage example tests (#2) * issueToken method is using its own TokenProvider, use the same as in config * use safe call to clientIdAsString for clientid as subject * add examples (apps and tests), some linting --- .../security/mock/oauth2/MockOAuth2Server.kt | 8 +- .../mock/oauth2/token/OAuth2TokenCallback.kt | 2 +- .../mock/oauth2/MockOAuth2ServerTest.kt | 2 +- .../oauth2/examples/AbstractExampleApp.kt | 112 ++++++++++++++++++ .../ExampleAppWithClientCredentialsClient.kt | 44 +++++++ ...ampleAppWithClientCredentialsClientTest.kt | 53 +++++++++ .../ExampleAppWithOpenIdConnect.kt | 87 ++++++++++++++ .../ExampleAppWithOpenIdConnectTest.kt | 82 +++++++++++++ .../securedapi/ExampleAppWithSecuredApi.kt | 23 ++++ .../ExampleAppWithSecuredApiTest.kt | 60 ++++++++++ 10 files changed, 465 insertions(+), 8 deletions(-) create mode 100644 src/test/kotlin/no/nav/security/mock/oauth2/examples/AbstractExampleApp.kt create mode 100644 src/test/kotlin/no/nav/security/mock/oauth2/examples/clientcredentials/ExampleAppWithClientCredentialsClient.kt create mode 100644 src/test/kotlin/no/nav/security/mock/oauth2/examples/clientcredentials/ExampleAppWithClientCredentialsClientTest.kt create mode 100644 src/test/kotlin/no/nav/security/mock/oauth2/examples/openidconnect/ExampleAppWithOpenIdConnect.kt create mode 100644 src/test/kotlin/no/nav/security/mock/oauth2/examples/openidconnect/ExampleAppWithOpenIdConnectTest.kt create mode 100644 src/test/kotlin/no/nav/security/mock/oauth2/examples/securedapi/ExampleAppWithSecuredApi.kt create mode 100644 src/test/kotlin/no/nav/security/mock/oauth2/examples/securedapi/ExampleAppWithSecuredApiTest.kt diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/MockOAuth2Server.kt b/src/main/kotlin/no/nav/security/mock/oauth2/MockOAuth2Server.kt index 66f0d8a8..da879090 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/MockOAuth2Server.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/MockOAuth2Server.kt @@ -16,7 +16,6 @@ import no.nav.security.mock.oauth2.extensions.toWellKnownUrl import no.nav.security.mock.oauth2.http.OAuth2HttpRequestHandler import no.nav.security.mock.oauth2.http.OAuth2HttpResponse import no.nav.security.mock.oauth2.token.OAuth2TokenCallback -import no.nav.security.mock.oauth2.token.OAuth2TokenProvider import okhttp3.HttpUrl import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse @@ -31,11 +30,9 @@ import java.util.concurrent.LinkedBlockingQueue private val log = KotlinLogging.logger {} class MockOAuth2Server( - config: OAuth2Config = OAuth2Config() + val config: OAuth2Config = OAuth2Config() ) { private val mockWebServer: MockWebServer = MockWebServer() - private val tokenProvider: OAuth2TokenProvider = - OAuth2TokenProvider() var dispatcher: Dispatcher = MockOAuth2Dispatcher(config) @@ -76,7 +73,7 @@ class MockOAuth2Server( ClientSecretBasic(ClientID(clientId), Secret("secret")), AuthorizationCodeGrant(AuthorizationCode("123"), URI.create("http://localhost")) ) - return tokenProvider.accessToken(tokenRequest, issuerUrl, null, OAuth2TokenCallback) + return config.tokenProvider.accessToken(tokenRequest, issuerUrl, null, OAuth2TokenCallback) } } @@ -95,7 +92,6 @@ class MockOAuth2Dispatcher( else -> mockResponse(httpRequestHandler.handleRequest(request.asOAuth2HttpRequest())) } - private fun mockResponse(response: OAuth2HttpResponse): MockResponse = MockResponse() .setHeaders(response.headers) diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallback.kt b/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallback.kt index b18f2f38..f1c1d37f 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallback.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallback.kt @@ -27,7 +27,7 @@ open class DefaultOAuth2TokenCallback( override fun subject(tokenRequest: TokenRequest): String { return when (GrantType.CLIENT_CREDENTIALS) { - tokenRequest.grantType() -> tokenRequest.clientID.value + tokenRequest.grantType() -> tokenRequest.clientIdAsString() else -> subject } } diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/MockOAuth2ServerTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/MockOAuth2ServerTest.kt index 571f888a..29fa0992 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/MockOAuth2ServerTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/MockOAuth2ServerTest.kt @@ -60,7 +60,7 @@ class MockOAuth2ServerTest { } @Test - fun enqueuedResponse(){ + fun enqueuedResponse() { assertWellKnownResponseForIssuer("default") server.enqueueResponse(MockResponse() .setResponseCode(200) diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/examples/AbstractExampleApp.kt b/src/test/kotlin/no/nav/security/mock/oauth2/examples/AbstractExampleApp.kt new file mode 100644 index 00000000..8cde14c3 --- /dev/null +++ b/src/test/kotlin/no/nav/security/mock/oauth2/examples/AbstractExampleApp.kt @@ -0,0 +1,112 @@ +package no.nav.security.mock.oauth2.examples + +import com.fasterxml.jackson.databind.ObjectMapper +import com.nimbusds.jose.JOSEObjectType +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.jwk.JWKSet +import com.nimbusds.jose.jwk.source.ImmutableJWKSet +import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier +import com.nimbusds.jose.proc.JWSKeySelector +import com.nimbusds.jose.proc.JWSVerificationKeySelector +import com.nimbusds.jose.proc.SecurityContext +import com.nimbusds.jose.util.DefaultResourceRetriever +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier +import com.nimbusds.jwt.proc.DefaultJWTProcessor +import com.nimbusds.oauth2.sdk.id.Issuer +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata +import mu.KotlinLogging +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import java.net.URL +import java.util.HashSet + +private val log = KotlinLogging.logger {} + +abstract class AbstractExampleApp(oauth2DiscoveryUrl: String) { + + val oauth2Client: OkHttpClient = OkHttpClient() + .newBuilder() + .followRedirects(false) + .build() + + val metadata = OIDCProviderMetadata.parse(DefaultResourceRetriever().retrieveResource(URL(oauth2DiscoveryUrl)).content) + + lateinit var exampleApp: MockWebServer + + fun start() { + exampleApp = MockWebServer() + exampleApp.start() + exampleApp.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + return runCatching { + handleRequest(request) + }.fold( + onSuccess = { result -> result }, + onFailure = { error -> + log.error("received unhandled exception.", error) + MockResponse() + .setResponseCode(500) + .setBody("unhandled exception with message ${error.message}") + } + ) + } + } + } + + fun shutdown() { + exampleApp.shutdown() + } + + fun url(path: String): HttpUrl = exampleApp.url(path) + + fun retrieveJwks(): JWKSet { + return oauth2Client.newCall( + Request.Builder() + .url(metadata.jwkSetURI.toURL()) + .get() + .build() + ).execute().body?.string()?.let { + JWKSet.parse(it) + } ?: throw RuntimeException("could not retrieve jwks") + } + + fun verifyJwt(jwt: String, issuer: Issuer, jwkSet: JWKSet): JWTClaimsSet { + val jwtProcessor: ConfigurableJWTProcessor = DefaultJWTProcessor() + jwtProcessor.jwsTypeVerifier = DefaultJOSEObjectTypeVerifier(JOSEObjectType("JWT")) + val keySelector: JWSKeySelector = JWSVerificationKeySelector( + JWSAlgorithm.RS256, + ImmutableJWKSet(jwkSet) + ) + jwtProcessor.jwsKeySelector = keySelector + jwtProcessor.jwtClaimsSetVerifier = DefaultJWTClaimsVerifier( + JWTClaimsSet.Builder().issuer(issuer.toString()).build(), + HashSet(listOf("sub", "iat", "exp", "aud")) + ) + return try { + jwtProcessor.process(jwt, null) + } catch (e: Exception) { + throw RuntimeException("invalid jwt.", e) + } + } + + fun bearerToken(request: RecordedRequest): String? = + request.headers["Authorization"] + ?.split("Bearer ") + ?.let { it[0] } + + fun notAuthorized(): MockResponse = MockResponse().setResponseCode(401) + + fun json(value: Any): MockResponse = MockResponse() + .setResponseCode(200) + .setHeader("Content-Type","application/json") + .setBody(ObjectMapper().writeValueAsString(value)) + + abstract fun handleRequest(request: RecordedRequest): MockResponse +} diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/examples/clientcredentials/ExampleAppWithClientCredentialsClient.kt b/src/test/kotlin/no/nav/security/mock/oauth2/examples/clientcredentials/ExampleAppWithClientCredentialsClient.kt new file mode 100644 index 00000000..dec44451 --- /dev/null +++ b/src/test/kotlin/no/nav/security/mock/oauth2/examples/clientcredentials/ExampleAppWithClientCredentialsClient.kt @@ -0,0 +1,44 @@ +package no.nav.security.mock.oauth2.examples.clientcredentials + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.security.mock.oauth2.examples.AbstractExampleApp +import okhttp3.Credentials +import okhttp3.FormBody +import okhttp3.Request +import okhttp3.Response +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest + +class ExampleAppWithClientCredentialsClient(oauth2DiscoveryUrl: String) : AbstractExampleApp(oauth2DiscoveryUrl) { + + override fun handleRequest(request: RecordedRequest): MockResponse { + return getClientCredentialsAccessToken() + ?.let { + MockResponse() + .setResponseCode(200) + .setBody("token=$it") + } + ?: MockResponse().setResponseCode(500).setBody("could not get access_token") + } + + private fun getClientCredentialsAccessToken(): String? { + val tokenResponse: Response = oauth2Client.newCall( + Request.Builder() + .url(metadata.tokenEndpointURI.toURL()) + .addHeader("Authorization", Credentials.basic("ExampleAppWithClientCredentialsClient", "test")) + .post( + FormBody.Builder() + .add("client_id", "ExampleAppWithClientCredentialsClient") + .add("scope", "scope1") + .add("grant_type", "client_credentials") + .build() + ) + .build() + ).execute() + return tokenResponse.body?.string()?.let { + ObjectMapper().readValue(it).get("access_token")?.textValue() + } + } +} diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/examples/clientcredentials/ExampleAppWithClientCredentialsClientTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/examples/clientcredentials/ExampleAppWithClientCredentialsClientTest.kt new file mode 100644 index 00000000..0be1ab43 --- /dev/null +++ b/src/test/kotlin/no/nav/security/mock/oauth2/examples/clientcredentials/ExampleAppWithClientCredentialsClientTest.kt @@ -0,0 +1,53 @@ +package no.nav.security.mock.oauth2.examples.clientcredentials + +import com.nimbusds.jwt.SignedJWT +import no.nav.security.mock.oauth2.MockOAuth2Server +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class ExampleAppWithClientCredentialsClientTest { + private lateinit var client: OkHttpClient + private lateinit var oAuth2Server: MockOAuth2Server + private lateinit var exampleApp: ExampleAppWithClientCredentialsClient + + private val ISSUER_ID = "test" + + @BeforeEach + fun before() { + oAuth2Server = MockOAuth2Server() + oAuth2Server.start() + exampleApp = ExampleAppWithClientCredentialsClient(oAuth2Server.wellKnownUrl(ISSUER_ID).toString()) + exampleApp.start() + client = OkHttpClient().newBuilder().build() + } + + @AfterEach + fun shutdown() { + oAuth2Server.shutdown() + exampleApp.shutdown() + } + + @Test + fun appShouldReturnClientCredentialsAccessTokenWhenInvoked() { + val response: Response = client.newCall( + Request.Builder() + .url(exampleApp.url("/clientcredentials")) + .get() + .build() + ).execute() + assertThat(response.code).isEqualTo(200) + + val token: SignedJWT? = response.body?.string() + ?.split("token=") + ?.let { it[1] } + ?.let { SignedJWT.parse(it) } + + assertThat(token).isNotNull + assertThat(token?.jwtClaimsSet?.subject).isEqualTo("ExampleAppWithClientCredentialsClient") + } +} diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/examples/openidconnect/ExampleAppWithOpenIdConnect.kt b/src/test/kotlin/no/nav/security/mock/oauth2/examples/openidconnect/ExampleAppWithOpenIdConnect.kt new file mode 100644 index 00000000..29fef1dc --- /dev/null +++ b/src/test/kotlin/no/nav/security/mock/oauth2/examples/openidconnect/ExampleAppWithOpenIdConnect.kt @@ -0,0 +1,87 @@ +package no.nav.security.mock.oauth2.examples.openidconnect + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.openid.connect.sdk.AuthenticationRequest +import mu.KotlinLogging +import no.nav.security.mock.oauth2.examples.AbstractExampleApp +import okhttp3.FormBody +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest + +private val log = KotlinLogging.logger {} + +class ExampleAppWithOpenIdConnect(oidcDiscoveryUrl: String) : AbstractExampleApp(oidcDiscoveryUrl) { + + override fun handleRequest(request: RecordedRequest): MockResponse { + return when (request.requestUrl?.encodedPath) { + "/login" -> { + MockResponse() + .setResponseCode(302) + .setHeader("Location", authenticationRequest().toURI()) + } + "/callback" -> { + log.debug("got callback: $request") + val code = request.requestUrl?.queryParameter("code")!! + val tokenResponse = oauth2Client.newCall( + Request.Builder() + .url(metadata.tokenEndpointURI.toURL()) + .post( + FormBody.Builder() + .add("client_id", "client1") + .add("scope", authenticationRequest().scope.toString()) + .add("code", code) + .add("redirect_uri", exampleApp.url("/callback").toString()) + .add("grant_type", "authorization_code") + .build() + ) + .build() + ).execute() + val idToken: String = ObjectMapper().readValue(tokenResponse.body!!.string()).get("id_token").textValue() + val idTokenClaims: JWTClaimsSet = verifyJwt(idToken, metadata.issuer, retrieveJwks()) + MockResponse() + .setResponseCode(200) + .setHeader("Set-Cookie", "id_token=$idToken") + .setBody("logged in as ${idTokenClaims.subject}") + } + "/secured" -> { + getCookies(request)["id_token"] + ?.let { + verifyJwt(it, metadata.issuer, retrieveJwks()) + }?.let { + MockResponse() + .setResponseCode(200) + .setBody("welcome ${it.subject}") + } ?: MockResponse().setResponseCode(302).setHeader("Location", exampleApp.url("/login")) + } + else -> MockResponse().setResponseCode(404) + } + } + + private fun getCookies(request: RecordedRequest): Map { + return request.getHeader("Cookie") + ?.split(";") + ?.filter { it.contains("=") } + ?.associate { + val (key, value) = it.split("=") + key.trim() to value.trim() + } ?: emptyMap() + } + + private fun authenticationRequest(): AuthenticationRequest = + AuthenticationRequest.parse( + metadata.authorizationEndpointURI, + mutableMapOf( + "client_id" to listOf("client"), + "response_type" to listOf("code"), + "redirect_uri" to listOf(exampleApp.url("/callback").toString()), + "response_mode" to listOf("query"), + "scope" to listOf("openid", "scope1"), + "state" to listOf("1234"), + "nonce" to listOf("5678") + ) + ) +} diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/examples/openidconnect/ExampleAppWithOpenIdConnectTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/examples/openidconnect/ExampleAppWithOpenIdConnectTest.kt new file mode 100644 index 00000000..7f57d0fe --- /dev/null +++ b/src/test/kotlin/no/nav/security/mock/oauth2/examples/openidconnect/ExampleAppWithOpenIdConnectTest.kt @@ -0,0 +1,82 @@ +package no.nav.security.mock.oauth2.examples.openidconnect + +import no.nav.security.mock.oauth2.MockOAuth2Server +import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class ExampleAppWithOpenIdConnectTest { + + private lateinit var client: OkHttpClient + private lateinit var oAuth2Server: MockOAuth2Server + private lateinit var exampleApp: ExampleAppWithOpenIdConnect + + private val ISSUER_ID = "test" + + @BeforeEach + fun before() { + oAuth2Server = MockOAuth2Server() + oAuth2Server.start() + exampleApp = ExampleAppWithOpenIdConnect(oAuth2Server.wellKnownUrl(ISSUER_ID).toString()) + exampleApp.start() + client = OkHttpClient() + .newBuilder() + .followRedirects(true) + .cookieJar(InmemoryCookieJar()) + .build() + } + + @AfterEach + fun shutdown() { + oAuth2Server.shutdown() + exampleApp.shutdown() + } + + @Test + fun loginWithOpenIdConnect() { + val loginResponse = client.newCall(Request.Builder().url(exampleApp.url("/login")).build()).execute() + assertThat(loginResponse.headers["Set-Cookie"]).contains("id_token=") + } + + @Test + fun loginAndAccessSecuredPathWithIdTokenForSubjectFoo() { + oAuth2Server.enqueueCallback( + DefaultOAuth2TokenCallback( + issuerId = ISSUER_ID, + subject = "foo" + ) + ) + val loginResponse = client.newCall(Request.Builder().url(exampleApp.url("/login")).build()).execute() + assertThat(loginResponse.headers["Set-Cookie"]).contains("id_token=") + val securedResponse = client.newCall(Request.Builder().url(exampleApp.url("/secured")).build()).execute() + assertThat(securedResponse.code).isEqualTo(200) + val body = securedResponse.body?.string() + assertThat(body).isEqualTo("welcome foo") + } + + @Test + fun requestToSecuredPathShouldRedirectToLogin() { + val loginResponse = OkHttpClient() + .newBuilder() + .followRedirects(false) + .build() + .newCall(Request.Builder().url(exampleApp.url("/secured")).build()).execute() + assertThat(loginResponse.code).isEqualTo(302) + assertThat(loginResponse.headers["Location"]).isEqualTo(exampleApp.url("/login").toString()) + } + + internal class InmemoryCookieJar : CookieJar { + private val cookieList: MutableList = mutableListOf() + override fun loadForRequest(url: HttpUrl): List = cookieList + override fun saveFromResponse(url: HttpUrl, cookies: List) { + cookieList.addAll(cookies) + } + } +} diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/examples/securedapi/ExampleAppWithSecuredApi.kt b/src/test/kotlin/no/nav/security/mock/oauth2/examples/securedapi/ExampleAppWithSecuredApi.kt new file mode 100644 index 00000000..4df1b4b0 --- /dev/null +++ b/src/test/kotlin/no/nav/security/mock/oauth2/examples/securedapi/ExampleAppWithSecuredApi.kt @@ -0,0 +1,23 @@ +package no.nav.security.mock.oauth2.examples.securedapi + +import no.nav.security.mock.oauth2.examples.AbstractExampleApp +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest + +class ExampleAppWithSecuredApi(oauth2DiscoveryUrl: String) : AbstractExampleApp(oauth2DiscoveryUrl) { + + override fun handleRequest(request: RecordedRequest): MockResponse { + return bearerToken(request) + ?.let { + verifyJwt(it, metadata.issuer, retrieveJwks()) + }?.let { + MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(greeting(it.subject)) + } ?: notAuthorized() + } + + private fun greeting(subject: String): String = + "{\n\"greeting\":\"welcome $subject\"\n}" +} diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/examples/securedapi/ExampleAppWithSecuredApiTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/examples/securedapi/ExampleAppWithSecuredApiTest.kt new file mode 100644 index 00000000..c769b948 --- /dev/null +++ b/src/test/kotlin/no/nav/security/mock/oauth2/examples/securedapi/ExampleAppWithSecuredApiTest.kt @@ -0,0 +1,60 @@ +package no.nav.security.mock.oauth2.examples.securedapi + +import com.nimbusds.jwt.SignedJWT +import no.nav.security.mock.oauth2.MockOAuth2Server +import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class ExampleAppWithSecuredApiTest { + private lateinit var client: OkHttpClient + private lateinit var oAuth2Server: MockOAuth2Server + private lateinit var exampleApp: ExampleAppWithSecuredApi + + private val ISSUER_ID = "test" + + @BeforeEach + fun before() { + oAuth2Server = MockOAuth2Server() + oAuth2Server.start() + exampleApp = ExampleAppWithSecuredApi(oAuth2Server.wellKnownUrl(ISSUER_ID).toString()) + exampleApp.start() + client = OkHttpClient().newBuilder().build() + } + + @AfterEach + fun shutdown() { + oAuth2Server.shutdown() + exampleApp.shutdown() + } + + @Test + fun apiShouldDenyAccessWithoutValidToken() { + val response: Response = client.newCall( + Request.Builder() + .url(exampleApp.url("/api")) + .get() + .build() + ).execute() + assertThat(response.code).isEqualTo(401) + } + + @Test + fun apiShouldAllowAccessWhenTokenIsValid() { + val token: SignedJWT = oAuth2Server.issueToken(ISSUER_ID, "myclient", DefaultOAuth2TokenCallback()) + val response: Response = client.newCall( + Request.Builder() + .url(exampleApp.url("/api")) + .addHeader("Authorization", token.serialize()) + .get() + .build() + ).execute() + assertThat(response.code).isEqualTo(200) + assertThat(response.body?.string()).contains(token.jwtClaimsSet.subject) + } +}