Skip to content

Commit 5beaf7a

Browse files
authored
feat: userinfo, pkce and improved route handling (navikt#130)
* new endpoint `/userinfo` returning claims from token in Authorization header * support for PKCE validation when included in auth and token request (fixes navikt#36) * improved http route handling API using builder pattern * support 405 method not allowed for routes (in place of always returning 404) (fixes navikt#98) * refactoring due to new route handling API * refactor `DebuggerRequestHandler` * **breaking**: removed `OAuth2HttpRouter` and `OAuth2HttpRouter.routes(vararg route: Route): OAuth2HttpRouter`: * Replaced by `fun routes(vararg route: Route): Route` and `fun routes(config: Route.Builder.() -> Unit): Route`
1 parent d081410 commit 5beaf7a

27 files changed

+949
-367
lines changed

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

+12-13
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,30 @@ import com.nimbusds.oauth2.sdk.TokenRequest
1111
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic
1212
import com.nimbusds.oauth2.sdk.auth.Secret
1313
import com.nimbusds.oauth2.sdk.id.ClientID
14-
import java.io.IOException
15-
import java.net.InetAddress
16-
import java.net.URI
17-
import java.time.Duration
18-
import java.util.UUID
19-
import java.util.concurrent.TimeUnit
2014
import mu.KotlinLogging
2115
import no.nav.security.mock.oauth2.extensions.toAuthorizationEndpointUrl
2216
import no.nav.security.mock.oauth2.extensions.toEndSessionEndpointUrl
2317
import no.nav.security.mock.oauth2.extensions.toJwksUrl
2418
import no.nav.security.mock.oauth2.extensions.toOAuth2AuthorizationServerMetadataUrl
2519
import no.nav.security.mock.oauth2.extensions.toTokenEndpointUrl
20+
import no.nav.security.mock.oauth2.extensions.toUserInfoUrl
2621
import no.nav.security.mock.oauth2.extensions.toWellKnownUrl
2722
import no.nav.security.mock.oauth2.http.MockWebServerWrapper
2823
import no.nav.security.mock.oauth2.http.OAuth2HttpRequestHandler
29-
import no.nav.security.mock.oauth2.http.OAuth2HttpRouter
30-
import no.nav.security.mock.oauth2.http.OAuth2HttpRouter.Companion.routes
24+
import no.nav.security.mock.oauth2.http.RequestHandler
3125
import no.nav.security.mock.oauth2.http.Route
32-
import no.nav.security.mock.oauth2.http.route
26+
import no.nav.security.mock.oauth2.http.routes
3327
import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback
3428
import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
3529
import okhttp3.HttpUrl
3630
import okhttp3.mockwebserver.MockResponse
3731
import okhttp3.mockwebserver.RecordedRequest
32+
import java.io.IOException
33+
import java.net.InetAddress
34+
import java.net.URI
35+
import java.time.Duration
36+
import java.util.UUID
37+
import java.util.concurrent.TimeUnit
3838

3939
private val log = KotlinLogging.logger { }
4040

@@ -47,11 +47,9 @@ open class MockOAuth2Server(
4747

4848
private val httpServer = config.httpServer
4949
private val defaultRequestHandler: OAuth2HttpRequestHandler = OAuth2HttpRequestHandler(config)
50-
private val router: OAuth2HttpRouter = routes(
50+
private val router: RequestHandler = routes(
5151
*additionalRoutes,
52-
route("") {
53-
defaultRequestHandler.handleRequest(it)
54-
}
52+
defaultRequestHandler.authorizationServer
5553
)
5654

5755
@JvmOverloads
@@ -91,6 +89,7 @@ open class MockOAuth2Server(
9189
fun issuerUrl(issuerId: String): HttpUrl = url(issuerId)
9290
fun authorizationEndpointUrl(issuerId: String): HttpUrl = url(issuerId).toAuthorizationEndpointUrl()
9391
fun endSessionEndpointUrl(issuerId: String): HttpUrl = url(issuerId).toEndSessionEndpointUrl()
92+
fun userInfoUrl(issuerId: String): HttpUrl = url(issuerId).toUserInfoUrl()
9493
fun baseUrl(): HttpUrl = url("")
9594

9695
fun issueToken(issuerId: String, clientId: String, tokenCallback: OAuth2TokenCallback): SignedJWT {

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

+12-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@ class OAuth2Exception(val errorObject: ErrorObject?, msg: String, throwable: Thr
1313
constructor(errorObject: ErrorObject?, msg: String) : this(errorObject, msg, null)
1414
}
1515

16-
fun missingParameter(name: String): Nothing = throw OAuth2Exception(OAuth2Error.INVALID_REQUEST, "missing required parameter $name")
17-
fun invalidGrant(grantType: GrantType): Nothing = throw OAuth2Exception(OAuth2Error.INVALID_GRANT, "grant_type $grantType not supported.")
18-
fun invalidRequest(message: String): Nothing = throw OAuth2Exception(OAuth2Error.INVALID_REQUEST, message)
16+
fun missingParameter(name: String): Nothing = "missing required parameter $name".let {
17+
throw OAuth2Exception(OAuth2Error.INVALID_REQUEST.setDescription(it), it)
18+
}
19+
20+
fun invalidGrant(grantType: GrantType): Nothing = "grant_type $grantType not supported.".let {
21+
throw OAuth2Exception(OAuth2Error.INVALID_GRANT, it)
22+
}
23+
24+
fun invalidRequest(message: String): Nothing = message.let {
25+
throw OAuth2Exception(OAuth2Error.INVALID_REQUEST, message)
26+
}
27+
1928
fun notFound(message: String): Nothing = throw OAuth2Exception(ErrorObject("not_found", "Resource not found", HTTPResponse.SC_NOT_FOUND), message)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package no.nav.security.mock.oauth2.debugger
2+
3+
import com.nimbusds.oauth2.sdk.OAuth2Error
4+
import no.nav.security.mock.oauth2.OAuth2Exception
5+
import okhttp3.Credentials
6+
import okhttp3.Headers
7+
import okhttp3.HttpUrl
8+
import okhttp3.MediaType.Companion.toMediaType
9+
import okhttp3.OkHttpClient
10+
import okhttp3.Request
11+
import okhttp3.RequestBody.Companion.toRequestBody
12+
import okhttp3.internal.toHostHeader
13+
import java.net.URLEncoder
14+
import java.nio.charset.StandardCharsets
15+
16+
internal class TokenRequest(
17+
val url: HttpUrl,
18+
clientAuthentication: ClientAuthentication,
19+
parameters: Map<String, String>
20+
) {
21+
val headers = when (clientAuthentication.clientAuthMethod) {
22+
ClientAuthentication.Method.CLIENT_SECRET_BASIC -> Headers.headersOf("Authorization", clientAuthentication.basic())
23+
else -> Headers.headersOf()
24+
}
25+
26+
val body: String = if (clientAuthentication.clientAuthMethod == ClientAuthentication.Method.CLIENT_SECRET_POST) {
27+
parameters.toKeyValueString("&").plus("&${clientAuthentication.form()}")
28+
} else {
29+
parameters.toKeyValueString("&")
30+
}
31+
32+
override fun toString(): String = "POST ${url.encodedPath} HTTP/1.1\n" +
33+
"Host: ${url.toHostHeader(true)}\n" +
34+
"Content-Type: application/x-www-form-urlencoded\n" +
35+
headers.joinToString("\n") {
36+
"${it.first}: ${it.second}"
37+
} +
38+
"\n\n$body"
39+
40+
private fun Map<String, String>.toKeyValueString(entrySeparator: String): String =
41+
this.map { "${it.key}=${it.value}" }
42+
.toList().joinToString(entrySeparator)
43+
}
44+
45+
internal data class ClientAuthentication(
46+
val clientId: String,
47+
val clientSecret: String,
48+
val clientAuthMethod: Method
49+
) {
50+
fun form(): String = "client_id=${clientId.urlEncode()}&client_secret=${clientSecret.urlEncode()}"
51+
fun basic(): String = Credentials.basic(clientId, clientSecret, StandardCharsets.UTF_8)
52+
53+
companion object {
54+
fun fromMap(map: Map<String, String>): ClientAuthentication =
55+
ClientAuthentication(
56+
map.require("client_id"),
57+
map.require("client_secret"),
58+
Method.valueOf(map.require("client_auth_method"))
59+
)
60+
61+
private fun Map<String, String>.require(key: String): String =
62+
this[key] ?: throw OAuth2Exception(OAuth2Error.INVALID_REQUEST, "missing required parameter $key")
63+
}
64+
65+
enum class Method {
66+
CLIENT_SECRET_POST,
67+
CLIENT_SECRET_BASIC
68+
}
69+
}
70+
71+
internal fun String.urlEncode(): String = URLEncoder.encode(this, StandardCharsets.UTF_8)
72+
73+
internal fun OkHttpClient.post(tokenRequest: TokenRequest): String =
74+
this.newCall(
75+
Request.Builder()
76+
.headers(tokenRequest.headers)
77+
.url(tokenRequest.url)
78+
.post(tokenRequest.body.toRequestBody("application/x-www-form-urlencoded".toMediaType()))
79+
.build()
80+
).execute().body?.string() ?: throw RuntimeException("could not get responsebody from url=${tokenRequest.url}")

0 commit comments

Comments
 (0)