Skip to content

Commit 1e8d271

Browse files
fix(jwt): provide algorithm when verifying signature and issuer (#353)
* fix(jwt): provide algorithm when verifying signature and issuer * test(userinfo): add tests for non-default algorithm * test(introspect): add tests for non-default algorithm * docs: fix minor typos * chore: refactor files, cleanup imports * refactor(tests): remove extra println Co-authored-by: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com>
1 parent 5f69e56 commit 1e8d271

File tree

12 files changed

+132
-13
lines changed

12 files changed

+132
-13
lines changed

CONTRIBUTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ Please fork the repo and start a new branch to work on.
55

66
## Building locally
77
This project is using [Gradle](https://gradle.org/) for its build tool.
8-
A Gradle Wrapper is included in the code though so you do not have to manage your own installation.
8+
A Gradle Wrapper is included in the code though, so you do not have to manage your own installation.
99

10-
To run a build simply exucute the following:
10+
To run a build simply execute the following:
1111

1212
```shell script
1313
./gradlew build
@@ -23,7 +23,7 @@ If you are adding a new feature or bug fix please ensure there is proper test co
2323
If you have a branch on your fork that is ready to be merged, please create a new pull request. The maintainers will review to make sure the above guidelines have been followed and if the changes are helpful to all library users, they will be merged.
2424

2525
## Releasing
26-
The release process has been automated in Github Actions. Every merge into master is automatically added to the
26+
The release process has been automated in GitHub Actions. Every merge into master is automatically added to the
2727
[draft release notes](https://github.com/navikt/mock-oauth2-server/releases) of the next version. Once the next
2828
version is ready to be released, simply publish the release with the version name as the title and tag and this
2929
will trigger to publishing process.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ Have a look at some examples in both Java and Kotlin in the src/test directory:
194194

195195
##### Server URLs
196196

197-
You can retrieve URLs from the server with the correct port and issuerId etc by invoking one of the ` fun *Url(issuerId: String): HttpUrl` functions/methods:
197+
You can retrieve URLs from the server with the correct port and issuerId etc. by invoking one of the ` fun *Url(issuerId: String): HttpUrl` functions/methods:
198198

199199
```kotlin
200200
val server = MockOAuth2Server()
@@ -293,7 +293,7 @@ add this to your config with preferred `JWS algorithm`:
293293

294294
*From the JSON example above:*
295295

296-
A token request to `http://localhost:8080/issuer1/token` with parameter `scope` equal to `scope1` will match the first tokencallback:
296+
A token request to `http://localhost:8080/issuer1/token` with parameter `scope` equal to `scope1` will match the first `tokenCallback`:
297297

298298
```json
299299
{
@@ -448,7 +448,7 @@ This project is currently maintained by the organisation [@navikt](https://githu
448448

449449
If you need to raise an issue or question about this library, please create an issue here and tag it with the appropriate label.
450450

451-
For contact requests within the [@navikt](https://github.com/navikt) org, you can use the slack channel #pig_sikkerhet
451+
For contact requests within the [@navikt](https://github.com/navikt) org, you can use the Slack channel #pig_sikkerhet
452452

453453
If you need to contact anyone directly, please see contributors.
454454

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ open class MockOAuth2Server(
4545
vararg additionalRoutes: Route
4646
) {
4747
constructor(vararg additionalRoutes: Route) : this(config = OAuth2Config(), additionalRoutes = additionalRoutes)
48+
constructor(config: OAuth2Config) : this(config = config, additionalRoutes = emptyArray())
4849

4950
private val httpServer = config.httpServer
5051
private val defaultRequestHandler: OAuth2HttpRequestHandler = OAuth2HttpRequestHandler(config)
@@ -304,9 +305,10 @@ internal fun Map<String, Any>.toJwtClaimsSet(): JWTClaimsSet =
304305
}.build()
305306

306307
fun <R> withMockOAuth2Server(
308+
config: OAuth2Config = OAuth2Config(),
307309
test: MockOAuth2Server.() -> R
308310
): R {
309-
val server = MockOAuth2Server()
311+
val server = MockOAuth2Server(config)
310312
server.start()
311313
try {
312314
return server.test()

src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ data class OAuth2HttpRequest(
149149
.query(originalUrl.query)
150150
.build()
151151
} ?: originalUrl
152-
153152
}
154153
}
155154

src/main/kotlin/no/nav/security/mock/oauth2/introspect/Introspect.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ private fun OAuth2HttpRequest.verifyToken(tokenProvider: OAuth2TokenProvider): J
5454
val tokenString = this.formParameters.get("token")
5555
val issuer = url.toIssuerUrl()
5656
val jwkSet = tokenProvider.publicJwkSet(issuer.issuerId())
57+
val algorithm = tokenProvider.getAlgorithm()
5758
return try {
58-
SignedJWT.parse(tokenString).verifySignatureAndIssuer(Issuer(issuer.toString()), jwkSet)
59+
SignedJWT.parse(tokenString).verifySignatureAndIssuer(Issuer(issuer.toString()), jwkSet, algorithm)
5960
} catch (e: Exception) {
6061
log.debug("token_introspection: failed signature validation")
6162
return null

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ data class KeyGenerator(
8686
}
8787
}
8888
).keyGenerator
89-
} else null
89+
} else {
90+
null
91+
}
9092
}.singleOrNull() ?: throw OAuth2Exception("Unsupported algorithm: $algorithm")
9193
}
9294

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ class OAuth2TokenProvider @JvmOverloads constructor(
2727
return JWKSet(keyProvider.signingKey(issuerId)).toPublicJWKSet()
2828
}
2929

30+
fun getAlgorithm(): JWSAlgorithm {
31+
return keyProvider.algorithm()
32+
}
33+
3034
fun idToken(
3135
tokenRequest: TokenRequest,
3236
issuerUrl: HttpUrl,

src/main/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfo.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ private fun OAuth2HttpRequest.verifyBearerToken(tokenProvider: OAuth2TokenProvid
3030
val tokenString = this.headers.bearerToken()
3131
val issuer = url.toIssuerUrl()
3232
val jwkSet = tokenProvider.publicJwkSet(issuer.issuerId())
33+
val algorithm = tokenProvider.getAlgorithm()
3334
return try {
34-
SignedJWT.parse(tokenString).verifySignatureAndIssuer(Issuer(issuer.toString()), jwkSet)
35+
SignedJWT.parse(tokenString).verifySignatureAndIssuer(Issuer(issuer.toString()), jwkSet, algorithm)
3536
} catch (e: Exception) {
3637
throw invalidToken(e.message ?: "could not verify bearer token")
3738
}

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

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

33
import com.nimbusds.jwt.JWTClaimsSet
4-
import io.kotest.core.spec.style.AnnotationSpec
54
import io.kotest.matchers.collections.shouldContainExactly
65
import io.kotest.matchers.should
76
import io.kotest.matchers.shouldBe

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
package no.nav.security.mock.oauth2.e2e
22

3+
import com.nimbusds.jose.JWSAlgorithm
34
import com.nimbusds.jwt.SignedJWT
45
import io.kotest.assertions.asClue
56
import io.kotest.matchers.maps.shouldContainAll
7+
import io.kotest.matchers.shouldBe
8+
import no.nav.security.mock.oauth2.MockOAuth2Server
9+
import no.nav.security.mock.oauth2.OAuth2Config
610
import no.nav.security.mock.oauth2.testutils.claims
711
import no.nav.security.mock.oauth2.testutils.client
812
import no.nav.security.mock.oauth2.testutils.get
913
import no.nav.security.mock.oauth2.testutils.parse
14+
import no.nav.security.mock.oauth2.token.KeyProvider
15+
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
1016
import no.nav.security.mock.oauth2.withMockOAuth2Server
1117
import okhttp3.Headers
1218
import org.junit.jupiter.api.Test
1319

1420
class UserInfoIntegrationTest {
1521

1622
private val client = client()
23+
private val rs384Config = OAuth2Config(
24+
tokenProvider = OAuth2TokenProvider(keyProvider = KeyProvider(initialKeys = emptyList(), algorithm = JWSAlgorithm.RS384.name))
25+
)
1726

1827
@Test
1928
fun `userinfo should return claims from token when valid bearer token is present`() {
@@ -33,6 +42,41 @@ class UserInfoIntegrationTest {
3342
}
3443
}
3544

45+
@Test
46+
fun `userinfo should return claims from token signed with non-default algorithm when valid bearer token is present`() {
47+
withMockOAuth2Server(config = rs384Config) {
48+
val issuerId = "default"
49+
val token = this.issueToken(issuerId = issuerId, subject = "foo", claims = mapOf("extra" to "bar"))
50+
token.header.algorithm.shouldBe(JWSAlgorithm.RS384)
51+
client.get(
52+
url = this.userInfoUrl(issuerId),
53+
headers = token.asBearerTokenHeader()
54+
).asClue {
55+
it.parse<Map<String, Any>>() shouldContainAll mapOf(
56+
"sub" to token.claims["sub"],
57+
"iss" to token.claims["iss"],
58+
"extra" to token.claims["extra"]
59+
)
60+
}
61+
}
62+
}
63+
64+
@Test
65+
fun `userinfo should return error from token signed with non-default algorithm does not match server config`() {
66+
val issuerId = "default"
67+
val rs384Server = MockOAuth2Server(config = rs384Config)
68+
val token = rs384Server.issueToken(issuerId = issuerId, subject = "foo", claims = mapOf("extra" to "bar"))
69+
withMockOAuth2Server {
70+
client.get(
71+
url = this.userInfoUrl(issuerId),
72+
headers = token.asBearerTokenHeader()
73+
).asClue {
74+
it.code shouldBe 401
75+
it.message shouldBe "Client Error"
76+
}
77+
}
78+
}
79+
3680
private fun SignedJWT.asBearerTokenHeader(): Headers = this.serialize().let {
3781
Headers.headersOf("Authorization", "Bearer $it")
3882
}

0 commit comments

Comments
 (0)