From 55538dee4b9bb5726a7f027da75fb3ff0b7cb948 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Wed, 11 Sep 2024 16:50:40 +0200 Subject: [PATCH] AuthHandshakeMessageHandler: also try Basic auth when username is ''. OpenShift's image registry expects the '' username to be handled using the Basic auth scheme. --- .../AuthHandshakeMessageHandler.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/Containers/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs b/src/Containers/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs index 8e4bc29b7656..33356dccb264 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs @@ -293,8 +293,7 @@ internal static (string credU, string credP)? GetDockerCredentialsFromEnvironmen if (!postResponse.IsSuccessStatusCode) { await postResponse.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); - //return null to try HTTP GET instead - return null; + return null; // try next method. } _logger.LogTrace("Received '{statuscode}'.", postResponse.StatusCode); TokenResponse? tokenResponse = JsonSerializer.Deserialize(postResponse.Content.ReadAsStream(cancellationToken)); @@ -316,14 +315,34 @@ internal static (string credU, string credP)? GetDockerCredentialsFromEnvironmen /// private async Task<(AuthenticationHeaderValue, DateTimeOffset)?> TryTokenGetAsync(DockerCredentials privateRepoCreds, AuthInfo bearerAuthInfo, CancellationToken cancellationToken) { + AuthenticationHeaderValue authHeader; + (AuthenticationHeaderValue, DateTimeOffset)? authenticationValueAndDuration; + + // For the username '', some registries expect Bearer auth while others expect Basic auth. + // We start by trying Bearer, and fall back to Basic. + if (privateRepoCreds.Username == "") + { + authHeader = new AuthenticationHeaderValue(BearerAuthScheme, privateRepoCreds.Password); + + authenticationValueAndDuration = await TryTokenGetAsync(authHeader, bearerAuthInfo, cancellationToken).ConfigureAwait(false); + if (authenticationValueAndDuration is not null) + { + return authenticationValueAndDuration; + } + } + // this doesn't seem to be called out in the spec, but actual username/password auth information should be converted into Basic auth here, // even though the overall Scheme we're authenticating for is Bearer - var header = privateRepoCreds.Username == "" - ? new AuthenticationHeaderValue(BearerAuthScheme, privateRepoCreds.Password) - : new AuthenticationHeaderValue(BasicAuthScheme, Convert.ToBase64String(Encoding.ASCII.GetBytes($"{privateRepoCreds.Username}:{privateRepoCreds.Password}"))); + authHeader = new AuthenticationHeaderValue(BasicAuthScheme, Convert.ToBase64String(Encoding.ASCII.GetBytes($"{privateRepoCreds.Username}:{privateRepoCreds.Password}"))); + authenticationValueAndDuration = await TryTokenGetAsync(authHeader, bearerAuthInfo, cancellationToken).ConfigureAwait(false); + return authenticationValueAndDuration; + } + + private async Task<(AuthenticationHeaderValue, DateTimeOffset)?> TryTokenGetAsync(AuthenticationHeaderValue authHeader, AuthInfo bearerAuthInfo, CancellationToken cancellationToken) + { var builder = new UriBuilder(new Uri(bearerAuthInfo.Realm)); - _logger.LogTrace("Attempting to authenticate on {uri} using GET.", bearerAuthInfo.Realm); + _logger.LogTrace("Attempting to authenticate on {uri} using GET with {scheme} auth.", bearerAuthInfo.Realm, authHeader.Scheme); var queryDict = System.Web.HttpUtility.ParseQueryString(""); if (bearerAuthInfo.Service is string svc) { @@ -335,12 +354,13 @@ internal static (string credU, string credP)? GetDockerCredentialsFromEnvironmen } builder.Query = queryDict.ToString(); var message = new HttpRequestMessage(HttpMethod.Get, builder.ToString()); - message.Headers.Authorization = header; + message.Headers.Authorization = authHeader; using var tokenResponse = await base.SendAsync(message, cancellationToken).ConfigureAwait(false); if (!tokenResponse.IsSuccessStatusCode) { - throw new UnableToAccessRepositoryException(_registryName); + await tokenResponse.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); + return null; // try next method. } TokenResponse? token = JsonSerializer.Deserialize(tokenResponse.Content.ReadAsStream(cancellationToken)); @@ -412,7 +432,8 @@ protected override async Task SendAsync(HttpRequestMessage request.Headers.Authorization = authHeader; return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } - return response; + + throw new UnableToAccessRepositoryException(_registryName); } else {