Просмотр исходного кода

Honoring error information on OAuth Exchange Code (#37224)

Juan Barahona 4 лет назад
Родитель
Сommit
a3565c498e

+ 16 - 19
src/Security/Authentication/OAuth/src/OAuthHandler.cs

@@ -1,8 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.Net.Http;
 using System.Net.Http.Headers;
@@ -11,12 +9,10 @@ using System.Security.Cryptography;
 using System.Text;
 using System.Text.Encodings.Web;
 using System.Text.Json;
-using System.Threading.Tasks;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.Authentication.OAuth
 {
@@ -215,25 +211,26 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
             requestMessage.Content = requestContent;
             requestMessage.Version = Backchannel.DefaultRequestVersion;
             var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);
-            if (response.IsSuccessStatusCode)
-            {
-                var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
-                return OAuthTokenResponse.Success(payload);
-            }
-            else
+            var body = await response.Content.ReadAsStringAsync();
+
+            return response.IsSuccessStatusCode switch
             {
-                var error = "OAuth token endpoint failure: " + await Display(response);
-                return OAuthTokenResponse.Failed(new Exception(error));
-            }
+                true => OAuthTokenResponse.Success(JsonDocument.Parse(body)),
+                false => PrepareFailedOAuthTokenReponse(response, body)
+            };  
         }
 
-        private static async Task<string> Display(HttpResponseMessage response)
+        private static OAuthTokenResponse PrepareFailedOAuthTokenReponse(HttpResponseMessage response, string body)
         {
-            var output = new StringBuilder();
-            output.Append("Status: " + response.StatusCode + ";");
-            output.Append("Headers: " + response.Headers.ToString() + ";");
-            output.Append("Body: " + await response.Content.ReadAsStringAsync() + ";");
-            return output.ToString();
+            var exception = OAuthTokenResponse.GetStandardErrorException(JsonDocument.Parse(body));
+
+            if (exception is null)
+            {
+                var errorMessage = $"OAuth token endpoint failure: Status: {response.StatusCode};Headers: {response.Headers};Body: {body};";
+                return OAuthTokenResponse.Failed(new Exception(errorMessage));
+            }
+
+            return OAuthTokenResponse.Failed(exception);
         }
 
         /// <summary>

+ 35 - 0
src/Security/Authentication/OAuth/src/OAuthTokenResponse.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Text;
 using System.Text.Json;
 
 namespace Microsoft.AspNetCore.Authentication.OAuth
@@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
             TokenType = root.GetString("token_type");
             RefreshToken = root.GetString("refresh_token");
             ExpiresIn = root.GetString("expires_in");
+            Error = GetStandardErrorException(response);
         }
 
         private OAuthTokenResponse(Exception error)
@@ -88,5 +90,38 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
         /// The exception in the event the response was a failure.
         /// </summary>
         public Exception? Error { get; set; }
+
+        internal static Exception? GetStandardErrorException(JsonDocument response)
+        {
+            var root = response.RootElement;
+            var error = root.GetString("error");
+
+            if (error is null)
+            {
+                return null;
+            }
+
+            var result = new StringBuilder("OAuth token endpoint failure: ");
+            result.Append(error);
+
+            if (root.TryGetProperty("error_description", out var errorDescription))
+            {
+                result.Append(";Description=");
+                result.Append(errorDescription);
+            }
+
+            if (root.TryGetProperty("error_uri", out var errorUri))
+            {
+                result.Append(";Uri=");
+                result.Append(errorUri);
+            }
+
+            var exception = new Exception(result.ToString());
+            exception.Data["error"] = error.ToString();
+            exception.Data["error_description"] = errorDescription.ToString();
+            exception.Data["error_uri"] = errorUri.ToString();
+
+            return exception;
+        }
     }
 }

+ 98 - 4
src/Security/Authentication/test/OAuthTests.cs

@@ -8,11 +8,10 @@ using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.TestHost;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
-using System;
-using System.Collections.Generic;
 using System.Net;
-using System.Threading.Tasks;
-using Xunit;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
 
 namespace Microsoft.AspNetCore.Authentication.OAuth
 {
@@ -396,6 +395,93 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
             Assert.Null(transaction.Response.Headers.Location);
         }
 
+        [Theory]
+        [InlineData(HttpStatusCode.OK)]
+        [InlineData(HttpStatusCode.BadRequest)]
+        public async Task ExchangeCodeAsync_ChecksForErrorInformation(HttpStatusCode httpStatusCode)
+        {
+            using var host = await CreateHost(
+                s => s.AddAuthentication().AddOAuth(
+                    "Weblie",
+                    opt =>
+                    {
+                        ConfigureDefaults(opt);
+                        opt.StateDataFormat = new TestStateDataFormat();
+                        opt.BackchannelHttpHandler = new TestHttpMessageHandler
+                        {
+                            Sender = req =>
+                            {
+                                if (req.RequestUri.AbsoluteUri == "https://example.com/provider/token")
+                                {
+                                    return ReturnJsonResponse(new
+                                    {
+                                        error = "incorrect_client_credentials",
+                                        error_description = "The client_id and/or client_secret passed are incorrect.",
+                                        error_uri = "https://example.com/troubleshooting-oauth-app-access-token-request-errors/#incorrect-client-credentials",
+                                    }, httpStatusCode);
+                                }
+
+                                return null;
+                            }
+                        };
+                        opt.Events = new OAuthEvents()
+                        {
+                            OnRemoteFailure = context =>
+                            {
+                                Assert.Equal("incorrect_client_credentials", context.Failure.Data["error"]);
+                                Assert.Equal("The client_id and/or client_secret passed are incorrect.", context.Failure.Data["error_description"]);
+                                Assert.Equal("https://example.com/troubleshooting-oauth-app-access-token-request-errors/#incorrect-client-credentials", context.Failure.Data["error_uri"]);
+                                return Task.CompletedTask;
+                            }
+                        };
+                    }));
+
+            using var server = host.GetTestServer();
+            var exception = await Assert.ThrowsAsync<Exception>(
+                () => server.SendAsync("https://www.example.com/oauth-callback?code=random_code&state=protected_state", ".AspNetCore.Correlation.correlationId=N"));
+        }
+
+        [Fact]
+        public async Task ExchangeCodeAsync_FallbackToBasicErrorReporting_WhenErrorInformationIsNotPresent()
+        {
+            using var host = await CreateHost(
+                s => s.AddAuthentication().AddOAuth(
+                    "Weblie",
+                    opt =>
+                    {
+                        ConfigureDefaults(opt);
+                        opt.StateDataFormat = new TestStateDataFormat();
+                        opt.BackchannelHttpHandler = new TestHttpMessageHandler
+                        {
+                            Sender = req =>
+                            {
+                                if (req.RequestUri.AbsoluteUri == "https://example.com/provider/token")
+                                {
+                                    return ReturnJsonResponse(new
+                                    {
+                                        ErrorCode = "ThisIsCustomErrorCode",
+                                        ErrorDescription = "ThisIsCustomErrorDescription"
+                                    }, HttpStatusCode.BadRequest);
+                                }
+
+                                return null;
+                            }
+                        };
+                        opt.Events = new OAuthEvents()
+                        {
+                            OnRemoteFailure = context =>
+                            {
+                                Assert.StartsWith("OAuth token endpoint failure:", context.Failure.Message);
+                                return Task.CompletedTask;
+                            }
+                        };
+                    }));
+
+            using var server = host.GetTestServer();
+            var exception = await Assert.ThrowsAsync<Exception>(
+                () => server.SendAsync("https://www.example.com/oauth-callback?code=random_code&state=protected_state", ".AspNetCore.Correlation.correlationId=N"));
+        }
+
         private static async Task<IHost> CreateHost(Action<IServiceCollection> configureServices, Func<HttpContext, Task<bool>> handler = null)
         {
             var host = new HostBuilder()
@@ -418,6 +504,14 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
             return host;
         }
 
+        private static HttpResponseMessage ReturnJsonResponse(object content, HttpStatusCode code = HttpStatusCode.OK)
+        {
+            var res = new HttpResponseMessage(code);
+            var text = JsonSerializer.Serialize(content);
+            res.Content = new StringContent(text, Encoding.UTF8, "application/json");
+            return res;
+        }
+
         private class TestStateDataFormat : ISecureDataFormat<AuthenticationProperties>
         {
             private AuthenticationProperties Data { get; set; }