Add AccessDeniedPath support to the OIDC/OAuth2/Twitter providers (#1887)
* Add AccessDeniedPath support to the OIDC/OAuth2/Twitter providers * Update the code documentation and remove an unnecessary call to SignOutAsync() * Introduce a new AccessDenied event and move most of the access denied handling logic to RemoteAuthenticationHandler * Add ReturnUrlParameter support to RemoteAuthenticationHandler * Remove AccessDeniedException and introduce RemoteAuthenticationHandler.HandleAccessDeniedErrorAsync() * Use OriginalPath instead of Request.Path * Update obsolete code comments * Add unit tests for the new AccessDenied event * Allow customizing the access denied path/return URL/return URL parameter from the AccessDenied event
This commit is contained in:
parent
1c4a6955b7
commit
7e14b052ea
|
|
@ -63,6 +63,7 @@ namespace OpenIdConnectSample
|
|||
o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
|
||||
o.SaveTokens = true;
|
||||
o.GetClaimsFromUserInfoEndpoint = true;
|
||||
o.AccessDeniedPath = "/access-denied-from-remote";
|
||||
|
||||
o.ClaimActions.MapAllExcept("aud", "iss", "iat", "nbf", "exp", "aio", "c_hash", "uti", "nonce");
|
||||
|
||||
|
|
@ -126,6 +127,16 @@ namespace OpenIdConnectSample
|
|||
return;
|
||||
}
|
||||
|
||||
if (context.Request.Path.Equals("/access-denied-from-remote"))
|
||||
{
|
||||
await WriteHtmlAsync(response, async res =>
|
||||
{
|
||||
await res.WriteAsync($"<h1>Access Denied error received from the remote authorization server</h1>");
|
||||
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.Request.Path.Equals("/Account/AccessDenied"))
|
||||
{
|
||||
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
|
|||
var returnUrl = properties.RedirectUri;
|
||||
if (string.IsNullOrEmpty(returnUrl))
|
||||
{
|
||||
returnUrl = OriginalPathBase + Request.Path + Request.QueryString;
|
||||
returnUrl = OriginalPathBase + OriginalPath + Request.QueryString;
|
||||
}
|
||||
var accessDeniedUri = Options.AccessDeniedPath + QueryString.Create(Options.ReturnUrlParameter, returnUrl);
|
||||
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri));
|
||||
|
|
@ -434,7 +434,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
|
|||
var redirectUri = properties.RedirectUri;
|
||||
if (string.IsNullOrEmpty(redirectUri))
|
||||
{
|
||||
redirectUri = OriginalPathBase + Request.Path + Request.QueryString;
|
||||
redirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
|
||||
}
|
||||
|
||||
var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri);
|
||||
|
|
|
|||
|
|
@ -63,6 +63,16 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
var error = query["error"];
|
||||
if (!StringValues.IsNullOrEmpty(error))
|
||||
{
|
||||
// Note: access_denied errors are special protocol errors indicating the user didn't
|
||||
// approve the authorization demand requested by the remote authorization server.
|
||||
// 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.
|
||||
if (StringValues.Equals(error, "access_denied"))
|
||||
{
|
||||
return await HandleAccessDeniedErrorAsync(properties);
|
||||
}
|
||||
|
||||
var failureMessage = new StringBuilder();
|
||||
failureMessage.Append(error);
|
||||
var errorDescription = query["error_description"];
|
||||
|
|
@ -194,7 +204,7 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
{
|
||||
if (string.IsNullOrEmpty(properties.RedirectUri))
|
||||
{
|
||||
properties.RedirectUri = CurrentUri;
|
||||
properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
|
||||
}
|
||||
|
||||
// OAuth2 10.12 CSRF
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
|
||||
using Microsoft.AspNetCore.Http.Authentication;
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Authentication.OAuth
|
||||
{
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
properties.RedirectUri = BuildRedirectUriIfRelative(Options.SignedOutRedirectUri);
|
||||
if (string.IsNullOrWhiteSpace(properties.RedirectUri))
|
||||
{
|
||||
properties.RedirectUri = CurrentUri;
|
||||
properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
|
||||
}
|
||||
}
|
||||
Logger.PostSignOutRedirect(properties.RedirectUri);
|
||||
|
|
@ -312,7 +312,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
// 2. CurrentUri if RedirectUri is not set)
|
||||
if (string.IsNullOrEmpty(properties.RedirectUri))
|
||||
{
|
||||
properties.RedirectUri = CurrentUri;
|
||||
properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
|
||||
}
|
||||
Logger.PostAuthenticationLocalRedirect(properties.RedirectUri);
|
||||
|
||||
|
|
@ -520,6 +520,16 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
// if any of the error fields are set, throw error null
|
||||
if (!string.IsNullOrEmpty(authorizationResponse.Error))
|
||||
{
|
||||
// Note: access_denied errors are special protocol errors indicating the user didn't
|
||||
// approve the authorization demand requested by the remote authorization server.
|
||||
// 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.
|
||||
if (string.Equals(authorizationResponse.Error, "access_denied", StringComparison.Ordinal))
|
||||
{
|
||||
return await HandleAccessDeniedErrorAsync(properties);
|
||||
}
|
||||
|
||||
return HandleRequestResult.Fail(CreateOpenIdConnectProtocolException(authorizationResponse, response: null), properties);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,12 +55,14 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
|
|||
|
||||
var properties = requestToken.Properties;
|
||||
|
||||
// REVIEW: see which of these are really errors
|
||||
|
||||
var denied = query["denied"];
|
||||
if (!StringValues.IsNullOrEmpty(denied))
|
||||
{
|
||||
return HandleRequestResult.Fail("The user denied permissions.", properties);
|
||||
// Note: denied errors are special protocol errors indicating the user didn't
|
||||
// approve the authorization demand requested by the remote authorization server.
|
||||
// Since it's a frequent scenario (that is not caused by incorrect configuration),
|
||||
// denied errors are handled differently using HandleAccessDeniedErrorAsync().
|
||||
return await HandleAccessDeniedErrorAsync(properties);
|
||||
}
|
||||
|
||||
var returnedToken = query["oauth_token"];
|
||||
|
|
@ -130,7 +132,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
|
|||
{
|
||||
if (string.IsNullOrEmpty(properties.RedirectUri))
|
||||
{
|
||||
properties.RedirectUri = CurrentUri;
|
||||
properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
|
||||
}
|
||||
|
||||
// If CallbackConfirmed is false, this will throw
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Authentication.WsFederation
|
|||
// Save the original challenge URI so we can redirect back to it when we're done.
|
||||
if (string.IsNullOrEmpty(properties.RedirectUri))
|
||||
{
|
||||
properties.RedirectUri = CurrentUri;
|
||||
properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
|
||||
}
|
||||
|
||||
var wsFederationMessage = new WsFederationMessage()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
// 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 Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access denied failure context information to handler providers.
|
||||
/// </summary>
|
||||
public class AccessDeniedContext : HandleRequestContext<RemoteAuthenticationOptions>
|
||||
{
|
||||
public AccessDeniedContext(
|
||||
HttpContext context,
|
||||
AuthenticationScheme scheme,
|
||||
RemoteAuthenticationOptions options)
|
||||
: base(context, scheme, options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint path the user agent will be redirected to.
|
||||
/// By default, this property is set to <see cref="RemoteAuthenticationOptions.AccessDeniedPath"/>.
|
||||
/// </summary>
|
||||
public PathString AccessDeniedPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional state values for the authentication session.
|
||||
/// </summary>
|
||||
public AuthenticationProperties Properties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the return URL that will be flowed up to the access denied page.
|
||||
/// If <see cref="ReturnUrlParameter"/> is not set, this property is not used.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter name that will be used to flow the return URL.
|
||||
/// By default, this property is set to <see cref="RemoteAuthenticationOptions.ReturnUrlParameter"/>.
|
||||
/// </summary>
|
||||
public string ReturnUrlParameter { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,12 +8,18 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
{
|
||||
public class RemoteAuthenticationEvents
|
||||
{
|
||||
public Func<AccessDeniedContext, Task> OnAccessDenied { get; set; } = context => Task.CompletedTask;
|
||||
public Func<RemoteFailureContext, Task> OnRemoteFailure { get; set; } = context => Task.CompletedTask;
|
||||
|
||||
public Func<TicketReceivedContext, Task> OnTicketReceived { get; set; } = context => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when there is a remote failure
|
||||
/// Invoked when an access denied error was returned by the remote server.
|
||||
/// </summary>
|
||||
public virtual Task AccessDenied(AccessDeniedContext context) => OnAccessDenied(context);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when there is a remote failure.
|
||||
/// </summary>
|
||||
public virtual Task RemoteFailure(RemoteFailureContext context) => OnRemoteFailure(context);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,17 +7,20 @@ namespace Microsoft.Extensions.Logging
|
|||
{
|
||||
internal static class LoggingExtensions
|
||||
{
|
||||
private static Action<ILogger, string, Exception> _authSchemeAuthenticated;
|
||||
private static Action<ILogger, string, Exception> _authSchemeNotAuthenticated;
|
||||
private static Action<ILogger, string, string, Exception> _authSchemeNotAuthenticatedWithFailure;
|
||||
private static Action<ILogger, string, Exception> _authSchemeChallenged;
|
||||
private static Action<ILogger, string, Exception> _authSchemeForbidden;
|
||||
private static Action<ILogger, string, Exception> _remoteAuthenticationError;
|
||||
private static Action<ILogger, Exception> _signInHandled;
|
||||
private static Action<ILogger, Exception> _signInSkipped;
|
||||
private static Action<ILogger, string, Exception> _correlationPropertyNotFound;
|
||||
private static Action<ILogger, string, Exception> _correlationCookieNotFound;
|
||||
private static Action<ILogger, string, string, Exception> _unexpectedCorrelationCookieValue;
|
||||
private static readonly Action<ILogger, string, Exception> _authSchemeAuthenticated;
|
||||
private static readonly Action<ILogger, string, Exception> _authSchemeNotAuthenticated;
|
||||
private static readonly Action<ILogger, string, string, Exception> _authSchemeNotAuthenticatedWithFailure;
|
||||
private static readonly Action<ILogger, string, Exception> _authSchemeChallenged;
|
||||
private static readonly Action<ILogger, string, Exception> _authSchemeForbidden;
|
||||
private static readonly Action<ILogger, string, Exception> _remoteAuthenticationError;
|
||||
private static readonly Action<ILogger, Exception> _signInHandled;
|
||||
private static readonly Action<ILogger, Exception> _signInSkipped;
|
||||
private static readonly Action<ILogger, string, Exception> _correlationPropertyNotFound;
|
||||
private static readonly Action<ILogger, string, Exception> _correlationCookieNotFound;
|
||||
private static readonly Action<ILogger, string, string, Exception> _unexpectedCorrelationCookieValue;
|
||||
private static readonly Action<ILogger, Exception> _accessDeniedError;
|
||||
private static readonly Action<ILogger, Exception> _accessDeniedContextHandled;
|
||||
private static readonly Action<ILogger, Exception> _accessDeniedContextSkipped;
|
||||
|
||||
static LoggingExtensions()
|
||||
{
|
||||
|
|
@ -65,6 +68,18 @@ namespace Microsoft.Extensions.Logging
|
|||
eventId: 16,
|
||||
logLevel: LogLevel.Warning,
|
||||
formatString: "The correlation cookie value '{CorrelationCookieName}' did not match the expected value '{CorrelationCookieValue}'.");
|
||||
_accessDeniedError = LoggerMessage.Define(
|
||||
eventId: 17,
|
||||
logLevel: LogLevel.Information,
|
||||
formatString: "Access was denied by the resource owner or by the remote server.");
|
||||
_accessDeniedContextHandled = LoggerMessage.Define(
|
||||
eventId: 18,
|
||||
logLevel: LogLevel.Debug,
|
||||
formatString: "The AccessDenied event returned Handled.");
|
||||
_accessDeniedContextSkipped = LoggerMessage.Define(
|
||||
eventId: 19,
|
||||
logLevel: LogLevel.Debug,
|
||||
formatString: "The AccessDenied event returned Skipped.");
|
||||
}
|
||||
|
||||
public static void AuthenticationSchemeAuthenticated(this ILogger logger, string authenticationScheme)
|
||||
|
|
@ -121,5 +136,20 @@ namespace Microsoft.Extensions.Logging
|
|||
{
|
||||
_unexpectedCorrelationCookieValue(logger, cookieName, cookieValue, null);
|
||||
}
|
||||
|
||||
public static void AccessDeniedError(this ILogger logger)
|
||||
{
|
||||
_accessDeniedError(logger, null);
|
||||
}
|
||||
|
||||
public static void AccessDeniedContextHandled(this ILogger logger)
|
||||
{
|
||||
_accessDeniedContextHandled(logger, null);
|
||||
}
|
||||
|
||||
public static void AccessDeniedContextSkipped(this ILogger logger)
|
||||
{
|
||||
_accessDeniedContextSkipped(logger, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Security.Cryptography;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -241,5 +242,48 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual async Task<HandleRequestResult> HandleAccessDeniedErrorAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Logger.AccessDeniedError();
|
||||
var context = new AccessDeniedContext(Context, Scheme, Options)
|
||||
{
|
||||
AccessDeniedPath = Options.AccessDeniedPath,
|
||||
Properties = properties,
|
||||
ReturnUrl = properties?.RedirectUri,
|
||||
ReturnUrlParameter = Options.ReturnUrlParameter
|
||||
};
|
||||
await Events.AccessDenied(context);
|
||||
|
||||
if (context.Result != null)
|
||||
{
|
||||
if (context.Result.Handled)
|
||||
{
|
||||
Logger.AccessDeniedContextHandled();
|
||||
}
|
||||
else if (context.Result.Skipped)
|
||||
{
|
||||
Logger.AccessDeniedContextSkipped();
|
||||
}
|
||||
|
||||
return context.Result;
|
||||
}
|
||||
|
||||
// If an access denied endpoint was specified, redirect the user agent.
|
||||
// Otherwise, invoke the RemoteFailure event for further processing.
|
||||
if (context.AccessDeniedPath.HasValue)
|
||||
{
|
||||
string uri = context.AccessDeniedPath;
|
||||
if (!string.IsNullOrEmpty(context.ReturnUrlParameter) && !string.IsNullOrEmpty(context.ReturnUrl))
|
||||
{
|
||||
uri = QueryHelpers.AddQueryString(uri, context.ReturnUrlParameter, context.ReturnUrl);
|
||||
}
|
||||
Response.Redirect(uri);
|
||||
|
||||
return HandleRequestResult.Handle();
|
||||
}
|
||||
|
||||
return HandleRequestResult.Fail("Access was denied by the resource owner or by the remote server.", properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +89,22 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
/// </summary>
|
||||
public PathString CallbackPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional path the user agent is redirected to if the user
|
||||
/// doesn't approve the authorization demand requested by the remote server.
|
||||
/// This property is not set by default. In this case, an exception is thrown
|
||||
/// if an access_denied response is returned by the remote authorization server.
|
||||
/// </summary>
|
||||
public PathString AccessDeniedPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the parameter used to convey the original location
|
||||
/// of the user before the remote challenge was triggered up to the access denied page.
|
||||
/// This property is only used when the <see cref="AccessDeniedPath"/> is explicitly specified.
|
||||
/// </summary>
|
||||
// Note: this deliberately matches the default parameter name used by the cookie handler.
|
||||
public string ReturnUrlParameter { get; set; } = "ReturnUrl";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication scheme corresponding to the middleware
|
||||
/// responsible of persisting user's identity after a successful authentication.
|
||||
|
|
|
|||
|
|
@ -357,6 +357,70 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
|||
Assert.Equal("The oauth state was missing or invalid.", error.GetBaseException().Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task ReplyPathWithAccessDeniedErrorFails(bool redirect)
|
||||
{
|
||||
var server = CreateServer(o =>
|
||||
{
|
||||
o.ClientId = "Test Id";
|
||||
o.ClientSecret = "Test Secret";
|
||||
o.StateDataFormat = new TestStateDataFormat();
|
||||
o.Events = redirect ? new OAuthEvents()
|
||||
{
|
||||
OnAccessDenied = ctx =>
|
||||
{
|
||||
ctx.Response.Redirect("/error?FailureMessage=AccessDenied");
|
||||
ctx.HandleResponse();
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
} : new OAuthEvents();
|
||||
});
|
||||
var sendTask = server.SendAsync("https://example.com/signin-google?error=access_denied&error_description=SoBad&error_uri=foobar&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=AccessDenied", transaction.Response.Headers.GetValues("Location").First());
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await Assert.ThrowsAnyAsync<Exception>(() => sendTask);
|
||||
Assert.Equal("Access was denied by the resource owner or by the remote server.", error.GetBaseException().Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReplyPathWithAccessDeniedError_AllowsCustomizingPath()
|
||||
{
|
||||
var server = CreateServer(o =>
|
||||
{
|
||||
o.ClientId = "Test Id";
|
||||
o.ClientSecret = "Test Secret";
|
||||
o.StateDataFormat = new TestStateDataFormat();
|
||||
o.AccessDeniedPath = "/access-denied";
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnAccessDenied = ctx =>
|
||||
{
|
||||
Assert.Equal("/access-denied", ctx.AccessDeniedPath.Value);
|
||||
Assert.Equal("http://testhost/redirect", ctx.ReturnUrl);
|
||||
Assert.Equal("ReturnUrl", ctx.ReturnUrlParameter);
|
||||
ctx.AccessDeniedPath = "/custom-denied-page";
|
||||
ctx.ReturnUrl = "http://www.google.com/";
|
||||
ctx.ReturnUrlParameter = "rurl";
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
};
|
||||
});
|
||||
var transaction = await server.SendAsync("https://example.com/signin-google?error=access_denied&error_description=SoBad&error_uri=foobar&state=protected_state",
|
||||
".AspNetCore.Correlation.Google.correlationId=N");
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.Equal("/custom-denied-page?rurl=http%3A%2F%2Fwww.google.com%2F", transaction.Response.Headers.GetValues("Location").First());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
|
|
@ -378,7 +442,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
|||
} : new OAuthEvents();
|
||||
});
|
||||
var sendTask = server.SendAsync("https://example.com/signin-google?error=OMG&error_description=SoBad&error_uri=foobar&state=protected_state",
|
||||
".AspNetCore.Correlation.Google.corrilationId=N");
|
||||
".AspNetCore.Correlation.Google.correlationId=N");
|
||||
if (redirect)
|
||||
{
|
||||
var transaction = await sendTask;
|
||||
|
|
@ -1205,7 +1269,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
|||
Assert.Equal("protected_state", protectedText);
|
||||
var properties = new AuthenticationProperties(new Dictionary<string, string>()
|
||||
{
|
||||
{ ".xsrf", "corrilationId" },
|
||||
{ ".xsrf", "correlationId" },
|
||||
{ "testkey", "testvalue" }
|
||||
});
|
||||
properties.RedirectUri = "http://testhost/redirect";
|
||||
|
|
|
|||
|
|
@ -251,6 +251,101 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
o.CallbackPath = "/oauth-callback";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleRequestAsync_RedirectsToAccessDeniedPathWhenExplicitlySet()
|
||||
{
|
||||
var server = CreateServer(
|
||||
s => s.AddAuthentication().AddOAuth(
|
||||
"Weblie",
|
||||
opt =>
|
||||
{
|
||||
opt.ClientId = "Test Id";
|
||||
opt.ClientSecret = "secret";
|
||||
opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
opt.AuthorizationEndpoint = "https://example.com/provider/login";
|
||||
opt.TokenEndpoint = "https://example.com/provider/token";
|
||||
opt.CallbackPath = "/oauth-callback";
|
||||
opt.AccessDeniedPath = "/access-denied";
|
||||
opt.StateDataFormat = new TestStateDataFormat();
|
||||
opt.Events.OnRemoteFailure = context => throw new InvalidOperationException("This event should not be called.");
|
||||
}));
|
||||
|
||||
var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=access_denied&state=protected_state",
|
||||
".AspNetCore.Correlation.Weblie.correlationId=N");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.Equal("/access-denied?ReturnUrl=http%3A%2F%2Ftesthost%2Fredirect", transaction.Response.Headers.Location.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleRequestAsync_InvokesAccessDeniedEvent()
|
||||
{
|
||||
var server = CreateServer(
|
||||
s => s.AddAuthentication().AddOAuth(
|
||||
"Weblie",
|
||||
opt =>
|
||||
{
|
||||
opt.ClientId = "Test Id";
|
||||
opt.ClientSecret = "secret";
|
||||
opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
opt.AuthorizationEndpoint = "https://example.com/provider/login";
|
||||
opt.TokenEndpoint = "https://example.com/provider/token";
|
||||
opt.CallbackPath = "/oauth-callback";
|
||||
opt.StateDataFormat = new TestStateDataFormat();
|
||||
opt.Events = new OAuthEvents()
|
||||
{
|
||||
OnAccessDenied = context =>
|
||||
{
|
||||
Assert.Equal("testvalue", context.Properties.Items["testkey"]);
|
||||
context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
context.HandleResponse();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
}));
|
||||
|
||||
var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=access_denied&state=protected_state",
|
||||
".AspNetCore.Correlation.Weblie.correlationId=N");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotAcceptable, transaction.Response.StatusCode);
|
||||
Assert.Null(transaction.Response.Headers.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleRequestAsync_InvokesRemoteFailureEventWhenAccessDeniedPathIsNotExplicitlySet()
|
||||
{
|
||||
var server = CreateServer(
|
||||
s => s.AddAuthentication().AddOAuth(
|
||||
"Weblie",
|
||||
opt =>
|
||||
{
|
||||
opt.ClientId = "Test Id";
|
||||
opt.ClientSecret = "secret";
|
||||
opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
opt.AuthorizationEndpoint = "https://example.com/provider/login";
|
||||
opt.TokenEndpoint = "https://example.com/provider/token";
|
||||
opt.CallbackPath = "/oauth-callback";
|
||||
opt.StateDataFormat = new TestStateDataFormat();
|
||||
opt.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = context =>
|
||||
{
|
||||
Assert.Equal("Access was denied by the resource owner or by the remote server.", context.Failure.Message);
|
||||
Assert.Equal("testvalue", context.Properties.Items["testkey"]);
|
||||
context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
context.HandleResponse();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
}));
|
||||
|
||||
var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=access_denied&state=protected_state",
|
||||
".AspNetCore.Correlation.Weblie.correlationId=N");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotAcceptable, transaction.Response.StatusCode);
|
||||
Assert.Null(transaction.Response.Headers.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoteAuthenticationFailed_OAuthError_IncludesProperties()
|
||||
{
|
||||
|
|
@ -270,7 +365,7 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
{
|
||||
OnRemoteFailure = context =>
|
||||
{
|
||||
Assert.Contains("declined", context.Failure.Message);
|
||||
Assert.Contains("custom_error", context.Failure.Message);
|
||||
Assert.Equal("testvalue", context.Properties.Items["testkey"]);
|
||||
context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
context.HandleResponse();
|
||||
|
|
@ -279,8 +374,8 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
};
|
||||
}));
|
||||
|
||||
var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=declined&state=protected_state",
|
||||
".AspNetCore.Correlation.Weblie.corrilationId=N");
|
||||
var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=custom_error&state=protected_state",
|
||||
".AspNetCore.Correlation.Weblie.correlationId=N");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotAcceptable, transaction.Response.StatusCode);
|
||||
Assert.Null(transaction.Response.Headers.Location);
|
||||
|
|
@ -323,7 +418,7 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
Assert.Equal("protected_state", protectedText);
|
||||
var properties = new AuthenticationProperties(new Dictionary<string, string>()
|
||||
{
|
||||
{ ".xsrf", "corrilationId" },
|
||||
{ ".xsrf", "correlationId" },
|
||||
{ "testkey", "testvalue" }
|
||||
});
|
||||
properties.RedirectUri = "http://testhost/redirect";
|
||||
|
|
|
|||
|
|
@ -783,6 +783,52 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
|
|||
events.ValidateExpectations();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnAccessDenied_Skip_NoMoreEventsRun()
|
||||
{
|
||||
var events = new ExpectedOidcEvents()
|
||||
{
|
||||
ExpectMessageReceived = true,
|
||||
ExpectAccessDenied = true
|
||||
};
|
||||
events.OnAccessDenied = context =>
|
||||
{
|
||||
context.SkipHandler();
|
||||
return Task.FromResult(0);
|
||||
};
|
||||
var server = CreateServer(events, AppWritePath);
|
||||
|
||||
var response = await PostAsync(server, "signin-oidc", "error=access_denied&state=protected_state");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
|
||||
events.ValidateExpectations();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnAccessDenied_Handled_NoMoreEventsRun()
|
||||
{
|
||||
var events = new ExpectedOidcEvents()
|
||||
{
|
||||
ExpectMessageReceived = true,
|
||||
ExpectAccessDenied = true
|
||||
};
|
||||
events.OnAccessDenied = context =>
|
||||
{
|
||||
Assert.Equal("testvalue", context.Properties.Items["testkey"]);
|
||||
context.HandleResponse();
|
||||
context.Response.StatusCode = StatusCodes.Status202Accepted;
|
||||
return Task.FromResult(0);
|
||||
};
|
||||
var server = CreateServer(events, AppNotImpl);
|
||||
|
||||
var response = await PostAsync(server, "signin-oidc", "error=access_denied&state=protected_state");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
Assert.Equal("", await response.Content.ReadAsStringAsync());
|
||||
events.ValidateExpectations();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnRemoteFailure_Skip_NoMoreEventsRun()
|
||||
{
|
||||
|
|
@ -1099,6 +1145,9 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
|
|||
public bool ExpectTokenValidated { get; set; }
|
||||
public bool InvokedTokenValidated { get; set; }
|
||||
|
||||
public bool ExpectAccessDenied { get; set; }
|
||||
public bool InvokedAccessDenied { get; set; }
|
||||
|
||||
public bool ExpectRemoteFailure { get; set; }
|
||||
public bool InvokedRemoteFailure { get; set; }
|
||||
|
||||
|
|
@ -1168,6 +1217,12 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
|
|||
return base.TicketReceived(context);
|
||||
}
|
||||
|
||||
public override Task AccessDenied(AccessDeniedContext context)
|
||||
{
|
||||
InvokedAccessDenied = true;
|
||||
return base.AccessDenied(context);
|
||||
}
|
||||
|
||||
public override Task RemoteFailure(RemoteFailureContext context)
|
||||
{
|
||||
InvokedRemoteFailure = true;
|
||||
|
|
@ -1201,6 +1256,7 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
|
|||
Assert.Equal(ExpectUserInfoReceived, InvokedUserInfoReceived);
|
||||
Assert.Equal(ExpectAuthenticationFailed, InvokeAuthenticationFailed);
|
||||
Assert.Equal(ExpectTicketReceived, InvokedTicketReceived);
|
||||
Assert.Equal(ExpectAccessDenied, InvokedAccessDenied);
|
||||
Assert.Equal(ExpectRemoteFailure, InvokedRemoteFailure);
|
||||
Assert.Equal(ExpectRedirectForSignOut, InvokedRedirectForSignOut);
|
||||
Assert.Equal(ExpectRemoteSignOut, InvokedRemoteSignOut);
|
||||
|
|
@ -1248,7 +1304,7 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
|
|||
private Task<HttpResponseMessage> PostAsync(TestServer server, string path, string form)
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var cookie = ".AspNetCore.Correlation." + OpenIdConnectDefaults.AuthenticationScheme + ".corrilationId=N";
|
||||
var cookie = ".AspNetCore.Correlation." + OpenIdConnectDefaults.AuthenticationScheme + ".correlationId=N";
|
||||
client.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||
return client.PostAsync("signin-oidc",
|
||||
new StringContent(form, Encoding.ASCII, "application/x-www-form-urlencoded"));
|
||||
|
|
@ -1273,7 +1329,7 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
|
|||
Assert.Equal("protected_state", protectedText);
|
||||
var properties = new AuthenticationProperties(new Dictionary<string, string>()
|
||||
{
|
||||
{ ".xsrf", "corrilationId" },
|
||||
{ ".xsrf", "correlationId" },
|
||||
{ OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, "redirect_uri" },
|
||||
{ "testkey", "testvalue" }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -174,6 +174,94 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
|
|||
Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleRequestAsync_RedirectsToAccessDeniedPathWhenExplicitlySet()
|
||||
{
|
||||
var server = CreateServer(o =>
|
||||
{
|
||||
o.ConsumerKey = "Test Consumer Key";
|
||||
o.ConsumerSecret = "Test Consumer Secret";
|
||||
o.BackchannelHttpHandler = new TestHttpMessageHandler
|
||||
{
|
||||
Sender = BackchannelRequestToken
|
||||
};
|
||||
o.AccessDeniedPath = "/access-denied";
|
||||
o.Events.OnRemoteFailure = context => throw new InvalidOperationException("This event should not be called.");
|
||||
},
|
||||
async context =>
|
||||
{
|
||||
var properties = new AuthenticationProperties();
|
||||
properties.Items["testkey"] = "testvalue";
|
||||
await context.ChallengeAsync("Twitter", properties);
|
||||
return true;
|
||||
});
|
||||
var transaction = await server.SendAsync("http://example.com/challenge");
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
var location = transaction.Response.Headers.Location.AbsoluteUri;
|
||||
Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location);
|
||||
Assert.True(transaction.Response.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookie));
|
||||
Assert.True(SetCookieHeaderValue.TryParseList(setCookie.ToList(), out var setCookieValues));
|
||||
Assert.Single(setCookieValues);
|
||||
var setCookieValue = setCookieValues.Single();
|
||||
var cookie = new CookieHeaderValue(setCookieValue.Name, setCookieValue.Value);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/signin-twitter?denied=ABCDEFG");
|
||||
request.Headers.Add(HeaderNames.Cookie, cookie.ToString());
|
||||
var client = server.CreateClient();
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
||||
Assert.Equal("/access-denied?ReturnUrl=%2Fchallenge", response.Headers.Location.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BadCallbackCallsAccessDeniedWithState()
|
||||
{
|
||||
var server = CreateServer(o =>
|
||||
{
|
||||
o.ConsumerKey = "Test Consumer Key";
|
||||
o.ConsumerSecret = "Test Consumer Secret";
|
||||
o.BackchannelHttpHandler = new TestHttpMessageHandler
|
||||
{
|
||||
Sender = BackchannelRequestToken
|
||||
};
|
||||
o.Events = new TwitterEvents()
|
||||
{
|
||||
OnAccessDenied = context =>
|
||||
{
|
||||
Assert.NotNull(context.Properties);
|
||||
Assert.Equal("testvalue", context.Properties.Items["testkey"]);
|
||||
context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
context.HandleResponse();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
},
|
||||
async context =>
|
||||
{
|
||||
var properties = new AuthenticationProperties();
|
||||
properties.Items["testkey"] = "testvalue";
|
||||
await context.ChallengeAsync("Twitter", properties);
|
||||
return true;
|
||||
});
|
||||
var transaction = await server.SendAsync("http://example.com/challenge");
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
var location = transaction.Response.Headers.Location.AbsoluteUri;
|
||||
Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location);
|
||||
Assert.True(transaction.Response.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookie));
|
||||
Assert.True(SetCookieHeaderValue.TryParseList(setCookie.ToList(), out var setCookieValues));
|
||||
Assert.Single(setCookieValues);
|
||||
var setCookieValue = setCookieValues.Single();
|
||||
var cookie = new CookieHeaderValue(setCookieValue.Name, setCookieValue.Value);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/signin-twitter?denied=ABCDEFG");
|
||||
request.Headers.Add(HeaderNames.Cookie, cookie.ToString());
|
||||
var client = server.CreateClient();
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BadCallbackCallsRemoteAuthFailedWithState()
|
||||
{
|
||||
|
|
@ -190,7 +278,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
|
|||
OnRemoteFailure = context =>
|
||||
{
|
||||
Assert.NotNull(context.Failure);
|
||||
Assert.Equal("The user denied permissions.", context.Failure.Message);
|
||||
Assert.Equal("Access was denied by the resource owner or by the remote server.", context.Failure.Message);
|
||||
Assert.NotNull(context.Properties);
|
||||
Assert.Equal("testvalue", context.Properties.Items["testkey"]);
|
||||
context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
|
|
|
|||
Loading…
Reference in New Issue