Skip to content

Commit 35a7869

Browse files
committed
tests: restructure tests, general readability improvements
1 parent 21e5952 commit 35a7869

File tree

9 files changed

+396
-633
lines changed

9 files changed

+396
-633
lines changed

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

Lines changed: 32 additions & 517 deletions
Large diffs are not rendered by default.

src/test/kotlin/no/nav/security/mock/oauth2/e2e/JwtBearerGrantIntegrationTest.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,19 @@ import io.kotest.matchers.string.shouldContain
1010
import no.nav.security.mock.oauth2.testutils.ParsedTokenResponse
1111
import no.nav.security.mock.oauth2.testutils.audience
1212
import no.nav.security.mock.oauth2.testutils.claims
13+
import no.nav.security.mock.oauth2.testutils.client
1314
import no.nav.security.mock.oauth2.testutils.shouldBeValidFor
1415
import no.nav.security.mock.oauth2.testutils.subject
1516
import no.nav.security.mock.oauth2.testutils.toTokenResponse
1617
import no.nav.security.mock.oauth2.testutils.tokenRequest
1718
import no.nav.security.mock.oauth2.testutils.verifyWith
1819
import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback
1920
import no.nav.security.mock.oauth2.withMockOAuth2Server
20-
import okhttp3.OkHttpClient
2121
import org.junit.jupiter.api.Test
2222

2323
class JwtBearerGrantIntegrationTest {
2424

25-
private val client: OkHttpClient = OkHttpClient()
26-
.newBuilder()
27-
.followRedirects(false)
28-
.build()
25+
private val client = client()
2926

3027
@Test
3128
fun `token request with JwtBearerGrant should exchange assertion with a new token containing many of the same claims`() {
@@ -57,7 +54,8 @@ class JwtBearerGrantIntegrationTest {
5754
response shouldBeValidFor GrantType.JWT_BEARER
5855
response.scope shouldContain "scope1"
5956
response.issuedTokenType shouldBe null
60-
response.accessToken!! should verifyWith(issuerId, this)
57+
response.accessToken.shouldNotBeNull()
58+
response.accessToken should verifyWith(issuerId, this)
6159
response.accessToken.subject shouldBe initialSubject
6260
response.accessToken.audience shouldContainExactly listOf("scope1")
6361
response.accessToken.claims["claim1"] shouldBe "value1"
@@ -66,7 +64,7 @@ class JwtBearerGrantIntegrationTest {
6664
}
6765

6866
@Test
69-
fun `token request with JwtBearerGrant should exchange assertion with a new token with scope specified in assertion claim or request parmas`() {
67+
fun `token request with JwtBearerGrant should exchange assertion with a new token with scope specified in assertion claim or request params`() {
7068
withMockOAuth2Server {
7169
val initialSubject = "mysub"
7270
val initialToken = this.issueToken(
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package no.nav.security.mock.oauth2.e2e
2+
3+
import com.nimbusds.jose.jwk.JWKSet
4+
import com.nimbusds.jwt.SignedJWT
5+
import com.nimbusds.oauth2.sdk.id.Issuer
6+
import io.kotest.assertions.asClue
7+
import io.kotest.matchers.collections.shouldContainExactly
8+
import io.kotest.matchers.nulls.shouldNotBeNull
9+
import io.kotest.matchers.shouldBe
10+
import io.kotest.matchers.string.shouldContain
11+
import io.kotest.matchers.string.shouldStartWith
12+
import no.nav.security.mock.oauth2.MockOAuth2Server
13+
import no.nav.security.mock.oauth2.extensions.verifySignatureAndIssuer
14+
import no.nav.security.mock.oauth2.http.OAuth2HttpResponse
15+
import no.nav.security.mock.oauth2.http.WellKnown
16+
import no.nav.security.mock.oauth2.http.route
17+
import no.nav.security.mock.oauth2.testutils.audience
18+
import no.nav.security.mock.oauth2.testutils.claims
19+
import no.nav.security.mock.oauth2.testutils.client
20+
import no.nav.security.mock.oauth2.testutils.get
21+
import no.nav.security.mock.oauth2.testutils.issuer
22+
import no.nav.security.mock.oauth2.testutils.parse
23+
import no.nav.security.mock.oauth2.testutils.post
24+
import no.nav.security.mock.oauth2.testutils.subject
25+
import no.nav.security.mock.oauth2.testutils.toTokenResponse
26+
import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback
27+
import no.nav.security.mock.oauth2.withMockOAuth2Server
28+
import okhttp3.HttpUrl.Companion.toHttpUrl
29+
import org.junit.jupiter.api.Test
30+
import java.time.Duration
31+
32+
class MockOAuth2ServerIntegrationTest {
33+
private val client = client()
34+
35+
@Test
36+
fun `server with addtional routes should serve wellknown and additional route`() {
37+
val s = MockOAuth2Server(
38+
route("/custom") {
39+
OAuth2HttpResponse(status = 200, body = "custom route")
40+
}
41+
).apply {
42+
start()
43+
}
44+
client.get(s.wellKnownUrl("someissuer")).body?.string() shouldContain s.issuerUrl("someissuer").toString()
45+
client.get(s.url("/custom")).body?.string() shouldBe "custom route"
46+
client.get(s.url("/someissuer/custom")).body?.string() shouldBe "custom route"
47+
}
48+
49+
@Test
50+
fun `server on fixed port should include fixed port in wellknown and issued token`() {
51+
val port = 1234
52+
val server = MockOAuth2Server()
53+
server.start(port = port)
54+
55+
val issuerUrl = "http://localhost:$port/issuer1"
56+
client.get("$issuerUrl/.well-known/openid-configuration".toHttpUrl()).asClue {
57+
it.parse<WellKnown>().issuer shouldBe issuerUrl
58+
server.issueToken("issuer1").issuer shouldBe issuerUrl
59+
}
60+
61+
server.shutdown()
62+
}
63+
64+
@Test
65+
fun `wellknown should include issuer id in urls`() {
66+
withMockOAuth2Server {
67+
val baseUrl = this.baseUrl().toString().removeSuffix("/")
68+
client.get(this.wellKnownUrl("default")).let {
69+
it.parse<WellKnown>() urlsShouldStartWith "$baseUrl/default"
70+
}
71+
client.get(this.wellKnownUrl("foo")).let {
72+
it.parse<WellKnown>() urlsShouldStartWith "$baseUrl/foo"
73+
}
74+
client.get(this.wellKnownUrl("path1/path2/path3")).let {
75+
it.parse<WellKnown>() urlsShouldStartWith "$baseUrl/path1/path2/path3"
76+
}
77+
}
78+
}
79+
80+
@Test
81+
fun `token request with enqueued token callback should return claims from tokencallback (with exception of id_token and oidc rules)`() {
82+
val server = MockOAuth2Server().apply { start() }
83+
server.enqueueCallback(
84+
DefaultOAuth2TokenCallback(
85+
issuerId = "custom",
86+
subject = "yolo",
87+
audience = listOf("myaud")
88+
)
89+
)
90+
91+
client.post(
92+
server.tokenEndpointUrl("custom"),
93+
mapOf(
94+
"client_id" to "client1",
95+
"client_secret" to "secret",
96+
"grant_type" to "authorization_code",
97+
"scope" to "openid scope1",
98+
"redirect_uri" to "http://mycallback",
99+
"code" to "1234"
100+
)
101+
).toTokenResponse().asClue {
102+
it.idToken.shouldNotBeNull()
103+
it.idToken.subject shouldBe "yolo"
104+
it.idToken.audience shouldBe listOf("client1")
105+
it.accessToken.shouldNotBeNull()
106+
it.accessToken.subject shouldBe "yolo"
107+
it.accessToken.audience shouldBe listOf("myaud")
108+
}
109+
server.shutdown()
110+
}
111+
112+
@Test
113+
fun `issue token directly from server should contain claims from callback and be verifiable with server jwks`() {
114+
withMockOAuth2Server {
115+
val signedJWT: SignedJWT = this.issueToken(
116+
"default",
117+
"client1",
118+
DefaultOAuth2TokenCallback(
119+
issuerId = "default",
120+
subject = "mysub",
121+
audience = listOf("myaud"),
122+
claims = mapOf("someclaim" to "claimvalue")
123+
)
124+
)
125+
val wellKnown = client.get(this.wellKnownUrl("default")).parse<WellKnown>()
126+
val jwks = client.get(wellKnown.jwksUri.toHttpUrl()).body?.let { JWKSet.parse(it.string()) }
127+
128+
jwks.shouldNotBeNull()
129+
130+
signedJWT.verifySignatureAndIssuer(Issuer(wellKnown.issuer), jwks).asClue {
131+
it.issuer shouldBe wellKnown.issuer
132+
it.subject shouldBe "mysub"
133+
it.audience shouldContainExactly listOf("myaud")
134+
it.claims["someclaim"] shouldBe "claimvalue"
135+
}
136+
}
137+
}
138+
139+
@Test
140+
fun `anyToken should issue token with claims from input and be verifyable by servers keys`() {
141+
withMockOAuth2Server {
142+
val customIssuer = "https://customissuer".toHttpUrl()
143+
val token = this.anyToken(
144+
customIssuer,
145+
mutableMapOf(
146+
"sub" to "mysub",
147+
"aud" to listOf("myapp"),
148+
"customInt" to 123,
149+
"customList" to listOf(1, 2, 3)
150+
),
151+
Duration.ofSeconds(10)
152+
)
153+
154+
val wellKnown = client.get(this.wellKnownUrl("default")).parse<WellKnown>()
155+
val jwks = client.get(wellKnown.jwksUri.toHttpUrl()).body?.let { JWKSet.parse(it.string()) }
156+
157+
jwks.shouldNotBeNull()
158+
159+
token.verifySignatureAndIssuer(Issuer(customIssuer.toString()), jwks).asClue {
160+
it.issuer shouldBe customIssuer.toString()
161+
it.subject shouldBe "mysub"
162+
it.audience shouldContainExactly listOf("myapp")
163+
it.claims["customInt"] shouldBe 123
164+
it.claims["customList"] to listOf(1, 2, 3)
165+
}
166+
}
167+
}
168+
169+
private infix fun WellKnown.urlsShouldStartWith(url: String) {
170+
issuer shouldStartWith url
171+
authorizationEndpoint shouldStartWith url
172+
tokenEndpoint shouldStartWith url
173+
jwksUri shouldStartWith url
174+
endSessionEndpoint shouldStartWith url
175+
}
176+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package no.nav.security.mock.oauth2.e2e
2+
3+
import io.kotest.assertions.asClue
4+
import io.kotest.matchers.collections.shouldContainExactly
5+
import io.kotest.matchers.ints.shouldBeGreaterThan
6+
import io.kotest.matchers.nulls.shouldNotBeNull
7+
import io.kotest.matchers.shouldBe
8+
import io.kotest.matchers.shouldNotBe
9+
import io.kotest.matchers.string.shouldStartWith
10+
import no.nav.security.mock.oauth2.MockOAuth2Server
11+
import no.nav.security.mock.oauth2.OAuth2Config
12+
import no.nav.security.mock.oauth2.testutils.audience
13+
import no.nav.security.mock.oauth2.testutils.authenticationRequest
14+
import no.nav.security.mock.oauth2.testutils.client
15+
import no.nav.security.mock.oauth2.testutils.get
16+
import no.nav.security.mock.oauth2.testutils.post
17+
import no.nav.security.mock.oauth2.testutils.subject
18+
import no.nav.security.mock.oauth2.testutils.toTokenResponse
19+
import no.nav.security.mock.oauth2.testutils.tokenRequest
20+
import okhttp3.HttpUrl.Companion.toHttpUrl
21+
import org.junit.jupiter.api.Test
22+
23+
class OidcAuthorizationCodeGrantIntegrationTest {
24+
25+
private val server = MockOAuth2Server().apply { start() }
26+
private val client = client()
27+
28+
@Test
29+
fun `authentication request should return 302 with redirectUri as location and query params state and code`() {
30+
31+
client.get(
32+
server.authorizationEndpointUrl("default").authenticationRequest(redirectUri = "http://mycallback", state = "mystate")
33+
).asClue { response ->
34+
response.code shouldBe 302
35+
response.headers["location"]?.toHttpUrl().asClue {
36+
it.toString() shouldStartWith "http://mycallback"
37+
it?.queryParameterNames shouldContainExactly setOf("code", "state")
38+
it?.queryParameter("state") shouldBe "mystate"
39+
}
40+
}
41+
}
42+
43+
@Test
44+
fun `complete authorization code flow should return tokens according to spec`() {
45+
val code = client.get(server.authorizationEndpointUrl("default").authenticationRequest()).let { authResponse ->
46+
authResponse.headers["location"]?.toHttpUrl()?.queryParameter("code")
47+
}
48+
49+
code.shouldNotBeNull()
50+
51+
client.tokenRequest(
52+
server.tokenEndpointUrl("default"),
53+
mapOf(
54+
"client_id" to "client1",
55+
"client_secret" to "secret",
56+
"grant_type" to "authorization_code",
57+
"scope" to "openid scope1",
58+
"redirect_uri" to "http://mycallback",
59+
"code" to code
60+
)
61+
).toTokenResponse().asClue {
62+
it.accessToken shouldNotBe null
63+
it.idToken shouldNotBe null
64+
it.expiresIn shouldBeGreaterThan 0
65+
it.scope shouldBe "openid scope1"
66+
it.idToken?.audience shouldContainExactly listOf("client1")
67+
it.accessToken?.audience shouldContainExactly listOf("scope1")
68+
}
69+
}
70+
71+
@Test
72+
fun `complete authorization code flow with interactivelogin enable should return tokens with sub=username posted to login`() {
73+
val server = MockOAuth2Server(OAuth2Config(interactiveLogin = true)).apply { start() }
74+
// simulate user interaction by doing the auth request as a post (instead of get with user punching username/pwd and submitting form)
75+
val code = client.post(
76+
server.authorizationEndpointUrl("default").authenticationRequest(),
77+
mapOf("username" to "foo")
78+
).let { authResponse ->
79+
authResponse.headers["location"]?.toHttpUrl()?.queryParameter("code")
80+
}
81+
82+
code.shouldNotBeNull()
83+
84+
client.tokenRequest(
85+
server.tokenEndpointUrl("default"),
86+
mapOf(
87+
"client_id" to "client1",
88+
"client_secret" to "secret",
89+
"grant_type" to "authorization_code",
90+
"scope" to "openid scope1",
91+
"redirect_uri" to "http://mycallback",
92+
"code" to code
93+
)
94+
).toTokenResponse().asClue {
95+
it.accessToken shouldNotBe null
96+
it.idToken shouldNotBe null
97+
it.expiresIn shouldBeGreaterThan 0
98+
it.scope shouldBe "openid scope1"
99+
it.idToken?.audience shouldContainExactly listOf("client1")
100+
it.accessToken?.audience shouldContainExactly listOf("scope1")
101+
it.idToken?.subject shouldBe "foo"
102+
}
103+
server.shutdown()
104+
}
105+
}

src/test/kotlin/no/nav/security/mock/oauth2/e2e/RefreshTokenGrantIntegrationTest.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
package no.nav.security.mock.oauth2.e2e
22

33
import com.nimbusds.oauth2.sdk.GrantType
4+
import io.kotest.matchers.nulls.shouldNotBeNull
45
import io.kotest.matchers.should
56
import io.kotest.matchers.shouldBe
67
import io.kotest.matchers.shouldNotBe
78
import no.nav.security.mock.oauth2.testutils.audience
89
import no.nav.security.mock.oauth2.testutils.authenticationRequest
9-
import no.nav.security.mock.oauth2.testutils.authorizationCode
10+
import no.nav.security.mock.oauth2.testutils.client
11+
import no.nav.security.mock.oauth2.testutils.post
1012
import no.nav.security.mock.oauth2.testutils.shouldBeValidFor
1113
import no.nav.security.mock.oauth2.testutils.subject
1214
import no.nav.security.mock.oauth2.testutils.toTokenResponse
1315
import no.nav.security.mock.oauth2.testutils.tokenRequest
1416
import no.nav.security.mock.oauth2.testutils.verifyWith
1517
import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback
1618
import no.nav.security.mock.oauth2.withMockOAuth2Server
19+
import okhttp3.HttpUrl.Companion.toHttpUrl
1720
import okhttp3.OkHttpClient
1821
import org.junit.jupiter.api.Test
1922

2023
class RefreshTokenGrantIntegrationTest {
21-
private val client: OkHttpClient = OkHttpClient()
22-
.newBuilder()
23-
.followRedirects(false)
24-
.build()
24+
private val client: OkHttpClient = client()
2525

2626
@Test
2727
fun `token request with refresh_token grant should return id_token and access_token with same subject as authorization code grant`() {
@@ -30,8 +30,15 @@ class RefreshTokenGrantIntegrationTest {
3030
val issuerId = "idprovider"
3131

3232
// Authenticate using Authorization Code Flow
33-
val codeResponse = client.authenticationRequest(this.authorizationEndpointUrl(issuerId), initialSubject)
34-
val authorizationCode = checkNotNull(codeResponse.authorizationCode)
33+
// simulate user interaction by doing the auth request as a post (instead of get with user punching username/pwd and submitting form)
34+
val authorizationCode = client.post(
35+
this.authorizationEndpointUrl("default").authenticationRequest(),
36+
mapOf("username" to initialSubject)
37+
).let { authResponse ->
38+
authResponse.headers["location"]?.toHttpUrl()?.queryParameter("code")
39+
}
40+
41+
authorizationCode.shouldNotBeNull()
3542

3643
// Token Request based on authorization code
3744
val tokenResponseBeforeRefresh = client.tokenRequest(

0 commit comments

Comments
 (0)