Skip to content

Commit c2f7a28

Browse files
authored
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
1 parent fb191cc commit c2f7a28

10 files changed

+465
-8
lines changed

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import no.nav.security.mock.oauth2.extensions.toWellKnownUrl
1616
import no.nav.security.mock.oauth2.http.OAuth2HttpRequestHandler
1717
import no.nav.security.mock.oauth2.http.OAuth2HttpResponse
1818
import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
19-
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
2019
import okhttp3.HttpUrl
2120
import okhttp3.mockwebserver.Dispatcher
2221
import okhttp3.mockwebserver.MockResponse
@@ -31,11 +30,9 @@ import java.util.concurrent.LinkedBlockingQueue
3130
private val log = KotlinLogging.logger {}
3231

3332
class MockOAuth2Server(
34-
config: OAuth2Config = OAuth2Config()
33+
val config: OAuth2Config = OAuth2Config()
3534
) {
3635
private val mockWebServer: MockWebServer = MockWebServer()
37-
private val tokenProvider: OAuth2TokenProvider =
38-
OAuth2TokenProvider()
3936

4037
var dispatcher: Dispatcher = MockOAuth2Dispatcher(config)
4138

@@ -76,7 +73,7 @@ class MockOAuth2Server(
7673
ClientSecretBasic(ClientID(clientId), Secret("secret")),
7774
AuthorizationCodeGrant(AuthorizationCode("123"), URI.create("http://localhost"))
7875
)
79-
return tokenProvider.accessToken(tokenRequest, issuerUrl, null, OAuth2TokenCallback)
76+
return config.tokenProvider.accessToken(tokenRequest, issuerUrl, null, OAuth2TokenCallback)
8077
}
8178
}
8279

@@ -95,7 +92,6 @@ class MockOAuth2Dispatcher(
9592
else -> mockResponse(httpRequestHandler.handleRequest(request.asOAuth2HttpRequest()))
9693
}
9794

98-
9995
private fun mockResponse(response: OAuth2HttpResponse): MockResponse =
10096
MockResponse()
10197
.setHeaders(response.headers)

src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallback.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ open class DefaultOAuth2TokenCallback(
2727

2828
override fun subject(tokenRequest: TokenRequest): String {
2929
return when (GrantType.CLIENT_CREDENTIALS) {
30-
tokenRequest.grantType() -> tokenRequest.clientID.value
30+
tokenRequest.grantType() -> tokenRequest.clientIdAsString()
3131
else -> subject
3232
}
3333
}

src/test/kotlin/no/nav/security/mock/oauth2/MockOAuth2ServerTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class MockOAuth2ServerTest {
6060
}
6161

6262
@Test
63-
fun enqueuedResponse(){
63+
fun enqueuedResponse() {
6464
assertWellKnownResponseForIssuer("default")
6565
server.enqueueResponse(MockResponse()
6666
.setResponseCode(200)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package no.nav.security.mock.oauth2.examples
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import com.nimbusds.jose.JOSEObjectType
5+
import com.nimbusds.jose.JWSAlgorithm
6+
import com.nimbusds.jose.jwk.JWKSet
7+
import com.nimbusds.jose.jwk.source.ImmutableJWKSet
8+
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier
9+
import com.nimbusds.jose.proc.JWSKeySelector
10+
import com.nimbusds.jose.proc.JWSVerificationKeySelector
11+
import com.nimbusds.jose.proc.SecurityContext
12+
import com.nimbusds.jose.util.DefaultResourceRetriever
13+
import com.nimbusds.jwt.JWTClaimsSet
14+
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor
15+
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier
16+
import com.nimbusds.jwt.proc.DefaultJWTProcessor
17+
import com.nimbusds.oauth2.sdk.id.Issuer
18+
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
19+
import mu.KotlinLogging
20+
import okhttp3.HttpUrl
21+
import okhttp3.OkHttpClient
22+
import okhttp3.Request
23+
import okhttp3.mockwebserver.Dispatcher
24+
import okhttp3.mockwebserver.MockResponse
25+
import okhttp3.mockwebserver.MockWebServer
26+
import okhttp3.mockwebserver.RecordedRequest
27+
import java.net.URL
28+
import java.util.HashSet
29+
30+
private val log = KotlinLogging.logger {}
31+
32+
abstract class AbstractExampleApp(oauth2DiscoveryUrl: String) {
33+
34+
val oauth2Client: OkHttpClient = OkHttpClient()
35+
.newBuilder()
36+
.followRedirects(false)
37+
.build()
38+
39+
val metadata = OIDCProviderMetadata.parse(DefaultResourceRetriever().retrieveResource(URL(oauth2DiscoveryUrl)).content)
40+
41+
lateinit var exampleApp: MockWebServer
42+
43+
fun start() {
44+
exampleApp = MockWebServer()
45+
exampleApp.start()
46+
exampleApp.dispatcher = object : Dispatcher() {
47+
override fun dispatch(request: RecordedRequest): MockResponse {
48+
return runCatching {
49+
handleRequest(request)
50+
}.fold(
51+
onSuccess = { result -> result },
52+
onFailure = { error ->
53+
log.error("received unhandled exception.", error)
54+
MockResponse()
55+
.setResponseCode(500)
56+
.setBody("unhandled exception with message ${error.message}")
57+
}
58+
)
59+
}
60+
}
61+
}
62+
63+
fun shutdown() {
64+
exampleApp.shutdown()
65+
}
66+
67+
fun url(path: String): HttpUrl = exampleApp.url(path)
68+
69+
fun retrieveJwks(): JWKSet {
70+
return oauth2Client.newCall(
71+
Request.Builder()
72+
.url(metadata.jwkSetURI.toURL())
73+
.get()
74+
.build()
75+
).execute().body?.string()?.let {
76+
JWKSet.parse(it)
77+
} ?: throw RuntimeException("could not retrieve jwks")
78+
}
79+
80+
fun verifyJwt(jwt: String, issuer: Issuer, jwkSet: JWKSet): JWTClaimsSet {
81+
val jwtProcessor: ConfigurableJWTProcessor<SecurityContext?> = DefaultJWTProcessor()
82+
jwtProcessor.jwsTypeVerifier = DefaultJOSEObjectTypeVerifier(JOSEObjectType("JWT"))
83+
val keySelector: JWSKeySelector<SecurityContext?> = JWSVerificationKeySelector(
84+
JWSAlgorithm.RS256,
85+
ImmutableJWKSet(jwkSet)
86+
)
87+
jwtProcessor.jwsKeySelector = keySelector
88+
jwtProcessor.jwtClaimsSetVerifier = DefaultJWTClaimsVerifier(
89+
JWTClaimsSet.Builder().issuer(issuer.toString()).build(),
90+
HashSet(listOf("sub", "iat", "exp", "aud"))
91+
)
92+
return try {
93+
jwtProcessor.process(jwt, null)
94+
} catch (e: Exception) {
95+
throw RuntimeException("invalid jwt.", e)
96+
}
97+
}
98+
99+
fun bearerToken(request: RecordedRequest): String? =
100+
request.headers["Authorization"]
101+
?.split("Bearer ")
102+
?.let { it[0] }
103+
104+
fun notAuthorized(): MockResponse = MockResponse().setResponseCode(401)
105+
106+
fun json(value: Any): MockResponse = MockResponse()
107+
.setResponseCode(200)
108+
.setHeader("Content-Type","application/json")
109+
.setBody(ObjectMapper().writeValueAsString(value))
110+
111+
abstract fun handleRequest(request: RecordedRequest): MockResponse
112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package no.nav.security.mock.oauth2.examples.clientcredentials
2+
3+
import com.fasterxml.jackson.databind.JsonNode
4+
import com.fasterxml.jackson.databind.ObjectMapper
5+
import com.fasterxml.jackson.module.kotlin.readValue
6+
import no.nav.security.mock.oauth2.examples.AbstractExampleApp
7+
import okhttp3.Credentials
8+
import okhttp3.FormBody
9+
import okhttp3.Request
10+
import okhttp3.Response
11+
import okhttp3.mockwebserver.MockResponse
12+
import okhttp3.mockwebserver.RecordedRequest
13+
14+
class ExampleAppWithClientCredentialsClient(oauth2DiscoveryUrl: String) : AbstractExampleApp(oauth2DiscoveryUrl) {
15+
16+
override fun handleRequest(request: RecordedRequest): MockResponse {
17+
return getClientCredentialsAccessToken()
18+
?.let {
19+
MockResponse()
20+
.setResponseCode(200)
21+
.setBody("token=$it")
22+
}
23+
?: MockResponse().setResponseCode(500).setBody("could not get access_token")
24+
}
25+
26+
private fun getClientCredentialsAccessToken(): String? {
27+
val tokenResponse: Response = oauth2Client.newCall(
28+
Request.Builder()
29+
.url(metadata.tokenEndpointURI.toURL())
30+
.addHeader("Authorization", Credentials.basic("ExampleAppWithClientCredentialsClient", "test"))
31+
.post(
32+
FormBody.Builder()
33+
.add("client_id", "ExampleAppWithClientCredentialsClient")
34+
.add("scope", "scope1")
35+
.add("grant_type", "client_credentials")
36+
.build()
37+
)
38+
.build()
39+
).execute()
40+
return tokenResponse.body?.string()?.let {
41+
ObjectMapper().readValue<JsonNode>(it).get("access_token")?.textValue()
42+
}
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package no.nav.security.mock.oauth2.examples.clientcredentials
2+
3+
import com.nimbusds.jwt.SignedJWT
4+
import no.nav.security.mock.oauth2.MockOAuth2Server
5+
import okhttp3.OkHttpClient
6+
import okhttp3.Request
7+
import okhttp3.Response
8+
import org.assertj.core.api.Assertions.assertThat
9+
import org.junit.jupiter.api.AfterEach
10+
import org.junit.jupiter.api.BeforeEach
11+
import org.junit.jupiter.api.Test
12+
13+
internal class ExampleAppWithClientCredentialsClientTest {
14+
private lateinit var client: OkHttpClient
15+
private lateinit var oAuth2Server: MockOAuth2Server
16+
private lateinit var exampleApp: ExampleAppWithClientCredentialsClient
17+
18+
private val ISSUER_ID = "test"
19+
20+
@BeforeEach
21+
fun before() {
22+
oAuth2Server = MockOAuth2Server()
23+
oAuth2Server.start()
24+
exampleApp = ExampleAppWithClientCredentialsClient(oAuth2Server.wellKnownUrl(ISSUER_ID).toString())
25+
exampleApp.start()
26+
client = OkHttpClient().newBuilder().build()
27+
}
28+
29+
@AfterEach
30+
fun shutdown() {
31+
oAuth2Server.shutdown()
32+
exampleApp.shutdown()
33+
}
34+
35+
@Test
36+
fun appShouldReturnClientCredentialsAccessTokenWhenInvoked() {
37+
val response: Response = client.newCall(
38+
Request.Builder()
39+
.url(exampleApp.url("/clientcredentials"))
40+
.get()
41+
.build()
42+
).execute()
43+
assertThat(response.code).isEqualTo(200)
44+
45+
val token: SignedJWT? = response.body?.string()
46+
?.split("token=")
47+
?.let { it[1] }
48+
?.let { SignedJWT.parse(it) }
49+
50+
assertThat(token).isNotNull
51+
assertThat(token?.jwtClaimsSet?.subject).isEqualTo("ExampleAppWithClientCredentialsClient")
52+
}
53+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package no.nav.security.mock.oauth2.examples.openidconnect
2+
3+
import com.fasterxml.jackson.databind.JsonNode
4+
import com.fasterxml.jackson.databind.ObjectMapper
5+
import com.fasterxml.jackson.module.kotlin.readValue
6+
import com.nimbusds.jwt.JWTClaimsSet
7+
import com.nimbusds.openid.connect.sdk.AuthenticationRequest
8+
import mu.KotlinLogging
9+
import no.nav.security.mock.oauth2.examples.AbstractExampleApp
10+
import okhttp3.FormBody
11+
import okhttp3.Request
12+
import okhttp3.mockwebserver.MockResponse
13+
import okhttp3.mockwebserver.RecordedRequest
14+
15+
private val log = KotlinLogging.logger {}
16+
17+
class ExampleAppWithOpenIdConnect(oidcDiscoveryUrl: String) : AbstractExampleApp(oidcDiscoveryUrl) {
18+
19+
override fun handleRequest(request: RecordedRequest): MockResponse {
20+
return when (request.requestUrl?.encodedPath) {
21+
"/login" -> {
22+
MockResponse()
23+
.setResponseCode(302)
24+
.setHeader("Location", authenticationRequest().toURI())
25+
}
26+
"/callback" -> {
27+
log.debug("got callback: $request")
28+
val code = request.requestUrl?.queryParameter("code")!!
29+
val tokenResponse = oauth2Client.newCall(
30+
Request.Builder()
31+
.url(metadata.tokenEndpointURI.toURL())
32+
.post(
33+
FormBody.Builder()
34+
.add("client_id", "client1")
35+
.add("scope", authenticationRequest().scope.toString())
36+
.add("code", code)
37+
.add("redirect_uri", exampleApp.url("/callback").toString())
38+
.add("grant_type", "authorization_code")
39+
.build()
40+
)
41+
.build()
42+
).execute()
43+
val idToken: String = ObjectMapper().readValue<JsonNode>(tokenResponse.body!!.string()).get("id_token").textValue()
44+
val idTokenClaims: JWTClaimsSet = verifyJwt(idToken, metadata.issuer, retrieveJwks())
45+
MockResponse()
46+
.setResponseCode(200)
47+
.setHeader("Set-Cookie", "id_token=$idToken")
48+
.setBody("logged in as ${idTokenClaims.subject}")
49+
}
50+
"/secured" -> {
51+
getCookies(request)["id_token"]
52+
?.let {
53+
verifyJwt(it, metadata.issuer, retrieveJwks())
54+
}?.let {
55+
MockResponse()
56+
.setResponseCode(200)
57+
.setBody("welcome ${it.subject}")
58+
} ?: MockResponse().setResponseCode(302).setHeader("Location", exampleApp.url("/login"))
59+
}
60+
else -> MockResponse().setResponseCode(404)
61+
}
62+
}
63+
64+
private fun getCookies(request: RecordedRequest): Map<String, String> {
65+
return request.getHeader("Cookie")
66+
?.split(";")
67+
?.filter { it.contains("=") }
68+
?.associate {
69+
val (key, value) = it.split("=")
70+
key.trim() to value.trim()
71+
} ?: emptyMap()
72+
}
73+
74+
private fun authenticationRequest(): AuthenticationRequest =
75+
AuthenticationRequest.parse(
76+
metadata.authorizationEndpointURI,
77+
mutableMapOf(
78+
"client_id" to listOf("client"),
79+
"response_type" to listOf("code"),
80+
"redirect_uri" to listOf(exampleApp.url("/callback").toString()),
81+
"response_mode" to listOf("query"),
82+
"scope" to listOf("openid", "scope1"),
83+
"state" to listOf("1234"),
84+
"nonce" to listOf("5678")
85+
)
86+
)
87+
}

0 commit comments

Comments
 (0)