Update OIDC signout flow

OIDC signout should return to CallbackPath then locally redirect to AuthProperties.RedirectUri
This commit is contained in:
Troy Dai 2016-07-28 00:54:23 -07:00
parent 0d5482685b
commit 26956c5ce1
5 changed files with 184 additions and 100 deletions

View File

@ -81,6 +81,7 @@ namespace OpenIdConnect.AzureAdSample
ClientSecret = clientSecret, // for code flow
Authority = authority,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
PostLogoutRedirectUri = "/usersignout",
// GetClaimsFromUserInfoEndpoint = true,
Events = new OpenIdConnectEvents()
{

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Authentication
{
return await HandleRemoteCallbackAsync();
}
return false;
}