Add error details for OAuth and OIDC remote errors #4682 (#11002)

This commit is contained in:
Chris Ross 2019-06-12 11:32:40 -07:00 committed by GitHub
parent 2ea746f37d
commit 4fa5a228cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 9 deletions

View File

@ -71,27 +71,40 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
// Since it's a frequent scenario (that is not caused by incorrect configuration),
// denied errors are handled differently using HandleAccessDeniedErrorAsync().
// Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
var errorDescription = query["error_description"];
var errorUri = query["error_uri"];
if (StringValues.Equals(error, "access_denied"))
{
var result = await HandleAccessDeniedErrorAsync(properties);
return !result.None ? result
: HandleRequestResult.Fail("Access was denied by the resource owner or by the remote server.", properties);
if (!result.None)
{
return result;
}
var deniedEx = new Exception("Access was denied by the resource owner or by the remote server.");
deniedEx.Data["error"] = error.ToString();
deniedEx.Data["error_description"] = errorDescription.ToString();
deniedEx.Data["error_uri"] = errorUri.ToString();
return HandleRequestResult.Fail(deniedEx, properties);
}
var failureMessage = new StringBuilder();
failureMessage.Append(error);
var errorDescription = query["error_description"];
if (!StringValues.IsNullOrEmpty(errorDescription))
{
failureMessage.Append(";Description=").Append(errorDescription);
}
var errorUri = query["error_uri"];
if (!StringValues.IsNullOrEmpty(errorUri))
{
failureMessage.Append(";Uri=").Append(errorUri);
}
return HandleRequestResult.Fail(failureMessage.ToString(), properties);
var ex = new Exception(failureMessage.ToString());
ex.Data["error"] = error.ToString();
ex.Data["error_description"] = errorDescription.ToString();
ex.Data["error_uri"] = errorUri.ToString();
return HandleRequestResult.Fail(ex, properties);
}
var code = query["code"];

View File

@ -1309,12 +1309,16 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
Logger.ResponseError(message.Error, description, errorUri);
}
return new OpenIdConnectProtocolException(string.Format(
var ex = new OpenIdConnectProtocolException(string.Format(
CultureInfo.InvariantCulture,
Resources.MessageContainsError,
message.Error,
description,
errorUri));
ex.Data["error"] = message.Error;
ex.Data["error_description"] = description;
ex.Data["error_uri"] = errorUri;
return ex;
}
}
}

View File

@ -420,6 +420,50 @@ namespace Microsoft.AspNetCore.Authentication.Google
Assert.Equal("/custom-denied-page?rurl=http%3A%2F%2Fwww.google.com%2F", transaction.Response.Headers.GetValues("Location").First());
}
[Fact]
public async Task ReplyPathWithAccessDeniedErrorAndNoAccessDeniedPath_FallsBackToRemoteError()
{
var accessDeniedCalled = false;
var remoteFailureCalled = false;
var server = CreateServer(o =>
{
o.ClientId = "Test Id";
o.ClientSecret = "Test Secret";
o.StateDataFormat = new TestStateDataFormat();
o.Events = new OAuthEvents()
{
OnAccessDenied = ctx =>
{
Assert.Null(ctx.AccessDeniedPath.Value);
Assert.Equal("http://testhost/redirect", ctx.ReturnUrl);
Assert.Equal("ReturnUrl", ctx.ReturnUrlParameter);
accessDeniedCalled = true;
return Task.FromResult(0);
},
OnRemoteFailure = ctx =>
{
var ex = ctx.Failure;
Assert.True(ex.Data.Contains("error"), "error");
Assert.True(ex.Data.Contains("error_description"), "error_description");
Assert.True(ex.Data.Contains("error_uri"), "error_uri");
Assert.Equal("access_denied", ex.Data["error"]);
Assert.Equal("whyitfailed", ex.Data["error_description"]);
Assert.Equal("https://example.com/fail", ex.Data["error_uri"]);
remoteFailureCalled = true;
ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message));
ctx.HandleResponse();
return Task.FromResult(0);
}
};
});
var transaction = await server.SendAsync("https://example.com/signin-google?error=access_denied&error_description=whyitfailed&error_uri=https://example.com/fail&state=protected_state",
".AspNetCore.Correlation.Google.correlationId=N");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.StartsWith("/error?FailureMessage=", transaction.Response.Headers.GetValues("Location").First());
Assert.True(accessDeniedCalled);
Assert.True(remoteFailureCalled);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
@ -434,24 +478,31 @@ namespace Microsoft.AspNetCore.Authentication.Google
{
OnRemoteFailure = ctx =>
{
var ex = ctx.Failure;
Assert.True(ex.Data.Contains("error"), "error");
Assert.True(ex.Data.Contains("error_description"), "error_description");
Assert.True(ex.Data.Contains("error_uri"), "error_uri");
Assert.Equal("itfailed", ex.Data["error"]);
Assert.Equal("whyitfailed", ex.Data["error_description"]);
Assert.Equal("https://example.com/fail", ex.Data["error_uri"]);
ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message));
ctx.HandleResponse();
return Task.FromResult(0);
}
} : new OAuthEvents();
});
var sendTask = server.SendAsync("https://example.com/signin-google?error=OMG&error_description=SoBad&error_uri=foobar&state=protected_state",
var sendTask = server.SendAsync("https://example.com/signin-google?error=itfailed&error_description=whyitfailed&error_uri=https://example.com/fail&state=protected_state",
".AspNetCore.Correlation.Google.correlationId=N");
if (redirect)
{
var transaction = await sendTask;
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.Equal("/error?FailureMessage=OMG" + UrlEncoder.Default.Encode(";Description=SoBad;Uri=foobar"), transaction.Response.Headers.GetValues("Location").First());
Assert.Equal("/error?FailureMessage=itfailed" + UrlEncoder.Default.Encode(";Description=whyitfailed;Uri=https://example.com/fail"), transaction.Response.Headers.GetValues("Location").First());
}
else
{
var error = await Assert.ThrowsAnyAsync<Exception>(() => sendTask);
Assert.Equal("OMG;Description=SoBad;Uri=foobar", error.GetBaseException().Message);
Assert.Equal("itfailed;Description=whyitfailed;Uri=https://example.com/fail", error.GetBaseException().Message);
}
}

View File

@ -1,9 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Xunit;
@ -63,5 +68,73 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
// Assert
Assert.Equal("Hi from the callback path", transaction.ResponseText);
}
[Fact]
public async Task ErrorResponseWithDetails()
{
var settings = new TestSettings(
opt =>
{
opt.StateDataFormat = new TestStateDataFormat();
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.ClientId = "Test Id";
opt.Events = new OpenIdConnectEvents()
{
OnRemoteFailure = ctx =>
{
var ex = ctx.Failure;
Assert.True(ex.Data.Contains("error"), "error");
Assert.True(ex.Data.Contains("error_description"), "error_description");
Assert.True(ex.Data.Contains("error_uri"), "error_uri");
Assert.Equal("itfailed", ex.Data["error"]);
Assert.Equal("whyitfailed", ex.Data["error_description"]);
Assert.Equal("https://example.com/fail", ex.Data["error_uri"]);
ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message));
ctx.HandleResponse();
return Task.FromResult(0);
}
};
});
var server = settings.CreateTestServer();
var transaction = await server.SendAsync(
"https://example.com/signin-oidc?error=itfailed&error_description=whyitfailed&error_uri=https://example.com/fail&state=protected_state",
".AspNetCore.Correlation.OpenIdConnect.correlationId=N");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.StartsWith("/error?FailureMessage=", transaction.Response.Headers.GetValues("Location").First());
}
private class TestStateDataFormat : ISecureDataFormat<AuthenticationProperties>
{
private AuthenticationProperties Data { get; set; }
public string Protect(AuthenticationProperties data)
{
return "protected_state";
}
public string Protect(AuthenticationProperties data, string purpose)
{
throw new NotImplementedException();
}
public AuthenticationProperties Unprotect(string protectedText)
{
Assert.Equal("protected_state", protectedText);
var properties = new AuthenticationProperties(new Dictionary<string, string>()
{
{ ".xsrf", "correlationId" },
{ "testkey", "testvalue" }
});
properties.RedirectUri = "http://testhost/redirect";
return properties;
}
public AuthenticationProperties Unprotect(string protectedText, string purpose)
{
throw new NotImplementedException();
}
}
}
}