Skip to content

Commit 5484d34

Browse files
tommytroenybelMekk
andauthored
feat: support refreshtoken (as JWT) from keycloak (#242)
* feat: support keycloak refresh token format * includes nonce from auth request in a plain JWT * see #210 Co-authored-by: Youssef Bel Mekki <38552193+ybelMekk@users.noreply.github.com>
1 parent 6ee165a commit 5484d34

File tree

4 files changed

+64
-4
lines changed

4 files changed

+64
-4
lines changed

src/main/kotlin/no/nav/security/mock/oauth2/grant/AuthorizationCodeGrantHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ internal class AuthorizationCodeHandler(
7676
val loginTokenCallbackOrDefault = getLoginTokenCallbackOrDefault(code, oAuth2TokenCallback)
7777
val idToken: SignedJWT = tokenProvider.idToken(tokenRequest, issuerUrl, loginTokenCallbackOrDefault, nonce)
7878
val accessToken: SignedJWT = tokenProvider.accessToken(tokenRequest, issuerUrl, loginTokenCallbackOrDefault, nonce)
79-
val refreshToken: RefreshToken = refreshTokenManager.refreshToken(loginTokenCallbackOrDefault)
79+
val refreshToken: RefreshToken = refreshTokenManager.refreshToken(loginTokenCallbackOrDefault, nonce)
8080

8181
return OAuth2TokenResponse(
8282
tokenType = "Bearer",
Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package no.nav.security.mock.oauth2.grant
22

3-
import java.util.UUID
3+
import com.nimbusds.jwt.JWTClaimsSet
4+
import com.nimbusds.jwt.PlainJWT
45
import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
6+
import java.util.UUID
57

68
typealias RefreshToken = String
79

@@ -10,9 +12,21 @@ internal data class RefreshTokenManager(
1012
) {
1113
operator fun get(refreshToken: RefreshToken) = cache[refreshToken]
1214

13-
fun refreshToken(tokenCallback: OAuth2TokenCallback): RefreshToken {
14-
val refreshToken = UUID.randomUUID().toString()
15+
fun refreshToken(tokenCallback: OAuth2TokenCallback, nonce: String?): RefreshToken {
16+
val jti = UUID.randomUUID().toString()
17+
// added for compatibility with keycloak js client which expects a jwt with nonce
18+
val refreshToken = nonce?.let { plainJWT(jti, nonce) } ?: jti
1519
cache[refreshToken] = tokenCallback
1620
return refreshToken
1721
}
22+
23+
private fun plainJWT(jti: String, nonce: String?): String =
24+
PlainJWT(
25+
JWTClaimsSet.parse(
26+
mapOf(
27+
"jti" to jti,
28+
"nonce" to nonce
29+
)
30+
)
31+
).serialize()
1832
}

src/test/kotlin/no/nav/security/mock/oauth2/grant/AuthorizationCodeHandlerTest.kt

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

33
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
4+
import com.nimbusds.jwt.PlainJWT
45
import com.nimbusds.jwt.SignedJWT
56
import com.nimbusds.oauth2.sdk.ResponseMode
67
import com.nimbusds.openid.connect.sdk.AuthenticationRequest
@@ -99,6 +100,16 @@ internal class AuthorizationCodeHandlerTest {
99100
}
100101
}
101102

103+
@Test
104+
fun `auth request with nonce should result in a token response with refresh token as a JWT containing the nonce`() {
105+
val code: String = handler.retrieveAuthorizationCode(Login("foo"))
106+
107+
handler.tokenResponse(tokenRequest(code = code), "http://myissuer".toHttpUrl(), DefaultOAuth2TokenCallback()).asClue {
108+
val claims = PlainJWT.parse(it.refreshToken).jwtClaimsSet.claims
109+
claims["nonce"] shouldBe "5678"
110+
}
111+
}
112+
102113
private fun AuthorizationCodeHandler.retrieveAuthorizationCode(login: Login): String =
103114
authorizationCodeResponse(
104115
authenticationRequest = "http://authorizationendpoint".toHttpUrl().authenticationRequest().asNimbusAuthRequest(),
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package no.nav.security.mock.oauth2.grant
2+
3+
import com.nimbusds.jwt.PlainJWT
4+
import io.kotest.assertions.asClue
5+
import io.kotest.matchers.shouldBe
6+
import io.kotest.matchers.shouldNotBe
7+
import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback
8+
import org.junit.jupiter.api.Test
9+
10+
internal class RefreshTokenManagerTest {
11+
12+
@Test
13+
fun `refresh token should be a jwt with nonce included if nonce is not null (for keycloak compatibility)`() {
14+
val mgr = RefreshTokenManager()
15+
val tokenCallback = DefaultOAuth2TokenCallback()
16+
17+
mgr.refreshToken(tokenCallback, "nonce123").asClue {
18+
val claims = PlainJWT.parse(it).jwtClaimsSet.claims
19+
20+
claims["nonce"] shouldBe "nonce123"
21+
claims["jti"] shouldNotBe null
22+
}
23+
}
24+
25+
@Test
26+
fun `tokencallback should be available in cache for specific refresh token`() {
27+
val mgr = RefreshTokenManager()
28+
val tokenCallback = DefaultOAuth2TokenCallback()
29+
30+
val refreshToken = mgr.refreshToken(tokenCallback, null)
31+
mgr[refreshToken] shouldBe tokenCallback
32+
val refreshToken2 = mgr.refreshToken(tokenCallback, "nonce123")
33+
mgr[refreshToken2] shouldBe tokenCallback
34+
}
35+
}

0 commit comments

Comments
 (0)