Skip to content

Commit e69cbbe

Browse files
oheger-boschsschuberth
authored andcommitted
feat(utils): Improve authentication of default HTTP client
The standard `JAVA_NET_AUTHENTICATOR` provided by OkHttp that was used so far was only querying the global Java Authenticator when the response contained a challenge with scheme `Basic`. There are, however, servers that do not send this challenge, but for which authentication with the credentials from the Java Authenticator would work. The GitHub package registry is an example of such a server. To support those servers, add a new `Authenticator` implementation that always queries credentials from the Java `Authenticator`. Signed-off-by: Oliver Heger <[email protected]>
1 parent 9908aa9 commit e69cbbe

File tree

2 files changed

+141
-1
lines changed

2 files changed

+141
-1
lines changed

utils/ort/src/main/kotlin/OkHttpClientHelper.kt

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package org.ossreviewtoolkit.utils.ort
2222
import java.io.File
2323
import java.io.IOException
2424
import java.lang.invoke.MethodHandles
25+
import java.net.HttpURLConnection
2526
import java.net.URI
2627
import java.time.Duration
2728
import java.util.concurrent.ConcurrentHashMap
@@ -41,6 +42,7 @@ import okhttp3.OkHttpClient
4142
import okhttp3.Request
4243
import okhttp3.Response
4344
import okhttp3.ResponseBody
45+
import okhttp3.Route
4446

4547
import okio.buffer
4648
import okio.sink
@@ -88,6 +90,7 @@ private val logger = loggerOf(MethodHandles.lookup().lookupClass())
8890
private const val CACHE_DIRECTORY = "cache/http"
8991
private val MAX_CACHE_SIZE_IN_BYTES = 1.gibibytes
9092
private const val READ_TIMEOUT_IN_SECONDS = 30L
93+
private const val AUTHORIZATION_HEADER = "Authorization"
9194

9295
/**
9396
* The default [OkHttpClient] for ORT to use.
@@ -124,7 +127,7 @@ val okHttpClient: OkHttpClient by lazy {
124127
.cache(cache)
125128
.connectionSpecs(specs)
126129
.readTimeout(Duration.ofSeconds(READ_TIMEOUT_IN_SECONDS))
127-
.authenticator(Authenticator.JAVA_NET_AUTHENTICATOR)
130+
.authenticator(JavaNetAuthenticatorWrapper())
128131
.proxyAuthenticator(Authenticator.JAVA_NET_AUTHENTICATOR)
129132
.build()
130133
}
@@ -276,3 +279,42 @@ suspend fun Call.await(): Response =
276279
}
277280
})
278281
}
282+
283+
/**
284+
* A wrapper implementation around OkHttp's [Authenticator.JAVA_NET_AUTHENTICATOR] that is less strict about querying
285+
* credentials for requests from the global Java authenticator.
286+
*
287+
* OkHttp already has a built-in [Authenticator] that uses the global Java authenticator; however, this implementation
288+
* is rather picky about the requests for which it delegates to the Java authenticator. It only kicks in for responses
289+
* containing a challenge with the "Basic" scheme. This excludes a number of servers that could be handled by the Java
290+
* authenticator. For instance, the GitHub package registry does not send any challenges, but it can be accessed with a
291+
* token provided by the Java authenticator.
292+
*
293+
* This implementation handles default authentication request by always querying the Java authenticator for credentials,
294+
* no matter which challenges are sent by the server. Only in special cases, such as unexpected response codes, it
295+
* delegates to the default OkHttp authenticator.
296+
*/
297+
internal class JavaNetAuthenticatorWrapper(
298+
private val wrappedAuthenticator: Authenticator = Authenticator.JAVA_NET_AUTHENTICATOR
299+
) : Authenticator {
300+
override fun authenticate(route: Route?, response: Response): Request? {
301+
if (response.code != HttpURLConnection.HTTP_UNAUTHORIZED) {
302+
return wrappedAuthenticator.authenticate(route, response)
303+
}
304+
305+
// The request already had an Authorization header; so obviously the credentials are not valid.
306+
if (response.request.header(AUTHORIZATION_HEADER) != null) return null
307+
308+
val requestUri = response.request.url.toUri()
309+
return requestPasswordAuthentication(requestUri)?.let { authentication ->
310+
response.request.newBuilder()
311+
.header(
312+
AUTHORIZATION_HEADER,
313+
Credentials.basic(
314+
authentication.userName,
315+
String(authentication.password)
316+
)
317+
).build()
318+
}
319+
}
320+
}

utils/ort/src/test/kotlin/OkHttpClientHelperTest.kt

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ package org.ossreviewtoolkit.utils.ort
2121

2222
import io.kotest.core.spec.style.WordSpec
2323
import io.kotest.engine.spec.tempdir
24+
import io.kotest.matchers.nulls.beNull
25+
import io.kotest.matchers.nulls.shouldNotBeNull
2426
import io.kotest.matchers.result.shouldBeFailure
2527
import io.kotest.matchers.should
2628
import io.kotest.matchers.shouldBe
@@ -34,13 +36,21 @@ import io.mockk.mockkStatic
3436
import io.mockk.unmockkAll
3537

3638
import java.io.IOException
39+
import java.net.HttpURLConnection
40+
import java.net.PasswordAuthentication
41+
import java.net.URI
3742
import java.time.Duration
3843

44+
import kotlin.reflect.KFunction
45+
46+
import okhttp3.Authenticator as OkAuthenticator
3947
import okhttp3.HttpUrl.Companion.toHttpUrl
4048
import okhttp3.OkHttpClient
49+
import okhttp3.Protocol
4150
import okhttp3.Request
4251
import okhttp3.Response
4352
import okhttp3.ResponseBody
53+
import okhttp3.Route
4454

4555
import okio.BufferedSource
4656

@@ -135,4 +145,92 @@ class OkHttpClientHelperTest : WordSpec({
135145
}
136146
}
137147
}
148+
149+
"JavaNetAuthenticatorWrapper" should {
150+
"delegate to the default authenticator if the response code is not 401" {
151+
val authenticator = mockk<OkAuthenticator>()
152+
val route = mockk<Route>()
153+
val response = createResponse(HttpURLConnection.HTTP_FORBIDDEN)
154+
val modifiedRequest = mockk<Request>()
155+
every { authenticator.authenticate(route, response) } returns modifiedRequest
156+
157+
val lenientAuthenticator = JavaNetAuthenticatorWrapper(authenticator)
158+
159+
lenientAuthenticator.authenticate(route, response) shouldBe modifiedRequest
160+
}
161+
162+
"query the Java authenticator" {
163+
val response = createResponse()
164+
preparePasswordAuthentication(success = true)
165+
166+
val lenientAuthenticator = JavaNetAuthenticatorWrapper(mockk())
167+
168+
lenientAuthenticator.authenticate(mockk(), response) shouldNotBeNull {
169+
header("Authorization") shouldBe "Basic c2NvdHQ6dGlnZXI="
170+
url shouldBe AUTH_URL.toHttpUrl()
171+
}
172+
}
173+
174+
"return null if the Java authenticator does not return a password authentication" {
175+
val response = createResponse()
176+
preparePasswordAuthentication(success = false)
177+
178+
val lenientAuthenticator = JavaNetAuthenticatorWrapper(mockk())
179+
180+
lenientAuthenticator.authenticate(mockk(), response) should beNull()
181+
}
182+
183+
"return null if the request already has an Authorization header" {
184+
val requestWithAuth = Request.Builder()
185+
.url(AUTH_URL)
186+
.header("Authorization", "Basic wrong-credentials")
187+
.build()
188+
val response = createResponse(originalRequest = requestWithAuth)
189+
preparePasswordAuthentication(success = true)
190+
191+
val lenientAuthenticator = JavaNetAuthenticatorWrapper(mockk())
192+
193+
lenientAuthenticator.authenticate(mockk(), response) should beNull()
194+
}
195+
}
138196
})
197+
198+
private const val USERNAME = "scott"
199+
private const val PASSWORD = "tiger"
200+
private const val AUTH_URL = "https://example.org/auth"
201+
202+
/**
203+
* Create a [Response] object with the given response [code]. The request of this response is set to [originalRequest]
204+
* if it is provided; otherwise, a default request to [AUTH_URL] is created.
205+
*/
206+
private fun createResponse(
207+
code: Int = HttpURLConnection.HTTP_UNAUTHORIZED,
208+
originalRequest: Request? = null
209+
): Response {
210+
val request = originalRequest ?: Request.Builder()
211+
.url(AUTH_URL)
212+
.build()
213+
return Response.Builder()
214+
.request(request)
215+
.code(code)
216+
.protocol(Protocol.HTTP_2)
217+
.message("Unauthorized")
218+
.build()
219+
}
220+
221+
/**
222+
* Mock the [requestPasswordAuthentication] function to return a [PasswordAuthentication] object for the test URI
223+
* based on the given [success] flag.
224+
*/
225+
private fun preparePasswordAuthentication(success: Boolean) {
226+
val result = if (success) {
227+
PasswordAuthentication(USERNAME, PASSWORD.toCharArray())
228+
} else {
229+
null
230+
}
231+
232+
val resolveFunc: (URI) -> PasswordAuthentication? = ::requestPasswordAuthentication
233+
mockkStatic(resolveFunc as KFunction<*>)
234+
235+
every { requestPasswordAuthentication(URI.create(AUTH_URL)) } returns result
236+
}

0 commit comments

Comments
 (0)