Skip to content

Commit 94fcbfa

Browse files
authored
feat: support for refresh_token grant, some more tests and test utils (#12)
* feat: support for refresh_token grant, some more tests and test utils * chore: suppress warnings for functions et al which is part of public API * chore: reduce visibility for grant handler classes
1 parent 1abd804 commit 94fcbfa

19 files changed

+365
-63
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import okhttp3.mockwebserver.MockWebServer
3030
import okhttp3.mockwebserver.RecordedRequest
3131

3232
// TODO make open so others can extend?
33+
@Suppress("unused", "MemberVisibilityCanBePrivate")
3334
class MockOAuth2Server(
3435
val config: OAuth2Config = OAuth2Config()
3536
) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.nimbusds.oauth2.sdk.ErrorObject
44
import com.nimbusds.oauth2.sdk.GrantType
55
import com.nimbusds.oauth2.sdk.OAuth2Error
66

7+
@Suppress("unused")
78
class OAuth2Exception(val errorObject: ErrorObject?, msg: String, throwable: Throwable?) :
89
RuntimeException(msg, throwable) {
910
constructor(msg: String) : this(null, msg, null)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package no.nav.security.mock.oauth2.extensions
2+
3+
import java.net.URLDecoder
4+
import java.nio.charset.StandardCharsets
5+
6+
internal fun String.keyValuesToMap(listDelimiter: String): Map<String, String> =
7+
this.split(listDelimiter)
8+
.filter { it.contains("=") }
9+
.associate {
10+
val (key, value) = it.split("=")
11+
key.urlDecode().trim() to value.urlDecode().trim()
12+
}
13+
14+
internal fun String.urlDecode(): String = URLDecoder.decode(this, StandardCharsets.UTF_8)

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.nimbusds.oauth2.sdk.OAuth2Error
66
import com.nimbusds.oauth2.sdk.TokenRequest
77
import com.nimbusds.openid.connect.sdk.AuthenticationRequest
88
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse
9-
import java.util.UUID
109
import kotlin.collections.set
1110
import mu.KotlinLogging
1211
import no.nav.security.mock.oauth2.OAuth2Exception
@@ -21,8 +20,9 @@ import okhttp3.HttpUrl
2120

2221
private val log = KotlinLogging.logger {}
2322

24-
class AuthorizationCodeHandler(
25-
private val tokenProvider: OAuth2TokenProvider = OAuth2TokenProvider()
23+
internal class AuthorizationCodeHandler(
24+
private val tokenProvider: OAuth2TokenProvider,
25+
private val refreshTokenManager: RefreshTokenManager
2626
) : GrantHandler {
2727

2828
private val codeToAuthRequestCache: MutableMap<AuthorizationCode, AuthenticationRequest> = HashMap()
@@ -67,14 +67,15 @@ class AuthorizationCodeHandler(
6767
val scope: String? = tokenRequest.scope?.toString()
6868
val nonce: String? = authenticationRequest?.nonce?.value
6969
val loginTokenCallbackOrDefault = getLoginTokenCallbackOrDefault(code, oAuth2TokenCallback)
70-
val idToken: SignedJWT = tokenProvider.idToken(tokenRequest, issuerUrl, nonce, loginTokenCallbackOrDefault)
70+
val idToken: SignedJWT = tokenProvider.idToken(tokenRequest, issuerUrl, loginTokenCallbackOrDefault, nonce)
7171
val accessToken: SignedJWT = tokenProvider.accessToken(tokenRequest, issuerUrl, loginTokenCallbackOrDefault, nonce)
72+
val refreshToken: RefreshToken = refreshTokenManager.refreshToken(loginTokenCallbackOrDefault)
7273

7374
return OAuth2TokenResponse(
7475
tokenType = "Bearer",
7576
idToken = idToken.serialize(),
7677
accessToken = accessToken.serialize(),
77-
refreshToken = UUID.randomUUID().toString(),
78+
refreshToken = refreshToken,
7879
expiresIn = idToken.expiresIn(),
7980
scope = scope
8081
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
77
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
88
import okhttp3.HttpUrl
99

10-
class ClientCredentialsGrantHandler(
10+
internal class ClientCredentialsGrantHandler(
1111
private val tokenProvider: OAuth2TokenProvider
1212
) : GrantHandler {
1313

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
1212
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
1313
import okhttp3.HttpUrl
1414

15-
class JwtBearerGrantHandler(private val tokenProvider: OAuth2TokenProvider) : GrantHandler {
15+
internal class JwtBearerGrantHandler(private val tokenProvider: OAuth2TokenProvider) : GrantHandler {
1616

1717
override fun tokenResponse(
1818
request: OAuth2HttpRequest,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package no.nav.security.mock.oauth2.grant
2+
3+
import com.nimbusds.jwt.SignedJWT
4+
import com.nimbusds.oauth2.sdk.GrantType
5+
import com.nimbusds.oauth2.sdk.RefreshTokenGrant
6+
import com.nimbusds.oauth2.sdk.TokenRequest
7+
import mu.KotlinLogging
8+
import no.nav.security.mock.oauth2.extensions.expiresIn
9+
import no.nav.security.mock.oauth2.http.OAuth2HttpRequest
10+
import no.nav.security.mock.oauth2.http.OAuth2TokenResponse
11+
import no.nav.security.mock.oauth2.invalidGrant
12+
import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
13+
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
14+
import okhttp3.HttpUrl
15+
16+
private val log = KotlinLogging.logger {}
17+
18+
internal class RefreshTokenGrantHandler(
19+
private val tokenProvider: OAuth2TokenProvider,
20+
private val refreshTokenManager: RefreshTokenManager
21+
) : GrantHandler {
22+
23+
override fun tokenResponse(
24+
request: OAuth2HttpRequest,
25+
issuerUrl: HttpUrl,
26+
oAuth2TokenCallback: OAuth2TokenCallback
27+
): OAuth2TokenResponse {
28+
val tokenRequest = request.asNimbusTokenRequest()
29+
val refreshToken = tokenRequest.refreshTokenGrant().refreshToken.value
30+
log.debug("issuing token for refreshToken=$refreshToken")
31+
val scope: String? = tokenRequest.scope?.toString()
32+
val refreshTokenCallbackOrDefault = refreshTokenManager[refreshToken] ?: oAuth2TokenCallback
33+
val idToken: SignedJWT = tokenProvider.idToken(tokenRequest, issuerUrl, refreshTokenCallbackOrDefault)
34+
val accessToken: SignedJWT = tokenProvider.accessToken(tokenRequest, issuerUrl, refreshTokenCallbackOrDefault)
35+
36+
return OAuth2TokenResponse(
37+
tokenType = "Bearer",
38+
idToken = idToken.serialize(),
39+
accessToken = accessToken.serialize(),
40+
refreshToken = refreshToken,
41+
expiresIn = idToken.expiresIn(),
42+
scope = scope
43+
)
44+
}
45+
46+
private fun TokenRequest.refreshTokenGrant(): RefreshTokenGrant =
47+
(this.authorizationGrant as? RefreshTokenGrant) ?: invalidGrant(GrantType.REFRESH_TOKEN)
48+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package no.nav.security.mock.oauth2.grant
2+
3+
import java.util.UUID
4+
import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
5+
6+
typealias RefreshToken = String
7+
8+
internal data class RefreshTokenManager(
9+
private val cache: MutableMap<RefreshToken, OAuth2TokenCallback> = HashMap()
10+
) {
11+
operator fun get(refreshToken: RefreshToken) = cache[refreshToken]
12+
13+
fun refreshToken(tokenCallback: OAuth2TokenCallback): RefreshToken {
14+
val refreshToken = UUID.randomUUID().toString()
15+
cache[refreshToken] = tokenCallback
16+
return refreshToken
17+
}
18+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import no.nav.security.mock.oauth2.invalidRequest
66

77
val TOKEN_EXCHANGE = GrantType("urn:ietf:params:oauth:grant-type:token-exchange")
88

9+
@Suppress("MemberVisibilityCanBePrivate")
910
class TokenExchangeGrant(
1011
val subjectTokenType: String,
1112
val subjectToken: String,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
1010
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
1111
import okhttp3.HttpUrl
1212

13-
class TokenExchangeGrantHandler(private val tokenProvider: OAuth2TokenProvider) : GrantHandler {
13+
internal class TokenExchangeGrantHandler(private val tokenProvider: OAuth2TokenProvider) : GrantHandler {
1414

1515
override fun tokenResponse(
1616
request: OAuth2HttpRequest,

0 commit comments

Comments
 (0)