Update OIDC signout flow
OIDC signout should return to CallbackPath then locally redirect to AuthProperties.RedirectUri
This commit is contained in:
parent
0d5482685b
commit
26956c5ce1
|
|
@ -81,6 +81,7 @@ namespace OpenIdConnect.AzureAdSample
|
|||
ClientSecret = clientSecret, // for code flow
|
||||
Authority = authority,
|
||||
ResponseType = OpenIdConnectResponseType.CodeIdToken,
|
||||
PostLogoutRedirectUri = "/usersignout",
|
||||
// GetClaimsFromUserInfoEndpoint = true,
|
||||
Events = new OpenIdConnectEvents()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ namespace Microsoft.Extensions.Logging
|
|||
private static Action<ILogger, Exception> _redeemingCodeForTokens;
|
||||
private static Action<ILogger, string, Exception> _enteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync;
|
||||
private static Action<ILogger, string, Exception> _enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync;
|
||||
private static Action<ILogger, string, Exception> _enteringOpenIdAuthenticationHandlerHandleSignOutAsync;
|
||||
private static Action<ILogger, string, Exception> _messageReceived;
|
||||
private static Action<ILogger, Exception> _messageReceivedContextHandledResponse;
|
||||
private static Action<ILogger, Exception> _messageReceivedContextSkipped;
|
||||
|
|
@ -47,6 +48,7 @@ namespace Microsoft.Extensions.Logging
|
|||
private static Action<ILogger, string, Exception> _invalidSecurityTokenType;
|
||||
private static Action<ILogger, string, Exception> _unableToValidateIdToken;
|
||||
private static Action<ILogger, string, Exception> _postAuthenticationLocalRedirect;
|
||||
private static Action<ILogger, string, Exception> _postSignOutRedirect;
|
||||
private static Action<ILogger, Exception> _remoteSignOutHandledResponse;
|
||||
private static Action<ILogger, Exception> _remoteSignOutSkipped;
|
||||
private static Action<ILogger, Exception> _remoteSignOut;
|
||||
|
|
@ -72,6 +74,10 @@ namespace Microsoft.Extensions.Logging
|
|||
eventId: 4,
|
||||
logLevel: LogLevel.Trace,
|
||||
formatString: "Entering {OpenIdConnectHandlerType}'s HandleUnauthorizedAsync.");
|
||||
_enteringOpenIdAuthenticationHandlerHandleSignOutAsync = LoggerMessage.Define<string>(
|
||||
eventId: 14,
|
||||
logLevel: LogLevel.Trace,
|
||||
formatString: "Entering {OpenIdConnectHandlerType}'s HandleSignOutAsync.");
|
||||
_postAuthenticationLocalRedirect = LoggerMessage.Define<string>(
|
||||
eventId: 5,
|
||||
logLevel: LogLevel.Trace,
|
||||
|
|
@ -180,6 +186,10 @@ namespace Microsoft.Extensions.Logging
|
|||
eventId: 32,
|
||||
logLevel: LogLevel.Debug,
|
||||
formatString: "TokenResponseReceived.Skipped");
|
||||
_postSignOutRedirect = LoggerMessage.Define<string>(
|
||||
eventId: 33,
|
||||
logLevel: LogLevel.Trace,
|
||||
formatString: "Using properties.RedirectUri for redirect post authentication: '{RedirectUri}'.");
|
||||
_userInformationReceived = LoggerMessage.Define<string>(
|
||||
eventId: 35,
|
||||
logLevel: LogLevel.Trace,
|
||||
|
|
@ -430,6 +440,11 @@ namespace Microsoft.Extensions.Logging
|
|||
_enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(logger, openIdConnectHandlerTypeName, null);
|
||||
}
|
||||
|
||||
public static void EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(this ILogger logger, string openIdConnectHandlerTypeName)
|
||||
{
|
||||
_enteringOpenIdAuthenticationHandlerHandleSignOutAsync(logger, openIdConnectHandlerTypeName, null);
|
||||
}
|
||||
|
||||
public static void UserInformationReceived(this ILogger logger, string user)
|
||||
{
|
||||
_userInformationReceived(logger, user, null);
|
||||
|
|
@ -440,6 +455,11 @@ namespace Microsoft.Extensions.Logging
|
|||
_postAuthenticationLocalRedirect(logger, redirectUri, null);
|
||||
}
|
||||
|
||||
public static void PostSignOutRedirect(this ILogger logger, string redirectUri)
|
||||
{
|
||||
_postSignOutRedirect(logger, redirectUri, null);
|
||||
}
|
||||
|
||||
public static void RemoteSignOutHandledResponse(this ILogger logger)
|
||||
{
|
||||
_remoteSignOutHandledResponse(logger, null);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.AspNetCore.Http.Authentication;
|
||||
using Microsoft.AspNetCore.Http.Features.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
|
@ -68,7 +69,14 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
{
|
||||
return await HandleRemoteSignOutAsync();
|
||||
}
|
||||
return await base.HandleRequestAsync();
|
||||
else if (Options.SignedOutCallbackPath.HasValue && Options.SignedOutCallbackPath == Request.Path)
|
||||
{
|
||||
return await HandleSignOutCallbackAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
return await base.HandleRequestAsync();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual async Task<bool> HandleRemoteSignOutAsync()
|
||||
|
|
@ -79,12 +87,14 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
{
|
||||
message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
|
||||
}
|
||||
|
||||
// assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
|
||||
else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrEmpty(Request.ContentType)
|
||||
// May have media/type; charset=utf-8, allow partial match.
|
||||
&& Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
|
||||
&& Request.Body.CanRead) {
|
||||
&& Request.Body.CanRead)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
|
||||
}
|
||||
|
|
@ -139,108 +149,134 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles Signout
|
||||
/// Sign out the Relying Party from the OpenID provider
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override async Task HandleSignOutAsync(SignOutContext signout)
|
||||
/// <returns>A task executing the sign out procedure</returns>
|
||||
protected override async Task HandleSignOutAsync(SignOutContext context)
|
||||
{
|
||||
if (signout != null)
|
||||
if (context == null)
|
||||
{
|
||||
if (_configuration == null && Options.ConfigurationManager != null)
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(GetType().FullName);
|
||||
|
||||
if (_configuration == null && Options.ConfigurationManager != null)
|
||||
{
|
||||
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
|
||||
}
|
||||
|
||||
var message = new OpenIdConnectMessage()
|
||||
{
|
||||
IssuerAddress = _configuration == null ? string.Empty : (_configuration.EndSessionEndpoint ?? string.Empty),
|
||||
|
||||
// Redirect back of SigneOutCallbackPath first before user agent is redirected to actual post logout redirect uri
|
||||
PostLogoutRedirectUri = BuildRedirectUriIfRelative(Options.SignedOutCallbackPath)
|
||||
};
|
||||
|
||||
// Get the post redirect URI.
|
||||
var properties = new AuthenticationProperties(context.Properties);
|
||||
if (string.IsNullOrEmpty(properties.RedirectUri))
|
||||
{
|
||||
properties.RedirectUri = BuildRedirectUriIfRelative(Options.PostLogoutRedirectUri);
|
||||
if (string.IsNullOrWhiteSpace(properties.RedirectUri))
|
||||
{
|
||||
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
|
||||
}
|
||||
|
||||
var message = new OpenIdConnectMessage()
|
||||
{
|
||||
IssuerAddress = _configuration == null ? string.Empty : (_configuration.EndSessionEndpoint ?? string.Empty),
|
||||
};
|
||||
|
||||
// Set End_Session_Endpoint in order:
|
||||
// 1. properties.Redirect
|
||||
// 2. Options.PostLogoutRedirectUri
|
||||
var properties = new AuthenticationProperties(signout.Properties);
|
||||
var logoutRedirectUri = properties.RedirectUri;
|
||||
if (!string.IsNullOrEmpty(logoutRedirectUri))
|
||||
{
|
||||
// Relative to PathBase
|
||||
if (logoutRedirectUri.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
logoutRedirectUri = BuildRedirectUri(logoutRedirectUri);
|
||||
}
|
||||
message.PostLogoutRedirectUri = logoutRedirectUri;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Options.PostLogoutRedirectUri))
|
||||
{
|
||||
logoutRedirectUri = Options.PostLogoutRedirectUri;
|
||||
// Relative to PathBase
|
||||
if (logoutRedirectUri.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
logoutRedirectUri = BuildRedirectUri(logoutRedirectUri);
|
||||
}
|
||||
message.PostLogoutRedirectUri = logoutRedirectUri;
|
||||
}
|
||||
|
||||
// Attach the identity token to the logout request when possible.
|
||||
message.IdTokenHint = await Context.Authentication.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken);
|
||||
|
||||
var redirectContext = new RedirectContext(Context, Options, properties)
|
||||
{
|
||||
ProtocolMessage = message
|
||||
};
|
||||
|
||||
await Options.Events.RedirectToIdentityProviderForSignOut(redirectContext);
|
||||
if (redirectContext.HandledResponse)
|
||||
{
|
||||
Logger.RedirectToIdentityProviderForSignOutHandledResponse();
|
||||
return;
|
||||
}
|
||||
else if (redirectContext.Skipped)
|
||||
{
|
||||
Logger.RedirectToIdentityProviderForSignOutSkipped();
|
||||
return;
|
||||
}
|
||||
|
||||
message = redirectContext.ProtocolMessage;
|
||||
|
||||
if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
|
||||
{
|
||||
var redirectUri = message.CreateLogoutRequestUrl();
|
||||
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
|
||||
{
|
||||
Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri);
|
||||
}
|
||||
|
||||
Response.Redirect(redirectUri);
|
||||
}
|
||||
else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
|
||||
{
|
||||
var inputs = new StringBuilder();
|
||||
foreach (var parameter in message.Parameters)
|
||||
{
|
||||
var name = HtmlEncoder.Encode(parameter.Key);
|
||||
var value = HtmlEncoder.Encode(parameter.Value);
|
||||
|
||||
var input = string.Format(CultureInfo.InvariantCulture, InputTagFormat, name, value);
|
||||
inputs.AppendLine(input);
|
||||
}
|
||||
|
||||
var issuer = HtmlEncoder.Encode(message.IssuerAddress);
|
||||
|
||||
var content = string.Format(CultureInfo.InvariantCulture, HtmlFormFormat, issuer, inputs);
|
||||
var buffer = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
Response.ContentLength = buffer.Length;
|
||||
Response.ContentType = "text/html;charset=UTF-8";
|
||||
|
||||
// Emit Cache-Control=no-cache to prevent client caching.
|
||||
Response.Headers[HeaderNames.CacheControl] = "no-cache";
|
||||
Response.Headers[HeaderNames.Pragma] = "no-cache";
|
||||
Response.Headers[HeaderNames.Expires] = "-1";
|
||||
|
||||
await Response.Body.WriteAsync(buffer, 0, buffer.Length);
|
||||
properties.RedirectUri = CurrentUri;
|
||||
}
|
||||
}
|
||||
Logger.PostSignOutRedirect(properties.RedirectUri);
|
||||
|
||||
// Attach the identity token to the logout request when possible.
|
||||
message.IdTokenHint = await Context.Authentication.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken);
|
||||
|
||||
var redirectContext = new RedirectContext(Context, Options, properties)
|
||||
{
|
||||
ProtocolMessage = message
|
||||
};
|
||||
|
||||
await Options.Events.RedirectToIdentityProviderForSignOut(redirectContext);
|
||||
if (redirectContext.HandledResponse)
|
||||
{
|
||||
Logger.RedirectToIdentityProviderForSignOutHandledResponse();
|
||||
return;
|
||||
}
|
||||
else if (redirectContext.Skipped)
|
||||
{
|
||||
Logger.RedirectToIdentityProviderForSignOutSkipped();
|
||||
return;
|
||||
}
|
||||
|
||||
message = redirectContext.ProtocolMessage;
|
||||
|
||||
if (!string.IsNullOrEmpty(message.State))
|
||||
{
|
||||
properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
|
||||
}
|
||||
|
||||
message.State = Options.StateDataFormat.Protect(properties);
|
||||
|
||||
if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
|
||||
{
|
||||
var redirectUri = message.CreateLogoutRequestUrl();
|
||||
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
|
||||
{
|
||||
Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri);
|
||||
}
|
||||
|
||||
Response.Redirect(redirectUri);
|
||||
}
|
||||
else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
|
||||
{
|
||||
var inputs = new StringBuilder();
|
||||
foreach (var parameter in message.Parameters)
|
||||
{
|
||||
var name = HtmlEncoder.Encode(parameter.Key);
|
||||
var value = HtmlEncoder.Encode(parameter.Value);
|
||||
|
||||
var input = string.Format(CultureInfo.InvariantCulture, InputTagFormat, name, value);
|
||||
inputs.AppendLine(input);
|
||||
}
|
||||
|
||||
var issuer = HtmlEncoder.Encode(message.IssuerAddress);
|
||||
|
||||
var content = string.Format(CultureInfo.InvariantCulture, HtmlFormFormat, issuer, inputs);
|
||||
var buffer = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
Response.ContentLength = buffer.Length;
|
||||
Response.ContentType = "text/html;charset=UTF-8";
|
||||
|
||||
// Emit Cache-Control=no-cache to prevent client caching.
|
||||
Response.Headers[HeaderNames.CacheControl] = "no-cache";
|
||||
Response.Headers[HeaderNames.Pragma] = "no-cache";
|
||||
Response.Headers[HeaderNames.Expires] = "-1";
|
||||
|
||||
await Response.Body.WriteAsync(buffer, 0, buffer.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response to the callback from OpenId provider after session ended.
|
||||
/// </summary>
|
||||
/// <returns>A task executing the callback procedure</returns>
|
||||
protected virtual Task<bool> HandleSignOutCallbackAsync()
|
||||
{
|
||||
StringValues protectedState;
|
||||
if (Request.Query.TryGetValue("State", out protectedState))
|
||||
{
|
||||
var properties = Options.StateDataFormat.Unprotect(protectedState);
|
||||
if (!string.IsNullOrEmpty(properties.RedirectUri))
|
||||
{
|
||||
Response.Redirect(properties.RedirectUri);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
Response.Redirect("/");
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1111,5 +1147,23 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a redirect path if the given path is a relative path.
|
||||
/// </summary>
|
||||
private string BuildRedirectUriIfRelative(string uri)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uri))
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
if (!uri.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
return BuildRedirectUri(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ namespace Microsoft.AspNetCore.Builder
|
|||
AutomaticChallenge = true;
|
||||
DisplayName = OpenIdConnectDefaults.Caption;
|
||||
CallbackPath = new PathString("/signin-oidc");
|
||||
SignedOutCallbackPath = new PathString("/signout-callback-oidc");
|
||||
RemoteSignOutPath = new PathString("/signout-oidc");
|
||||
|
||||
Events = new OpenIdConnectEvents();
|
||||
Scope.Add("openid");
|
||||
Scope.Add("profile");
|
||||
|
|
@ -120,9 +122,15 @@ namespace Microsoft.AspNetCore.Builder
|
|||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the 'post_logout_redirect_uri'
|
||||
/// The request path within the application's base path where the user agent will be returned after sign out from the identity provider.
|
||||
/// </summary>
|
||||
/// <remarks>This is sent to the OP as the redirect for the user-agent.</remarks>
|
||||
public PathString SignedOutCallbackPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The uri where the user agent will be returned to after application is signed out from the identity provider.
|
||||
/// The redirect will happen after the <paramref name="SignoutCallbackPath"/> is invoked.
|
||||
/// </summary>
|
||||
/// <remarks>This URI is optional and it can be out of the application's domain.</remarks>
|
||||
public string PostLogoutRedirectUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
{
|
||||
return await HandleRemoteCallbackAsync();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue