This commit is contained in:
Hao Kung 2015-04-16 12:26:42 -07:00
parent 9ce84d39c2
commit 501bd4ff10
11 changed files with 1587 additions and 273 deletions

View File

@ -5,8 +5,8 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
public interface INonceCache
{
string AddNonce(string nonce);
bool TryAddNonce(string nonce);
bool TryRemoveNonce(string nonce);
bool HasNonce(string nonce);
}
}
}

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// The prefix used to for the a nonce in the cookie
/// </summary>
internal const string CookieNoncePrefix = ".AspNet.OpenIdConnect.Nonce.";
public const string CookieNoncePrefix = ".AspNet.OpenIdConnect.Nonce.";
/// <summary>
/// The property for the RedirectUri that was used when asking for a 'authorizationCode'
@ -36,6 +36,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// Constant used to identify state in openIdConnect protocal message
/// </summary>
internal const string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties";
public const string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties";
}
}

View File

@ -26,5 +26,16 @@ namespace Microsoft.AspNet.Builder
Name = optionsName
});
}
/// <summary>
/// Adds the <see cref="OpenIdConnectAuthenticationMiddleware"/> into the ASP.NET runtime.
/// </summary>
/// <param name="app">The application builder</param>
/// <param name="options">Options which control the processing of the OpenIdConnect protocol and token validation.</param>
/// <returns>The application builder</returns>
public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app, IOptions<OpenIdConnectAuthenticationOptions> options)
{
return app.UseMiddleware<OpenIdConnectAuthenticationMiddleware>(options);
}
}
}
}

View File

@ -75,7 +75,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
// Set End_Session_Endpoint in order:
// 1. properties.Redirect
// 2. Options.Wreply
// 2. Options.PostLogoutRedirectUri
var properties = new AuthenticationProperties(signout.Properties);
if (!string.IsNullOrEmpty(properties.RedirectUri))
{
@ -98,7 +98,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
string redirectUri = notification.ProtocolMessage.CreateLogoutRequestUrl();
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
{
_logger.LogWarning("The logout redirect URI is malformed: {0}", (redirectUri ?? "null"));
_logger.LogWarning(Resources.OIDCH_0051_RedirectUriLogoutIsNotWellFormed, redirectUri);
}
Response.Redirect(redirectUri);
@ -115,28 +115,37 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity.
/// </summary>
/// <returns></returns>
/// <remarks>Uses log id's OIDCH-0026 - OIDCH-0050, next num: 37</remarks>
protected override async Task ApplyResponseChallengeAsync()
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(Resources.OIDCH_0026_ApplyResponseChallengeAsync, this.GetType());
}
if (ShouldConvertChallengeToForbidden())
{
_logger.LogDebug(Resources.OIDCH_0027_401_ConvertedTo_403);
Response.StatusCode = 403;
return;
}
if (Response.StatusCode != 401)
{
_logger.LogDebug(Resources.OIDCH_0028_StatusCodeNot401, Response.StatusCode);
return;
}
// When Automatic should redirect on 401 even if there wasn't an explicit challenge.
if (ChallengeContext == null && !Options.AutomaticAuthentication)
{
_logger.LogDebug(Resources.OIDCH_0029_ChallengeContextEqualsNull);
return;
}
// order for redirect_uri
// order for local RedirectUri
// 1. challenge.Properties.RedirectUri
// 2. CurrentUri
// 2. CurrentUri if Options.DefaultToCurrentUriOnRedirect is true)
AuthenticationProperties properties;
if (ChallengeContext == null)
{
@ -147,12 +156,22 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
properties = new AuthenticationProperties(ChallengeContext.Properties);
}
if (string.IsNullOrEmpty(properties.RedirectUri))
if (!string.IsNullOrWhiteSpace(properties.RedirectUri))
{
_logger.LogDebug(Resources.OIDCH_0030_Using_Properties_RedirectUri, properties.RedirectUri);
}
else if (Options.DefaultToCurrentUriOnRedirect)
{
_logger.LogDebug(Resources.OIDCH_0032_UsingCurrentUriRedirectUri, CurrentUri);
properties.RedirectUri = CurrentUri;
}
// this value will be passed to the AuthorizationCodeReceivedNotification
if (!string.IsNullOrWhiteSpace(Options.RedirectUri))
{
_logger.LogDebug(Resources.OIDCH_0031_Using_Options_RedirectUri, Options.RedirectUri);
}
// When redeeming a 'code' for an AccessToken, this value is needed
if (!string.IsNullOrWhiteSpace(Options.RedirectUri))
{
properties.Dictionary.Add(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey, Options.RedirectUri);
@ -163,14 +182,15 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
OpenIdConnectMessage openIdConnectMessage = new OpenIdConnectMessage
var message = new OpenIdConnectMessage
{
ClientId = Options.ClientId,
IssuerAddress = _configuration == null ? string.Empty : (_configuration.AuthorizationEndpoint ?? string.Empty),
IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty,
RedirectUri = Options.RedirectUri,
// [brentschmaltz] - this should be a property on RedirectToIdentityProviderNotification not on the OIDCMessage.
RequestType = OpenIdConnectRequestType.AuthenticationRequest,
Resource = Options.Resource,
ResponseMode = OpenIdConnectResponseModes.FormPost,
ResponseMode = Options.ResponseMode,
ResponseType = Options.ResponseType,
Scope = Options.Scope,
State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + Uri.EscapeDataString(Options.StateDataFormat.Protect(properties))
@ -178,33 +198,45 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
if (Options.ProtocolValidator.RequireNonce)
{
openIdConnectMessage.Nonce = Options.ProtocolValidator.GenerateNonce();
message.Nonce = Options.ProtocolValidator.GenerateNonce();
if (Options.NonceCache != null)
{
Options.NonceCache.AddNonce(openIdConnectMessage.Nonce);
if (!Options.NonceCache.TryAddNonce(message.Nonce))
{
_logger.LogError(Resources.OIDCH_0033_TryAddNonceFailed, message.Nonce);
throw new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0033_TryAddNonceFailed, message.Nonce));
}
}
else
{
RememberNonce(openIdConnectMessage.Nonce);
WriteNonceCookie(message.Nonce);
}
}
var notification = new RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage
ProtocolMessage = message
};
await Options.Notifications.RedirectToIdentityProvider(notification);
if (!notification.HandledResponse)
await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification);
if (redirectToIdentityProviderNotification.HandledResponse)
{
string redirectUri = notification.ProtocolMessage.CreateAuthenticationRequestUrl();
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
{
_logger.LogWarning("Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}", (redirectUri ?? "null"));
}
Response.Redirect(redirectUri);
_logger.LogInformation(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse);
return;
}
else if (redirectToIdentityProviderNotification.Skipped)
{
_logger.LogInformation(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped);
return;
}
string redirectUri = redirectToIdentityProviderNotification.ProtocolMessage.CreateAuthenticationRequestUrl();
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
{
_logger.LogWarning(Resources.OIDCH_0036_UriIsNotWellFormed, redirectUri);
}
Response.Redirect(redirectUri);
}
protected override AuthenticationTicket AuthenticateCore()
@ -216,15 +248,21 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// Invoked to process incoming OpenIdConnect messages.
/// </summary>
/// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns>
/// <remarks>Uses log id's OIDCH-0000 - OIDCH-0025</remarks>
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType());
}
// Allow login to be constrained to a specific path. Need to make this runtime configurable.
if (Options.CallbackPath.HasValue && Options.CallbackPath != (Request.PathBase + Request.Path))
{
return null;
}
OpenIdConnectMessage openIdConnectMessage = null;
OpenIdConnectMessage message = null;
// assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)
@ -235,180 +273,215 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
IFormCollection form = await Request.ReadFormAsync();
Request.Body.Seek(0, SeekOrigin.Begin);
openIdConnectMessage = new OpenIdConnectMessage(form);
message = new OpenIdConnectMessage(form);
}
if (openIdConnectMessage == null)
if (message == null)
{
return null;
}
try
{
var messageReceivedNotification = new MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
if (_logger.IsEnabled(LogLevel.Debug))
{
ProtocolMessage = openIdConnectMessage
};
_logger.LogDebug(Resources.OIDCH_0001_MessageReceived, message.BuildRedirectUrl());
}
var messageReceivedNotification =
new MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = message
};
await Options.Notifications.MessageReceived(messageReceivedNotification);
if (messageReceivedNotification.HandledResponse)
{
_logger.LogInformation(Resources.OIDCH_0002_MessageReceivedNotificationHandledResponse);
return messageReceivedNotification.AuthenticationTicket;
}
if (messageReceivedNotification.Skipped)
{
_logger.LogInformation(Resources.OIDCH_0003_MessageReceivedNotificationSkipped);
return null;
}
// runtime always adds state, if we don't find it OR we failed to 'unprotect' it this is not a message we
// should process.
AuthenticationProperties properties = GetPropertiesFromState(openIdConnectMessage.State);
// runtime always adds state, if we don't find it OR we failed to 'unprotect' it this is not a message we should process.
if (string.IsNullOrWhiteSpace(message.State))
{
_logger.LogError(Resources.OIDCH_0004_MessageStateIsNullOrWhiteSpace);
return null;
}
var properties = GetPropertiesFromState(message.State);
if (properties == null)
{
_logger.LogWarning("The state field is missing or invalid.");
_logger.LogError(Resources.OIDCH_0005_MessageStateIsInvalid);
return null;
}
// devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users.
if (!string.IsNullOrWhiteSpace(openIdConnectMessage.Error))
if (!string.IsNullOrWhiteSpace(message.Error))
{
throw new OpenIdConnectProtocolException(
string.Format(CultureInfo.InvariantCulture,
openIdConnectMessage.Error,
Resources.Exception_OpenIdConnectMessageError, openIdConnectMessage.ErrorDescription ?? string.Empty, openIdConnectMessage.ErrorUri ?? string.Empty));
_logger.LogError(Resources.OIDCH_0006_MessageErrorNotNull, message.Error);
throw new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0006_MessageErrorNotNull, message.Error));
}
// code is only accepted with id_token, in this version, hence check for code is inside this if
// OpenIdConnect protocol allows a Code to be received without the id_token
if (string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken))
{
_logger.LogWarning("The id_token is missing.");
return null;
}
var securityTokenReceivedNotification = new SecurityTokenReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage
};
await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification);
if (securityTokenReceivedNotification.HandledResponse)
{
return securityTokenReceivedNotification.AuthenticationTicket;
}
if (securityTokenReceivedNotification.Skipped)
{
return null;
}
AuthenticationTicket ticket = null;
JwtSecurityToken jwt = null;
if (_configuration == null && Options.ConfigurationManager != null)
{
_logger.LogDebug(Resources.OIDCH_0007_UpdatingConfiguration);
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
// Copy and augment to avoid cross request race conditions for updated configurations.
TokenValidationParameters validationParameters = Options.TokenValidationParameters.Clone();
if (_configuration != null)
// OpenIdConnect protocol allows a Code to be received without the id_token
if (!string.IsNullOrWhiteSpace(message.IdToken))
{
if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer))
{
validationParameters.ValidIssuer = _configuration.Issuer;
}
else if (!string.IsNullOrWhiteSpace(_configuration.Issuer))
{
validationParameters.ValidIssuers = (validationParameters.ValidIssuers == null ? new[] { _configuration.Issuer } : validationParameters.ValidIssuers.Concat(new[] { _configuration.Issuer }));
}
validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys));
}
AuthenticationTicket ticket;
SecurityToken validatedToken = null;
ClaimsPrincipal principal = null;
JwtSecurityToken jwt = null;
foreach (var validator in Options.SecurityTokenValidators)
{
if (validator.CanReadToken(openIdConnectMessage.IdToken))
{
principal = validator.ValidateToken(openIdConnectMessage.IdToken, validationParameters, out validatedToken);
jwt = validatedToken as JwtSecurityToken;
if (jwt == null)
_logger.LogDebug(Resources.OIDCH_0020_IdTokenReceived, message.IdToken);
var securityTokenReceivedNotification =
new SecurityTokenReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
throw new InvalidOperationException("Validated Security Token must be a JwtSecurityToken was: " + (validatedToken == null ? "null" : validatedToken.GetType().ToString()));
ProtocolMessage = message
};
await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification);
if (securityTokenReceivedNotification.HandledResponse)
{
_logger.LogInformation(Resources.OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse);
return securityTokenReceivedNotification.AuthenticationTicket;
}
if (securityTokenReceivedNotification.Skipped)
{
_logger.LogInformation(Resources.OIDCH_0009_SecurityTokenReceivedNotificationSkipped);
return null;
}
// Copy and augment to avoid cross request race conditions for updated configurations.
TokenValidationParameters validationParameters = Options.TokenValidationParameters.Clone();
if (_configuration != null)
{
if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer))
{
validationParameters.ValidIssuer = _configuration.Issuer;
}
else if (!string.IsNullOrWhiteSpace(_configuration.Issuer))
{
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(new[] { _configuration.Issuer }) ?? new[] { _configuration.Issuer };
}
validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys;
}
SecurityToken validatedToken = null;
ClaimsPrincipal principal = null;
foreach (var validator in Options.SecurityTokenValidators)
{
if (validator.CanReadToken(message.IdToken))
{
principal = validator.ValidateToken(message.IdToken, validationParameters, out validatedToken);
jwt = validatedToken as JwtSecurityToken;
if (jwt == null)
{
_logger.LogError(Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType());
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType()));
}
}
}
}
if (validatedToken == null)
{
throw new InvalidOperationException("No SecurityTokenValidator found for token: " + openIdConnectMessage.IdToken);
}
ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme);
if (!string.IsNullOrWhiteSpace(openIdConnectMessage.SessionState))
{
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = openIdConnectMessage.SessionState;
}
if (_configuration != null && !string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe))
{
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe;
}
if (Options.UseTokenLifetime)
{
// Override any session persistence to match the token lifetime.
DateTime issued = validatedToken.ValidFrom;
if (issued != DateTime.MinValue)
if (validatedToken == null)
{
ticket.Properties.IssuedUtc = issued;
_logger.LogError(Resources.OIDCH_0011_UnableToValidateToken, message.IdToken);
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0011_UnableToValidateToken, message.IdToken));
}
DateTime expires = validatedToken.ValidTo;
if (expires != DateTime.MinValue)
ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme);
if (!string.IsNullOrWhiteSpace(message.SessionState))
{
ticket.Properties.ExpiresUtc = expires;
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = message.SessionState;
}
ticket.Properties.AllowRefresh = false;
if (_configuration != null && !string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe))
{
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe;
}
// Rename?
if (Options.UseTokenLifetime)
{
DateTime issued = validatedToken.ValidFrom;
if (issued != DateTime.MinValue)
{
ticket.Properties.IssuedUtc = issued;
}
DateTime expires = validatedToken.ValidTo;
if (expires != DateTime.MinValue)
{
ticket.Properties.ExpiresUtc = expires;
}
}
var securityTokenValidatedNotification =
new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
AuthenticationTicket = ticket,
ProtocolMessage = message
};
await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification);
if (securityTokenValidatedNotification.HandledResponse)
{
_logger.LogInformation(Resources.OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse);
return securityTokenValidatedNotification.AuthenticationTicket;
}
if (securityTokenValidatedNotification.Skipped)
{
_logger.LogInformation(Resources.OIDCH_0013_SecurityTokenValidatedNotificationSkipped);
return null;
}
string nonce = jwt.Payload.Nonce;
if (Options.NonceCache != null)
{
// if the nonce cannot be removed, it was used
if (!Options.NonceCache.TryRemoveNonce(nonce))
{
nonce = null;
}
}
else
{
nonce = ReadNonceCookie(nonce);
}
var protocolValidationContext = new OpenIdConnectProtocolValidationContext
{
AuthorizationCode = message.Code,
Nonce = nonce,
};
Options.ProtocolValidator.Validate(jwt, protocolValidationContext);
}
var securityTokenValidatedNotification = new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
if (message.Code != null)
{
AuthenticationTicket = ticket,
ProtocolMessage = openIdConnectMessage
};
_logger.LogDebug(Resources.OIDCH_0014_CodeReceived, message.Code);
if (ticket == null)
{
ticket = new AuthenticationTicket(properties, Options.AuthenticationScheme);
}
await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification);
if (securityTokenValidatedNotification.HandledResponse)
{
return securityTokenValidatedNotification.AuthenticationTicket;
}
if (securityTokenValidatedNotification.Skipped)
{
return null;
}
var protocolValidationContext = new OpenIdConnectProtocolValidationContext
{
AuthorizationCode = openIdConnectMessage.Code,
Nonce = RetrieveNonce(jwt.Payload.Nonce),
};
Options.ProtocolValidator.Validate(jwt, protocolValidationContext);
if (openIdConnectMessage.Code != null)
{
var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options)
{
AuthenticationTicket = ticket,
Code = openIdConnectMessage.Code,
Code = message.Code,
JwtSecurityToken = jwt,
ProtocolMessage = openIdConnectMessage,
ProtocolMessage = message,
RedirectUri = ticket.Properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ?
ticket.Properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : string.Empty,
};
@ -416,11 +489,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification);
if (authorizationCodeReceivedNotification.HandledResponse)
{
_logger.LogInformation(Resources.OIDCH_0015_CodeReceivedNotificationHandledResponse);
return authorizationCodeReceivedNotification.AuthenticationTicket;
}
if (authorizationCodeReceivedNotification.Skipped)
{
_logger.LogInformation(Resources.OIDCH_0016_CodeReceivedNotificationSkipped);
return null;
}
}
@ -429,7 +504,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
}
catch (Exception exception)
{
_logger.LogError("Exception occurred while processing message", exception);
_logger.LogError(Resources.OIDCH_0017_ExceptionOccurredWhileProcessingMessage, exception);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification.
if (Options.RefreshOnIssuerKeyNotFound && exception.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException)))
@ -437,20 +512,23 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
Options.ConfigurationManager.RequestRefresh();
}
var authenticationFailedNotification = new AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage,
Exception = exception
};
var authenticationFailedNotification =
new AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = message,
Exception = exception
};
await Options.Notifications.AuthenticationFailed(authenticationFailedNotification);
if (authenticationFailedNotification.HandledResponse)
{
_logger.LogInformation(Resources.OIDCH_0018_AuthenticationFailedNotificationHandledResponse);
return authenticationFailedNotification.AuthenticationTicket;
}
if (authenticationFailedNotification.Skipped)
{
_logger.LogInformation(Resources.OIDCH_0019_AuthenticationFailedNotificationSkipped);
return null;
}
@ -464,7 +542,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <param name="nonce">the nonce to remember.</param>
/// <remarks><see cref="HttpResponse.Cookies.Append"/>is called to add a cookie with the name: 'OpenIdConnectAuthenticationDefaults.Nonce + <see cref="OpenIdConnectAuthenticationOptions.StringDataFormat.Protect"/>(nonce)'.
/// The value of the cookie is: "N".</remarks>
private void RememberNonce(string nonce)
private void WriteNonceCookie(string nonce)
{
if (string.IsNullOrWhiteSpace(nonce))
{
@ -484,13 +562,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// Searches <see cref="HttpRequest.Cookies"/> for a matching nonce.
/// </summary>
/// <param name="nonceExpectedValue">the nonce that was found in the jwt token.</param>
/// <returns>'nonceExpectedValue' if a cookie is found that matches, null otherwise.</returns>
/// <param name="nonce">the nonce that we are looking for.</param>
/// <returns>echos 'nonce' if a cookie is found that matches, null otherwise.</returns>
/// <remarks>Examine <see cref="HttpRequest.Cookies.Keys"/> that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'.
/// <see cref="OpenIdConnectAuthenticationOptions.StringDataFormat.Unprotect"/> is used to obtain the actual 'nonce'. If the nonce is found, then <see cref="HttpResponse.Cookies.Delete"/> is called.</remarks>
private string RetrieveNonce(string nonceExpectedValue)
private string ReadNonceCookie(string nonce)
{
if (nonceExpectedValue == null)
if (nonce == null)
{
return null;
}
@ -502,7 +580,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
try
{
string nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(OpenIdConnectAuthenticationDefaults.CookieNoncePrefix.Length, nonceKey.Length - OpenIdConnectAuthenticationDefaults.CookieNoncePrefix.Length));
if (nonceDecodedValue == nonceExpectedValue)
if (nonceDecodedValue == nonce)
{
var cookieOptions = new CookieOptions
{
@ -511,7 +589,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
};
Response.Cookies.Delete(nonceKey, cookieOptions);
return nonceExpectedValue;
return nonce;
}
}
catch (Exception ex)

View File

@ -30,9 +30,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// Initializes a <see cref="OpenIdConnectAuthenticationMiddleware"/>
/// </summary>
/// <param name="next">The next middleware in the ASP.NET pipeline to invoke</param>
/// <param name="app">The ASP.NET application</param>
/// <param name="options">Configuration options for the middleware</param>
/// <param name="next">The next middleware in the ASP.NET pipeline to invoke.</param>
/// <param name="dataProtectionProvider"> provider for creating a data protector.</param>
/// <param name="loggerFactory">factory for creating a <see cref="ILogger"/>.</param>
/// <param name="options">a <see cref="IOptions{OpenIdConnectAuthenticationOptions}"/> instance that will supply <see cref="OpenIdConnectAuthenticationOptions"/>
/// if configureOptions is null.</param>
/// <param name="configureOptions">a <see cref="ConfigureOptions{OpenIdConnectAuthenticationOptions}"/> instance that will be passed to an instance of <see cref="OpenIdConnectAuthenticationOptions"/>
/// that is retrieved by calling <see cref="IOptions{OpenIdConnectAuthenticationOptions}.GetNamedOptions(string)"/> where string == <see cref="ConfigureOptions{OpenIdConnectAuthenticationOptions}.Name"/> provides runtime configuration.</param>
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")]
public OpenIdConnectAuthenticationMiddleware(
[NotNull] RequestDelegate next,
@ -40,21 +44,15 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IOptions<ExternalAuthenticationOptions> externalOptions,
[NotNull] IOptions<OpenIdConnectAuthenticationOptions> options,
ConfigureOptions<OpenIdConnectAuthenticationOptions> configureOptions)
ConfigureOptions<OpenIdConnectAuthenticationOptions> configureOptions = null)
: base(next, options, configureOptions)
{
_logger = loggerFactory.CreateLogger<OpenIdConnectAuthenticationMiddleware>();
if (string.IsNullOrEmpty(Options.SignInScheme))
if (string.IsNullOrEmpty(Options.SignInScheme) && !string.IsNullOrEmpty(externalOptions.Options.SignInScheme))
{
Options.SignInScheme = externalOptions.Options.SignInScheme;
}
if (string.IsNullOrWhiteSpace(Options.TokenValidationParameters.AuthenticationType))
{
Options.TokenValidationParameters.AuthenticationType = Options.AuthenticationScheme;
}
if (Options.StateDataFormat == null)
{
var dataProtector = dataProtectionProvider.CreateProtector(
@ -152,7 +150,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
var webRequestHandler = handler as WebRequestHandler;
if (webRequestHandler == null)
{
throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch);
throw new InvalidOperationException(Resources.OIDCH_0102_ExceptionValidatorHandlerMismatch);
}
webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate;
}

View File

@ -52,32 +52,31 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
[SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions.set_Caption(System.String)", Justification = "Not a LOC field")]
public OpenIdConnectAuthenticationOptions(string authenticationScheme)
{
// REVIEW: why was this active by default??
//AuthenticationMode = AuthenticationMode.Active;
AuthenticationScheme = authenticationScheme;
BackchannelTimeout = TimeSpan.FromMinutes(1);
Caption = OpenIdConnectAuthenticationDefaults.Caption;
ProtocolValidator = new OpenIdConnectProtocolValidator();
RefreshOnIssuerKeyNotFound = true;
ResponseMode = OpenIdConnectResponseModes.FormPost;
ResponseType = OpenIdConnectResponseTypes.CodeIdToken;
Scope = OpenIdConnectScopes.OpenIdProfile;
TokenValidationParameters = new TokenValidationParameters();
UseTokenLifetime = true;
}
/// <summary>
/// Gets or sets the expected audience for any received JWT token.
/// </summary>
/// <value>
/// The expected audience for any received JWT token.
/// </value>
public string Audience { get; set; }
/// <summary>
/// Gets or sets the Authority to use when making OpenIdConnect calls.
/// </summary>
public string Authority { get; set; }
/// <summary>
/// An optional constrained path on which to process the authentication callback.
/// If not provided and RedirectUri is available, this value will be generated from RedirectUri.
/// </summary>
/// <remarks>If you set this value, then the <see cref="OpenIdConnectAuthenticationHandler"/> will only listen for posts at this address.
/// If the IdentityProvider does not post to this address, you may end up in a 401 -> IdentityProvider -> Client -> 401 -> ...</remarks>
public PathString CallbackPath { get; set; }
#if DNX451
/// <summary>
/// Gets or sets the a pinned certificate validator to use to validate the endpoints used
@ -112,7 +111,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
if (value <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException("BackchannelTimeout", value, Resources.ArgsException_BackchallelLessThanZero);
throw new ArgumentOutOfRangeException("BackchannelTimeout", value, Resources.OIDCH_0101_BackChallnelLessThanZero);
}
_backchannelTimeout = value;
@ -128,6 +127,14 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
set { Description.Caption = value; }
}
/// <summary>
/// An optional constrained path on which to process the authentication callback.
/// If not provided and RedirectUri is available, this value will be generated from RedirectUri.
/// </summary>
/// <remarks>If you set this value, then the <see cref="OpenIdConnectAuthenticationHandler"/> will only listen for posts at this address.
/// If the IdentityProvider does not post to this address, you may end up in a 401 -> IdentityProvider -> Client -> 401 -> ...</remarks>
public PathString CallbackPath { get; set; }
/// <summary>
/// Gets or sets the 'client_id'.
/// </summary>
@ -145,11 +152,16 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
public OpenIdConnectConfiguration Configuration { get; set; }
/// <summary>
/// The OpenIdConnect protocol http://openid.net/specs/openid-connect-core-1_0.html
/// recommends adding a nonce to a request as a mitigation against replay attacks when requesting id_tokens.
/// By default the runtime uses cookies with unique names generated from a hash of the nonce.
/// Responsible for retrieving, caching, and refreshing the configuration from metadata.
/// If not provided, then one will be created using the MetadataAddress and Backchannel properties.
/// </summary>
public INonceCache NonceCache { get; set; }
public IConfigurationManager<OpenIdConnectConfiguration> ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets a value controlling if the 'CurrentUri' should be used as the 'local redirect' post authentication
/// if AuthenticationProperties.RedirectUri is null or empty.
/// </summary>
public bool DefaultToCurrentUriOnRedirect { get; set; }
/// <summary>
/// Gets or sets the discovery endpoint for obtaining metadata
@ -157,24 +169,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
public string MetadataAddress { get; set; }
/// <summary>
/// Gets or sets the expected audience for any received JWT token.
/// The OpenIdConnect protocol http://openid.net/specs/openid-connect-core-1_0.html
/// recommends adding a nonce to a request as a mitigation against replay attacks when requesting id_tokens.
/// By default the runtime uses cookies with unique names generated from a hash of the nonce.
/// </summary>
/// <value>
/// The expected audience for any received JWT token.
/// </value>
public string Audience { get; set; }
/// <summary>
/// Responsible for retrieving, caching, and refreshing the configuration from metadata.
/// If not provided, then one will be created using the MetadataAddress and Backchannel properties.
/// </summary>
public IConfigurationManager<OpenIdConnectConfiguration> ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic
/// recovery in the event of a signature key rollover. This is enabled by default.
/// </summary>
public bool RefreshOnIssuerKeyNotFound { get; set; }
public INonceCache NonceCache { get; set; }
/// <summary>
/// Gets or sets the <see cref="OpenIdConnectAuthenticationNotifications"/> to notify when processing OpenIdConnect messages.
@ -217,11 +216,22 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
[SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "By Design")]
public string RedirectUri { get; set; }
/// <summary>
/// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic
/// recovery in the event of a signature key rollover. This is enabled by default.
/// </summary>
public bool RefreshOnIssuerKeyNotFound { get; set; }
/// <summary>
/// Gets or sets the 'resource'.
/// </summary>
public string Resource { get; set; }
/// <summary>
/// Gets or sets the 'response_mode'.
/// </summary>
public string ResponseMode { get; private set; }
/// <summary>
/// Gets or sets the 'response_type'.
/// </summary>
@ -233,10 +243,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
public string Scope { get; set; }
/// <summary>
/// Gets or sets the authentication scheme corresponding to the middleware
/// responsible of persisting user's identity after a successful authentication.
/// This value typically corresponds to a cookie middleware registered in the Startup class.
/// When omitted, <see cref="ExternalAuthenticationOptions.SignInScheme"/> is used as a fallback value.
/// Gets or sets the SignInScheme which will be used to set the <see cref="System.Security.Claims.ClaimsIdentity.AuthenticationType"/>.
/// </summary>
public string SignInScheme { get; set; }

View File

@ -15,7 +15,7 @@ namespace Microsoft.Framework.DependencyInjection
{
public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] Action<OpenIdConnectAuthenticationOptions> configure)
{
return services.ConfigureOpenIdConnectAuthentication(configure, optionsName: "");
return ConfigureOpenIdConnectAuthentication(services, configure, null);
}
public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] Action<OpenIdConnectAuthenticationOptions> configure, string optionsName)
@ -25,7 +25,7 @@ namespace Microsoft.Framework.DependencyInjection
public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config)
{
return services.ConfigureOpenIdConnectAuthentication(config, optionsName: "");
return ConfigureOpenIdConnectAuthentication(services, config, null);
}
public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config, string optionsName)

View File

@ -1,18 +1,9 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.34014
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.AspNet.Authentication.OpenIdConnect {
using System;
// <auto-generated />
namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
using System.Globalization;
using System.Reflection;
using System.Resources;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
@ -24,78 +15,337 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
internal Resources()
{
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Owin.Security.OpenIdConnect.Resources", IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly);
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if (object.ReferenceEquals(resourceMan, null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Authentication.OpenIdConnect.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set {
set
{
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to BackchannelTimeout cannot be less or equal to TimeSpan.Zero..
/// OIDCH_0101: BackchannelTimeout cannot be less or equal to TimeSpan.Zero.
/// </summary>
internal static string ArgsException_BackchallelLessThanZero {
get {
return ResourceManager.GetString("ArgsException_BackchallelLessThanZero", resourceCulture);
}
internal static string OIDCH_0101_BackChallnelLessThanZero
{
get { return ResourceManager.GetString("OIDCH_0101_BackChallnelLessThanZero"); }
}
/// <summary>
/// Looks up a localized string similar to &quot;OpenIdConnectMessage.Error was not null, indicating an error. Error: &apos;{0}&apos;. Error_Description (may be empty): &apos;{1}&apos;. Error_Uri (may be empty): &apos;{2}&apos;.&quot;.
/// OIDCH0102: An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
/// </summary>
internal static string Exception_OpenIdConnectMessageError {
get {
return ResourceManager.GetString("Exception_OpenIdConnectMessageError", resourceCulture);
}
internal static string OIDCH_0102_ExceptionValidatorHandlerMismatch
{
get { return ResourceManager.GetString("OIDCH_0102_Exception_ValidatorHandlerMismatch"); }
}
/// <summary>
/// Looks up a localized string similar to OIDC_20001: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: &apos;{0}&apos;..
/// OIDCH_0051: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: '{0}'.
/// </summary>
internal static string Exception_RedirectUri_LogoutQueryString_IsNotWellFormed {
get {
return ResourceManager.GetString("Exception_RedirectUri_LogoutQueryString_IsNotWellFormed", resourceCulture);
}
internal static string OIDCH_0051_RedirectUriLogoutIsNotWellFormed
{
get { return ResourceManager.GetString("OIDCH_0051_RedirectUriLogoutIsNotWellFormed"); }
}
/// <summary>
/// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler..
/// OIDCH_0026: Entering: '{0}'
/// </summary>
internal static string Exception_ValidatorHandlerMismatch {
get {
return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture);
}
internal static string OIDCH_0026_ApplyResponseChallengeAsync
{
get { return ResourceManager.GetString("OIDCH_0026_ApplyResponseChallengeAsync"); }
}
/// <summary>
/// OIDCH_0027: converted 401 to 403.
/// </summary>
internal static string OIDCH_0027_401_ConvertedTo_403
{
get { return ResourceManager.GetString("OIDCH_0027_401_ConvertedTo_403"); }
}
/// <summary>
/// OIDCH_0028: Response.StatusCode != 401, StatusCode: '{0}'."
/// </summary>
internal static string OIDCH_0028_StatusCodeNot401
{
get { return ResourceManager.GetString("OIDCH_0028_StatusCodeNot401"); }
}
/// <summary>
/// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication
/// </summary>
internal static string OIDCH_0029_ChallengeContextEqualsNull
{
get { return ResourceManager.GetString("OIDCH_0029_ChallengeContextEqualsNull"); }
}
/// <summary>
/// OIDCH_0030: using properties.RedirectUri for 'local redirect' post authentication: '{0}'.
/// </summary>
internal static string OIDCH_0030_Using_Properties_RedirectUri
{
get { return ResourceManager.GetString("OIDCH_0030_Using_Properties_RedirectUri"); }
}
/// <summary>
/// OIDCH_0031: using Options.RedirectUri for 'redirect_uri': '{0}'.
/// </summary>
internal static string OIDCH_0031_Using_Options_RedirectUri
{
get { return ResourceManager.GetString("OIDCH_0031_Using_Options_RedirectUri"); }
}
/// <summary>
/// OIDCH_0032: using the CurrentUri for 'local redirect' post authentication: '{0}'.
/// </summary>
internal static string OIDCH_0032_UsingCurrentUriRedirectUri
{
get { return ResourceManager.GetString("OIDCH_0032_UsingCurrentUriRedirectUri"); }
}
/// <summary>
/// OIDCH_0033: ProtocolValidator.RequireNonce == true. Options.NonceCache.TryAddNonce returned false. This usually indicates the nonce is not unique or has been used. The nonce is: '{0}'.
/// </summary>
internal static string OIDCH_0033_TryAddNonceFailed
{
get { return ResourceManager.GetString("OIDCH_0033_TryAddNonceFailed"); }
}
/// <summary>
/// OIDCH_0034: redirectToIdentityProviderNotification.HandledResponse
/// </summary>
internal static string OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse
{
get { return ResourceManager.GetString("OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse"); }
}
/// <summary>
/// OIDCH_0035: redirectToIdentityProviderNotification.Skipped
/// </summary>
internal static string OIDCH_0035_RedirectToIdentityProviderNotificationSkipped
{
get { return ResourceManager.GetString("OIDCH_0035_RedirectToIdentityProviderNotificationSkipped"); }
}
/// <summary>
/// OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}", (redirectUri ?? "null"))
/// </summary>
internal static string OIDCH_0036_UriIsNotWellFormed
{
get { return ResourceManager.GetString("OIDCH_0036_UriIsNotWellFormed"); }
}
/// <summary>
/// OIDCH_0000: Entering: '{0}'.
/// </summary>
internal static string OIDCH_0000_AuthenticateCoreAsync
{
get { return ResourceManager.GetString("OIDCH_0000_AuthenticateCoreAsync"); }
}
/// <summary>
/// OIDCH_0001: MessageReceived: '{0}'.
/// </summary>
internal static string OIDCH_0001_MessageReceived
{
get { return ResourceManager.GetString("OIDCH_0001_MessageReceived"); }
}
/// <summary>
/// OIDCH_0001: MessageReceived: '{0}'.
/// </summary>
internal static string FormatOIDCH_0001_MessageReceived(object p0)
{
return string.Format(CultureInfo.CurrentCulture, ResourceManager.GetString("OIDCH_0001_MessageReceived"), p0);
}
/// <summary>
/// OIDCH_0002: messageReceivedNotification.HandledResponse
/// </summary>
internal static string OIDCH_0002_MessageReceivedNotificationHandledResponse
{
get { return ResourceManager.GetString("OIDCH_0002_MessageReceivedNotificationHandledResponse"); }
}
/// <summary>
/// OIDCH_0003: messageReceivedNotification.Skipped
/// </summary>
internal static string OIDCH_0003_MessageReceivedNotificationSkipped
{
get { return ResourceManager.GetString("OIDCH_0003_MessageReceivedNotificationSkipped"); }
}
/// <summary>
/// OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or whitespace. State is required to process the message.
/// </summary>
internal static string OIDCH_0004_MessageStateIsNullOrWhiteSpace
{
get { return ResourceManager.GetString("OIDCH_0004_MessageStateIsNullOrWhiteSpace"); }
}
/// <summary>
/// OIDCH_0005: unable to unprotect the message.State
/// </summary>
internal static string OIDCH_0005_MessageStateIsInvalid
{
get { return ResourceManager.GetString("OIDCH_0005_MessageStateIsInvalid"); }
}
/// <summary>
/// OIDCH_0006_MessageErrorNotNull: '{0}'.
/// </summary>
internal static string OIDCH_0006_MessageErrorNotNull
{
get { return ResourceManager.GetString("OIDCH_0006_MessageErrorNotNull"); }
}
/// <summary>
/// OIDCH_0007: updating configuration
/// </summary>
internal static string OIDCH_0007_UpdatingConfiguration
{
get { return ResourceManager.GetString("OIDCH_0007_UpdatingConfiguration"); }
}
/// <summary>
/// OIDCH_0008: securityTokenReceivedNotification.HandledResponse
/// </summary>
internal static string OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse
{
get { return ResourceManager.GetString("OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse"); }
}
/// <summary>
/// OIDCH_0009: securityTokenReceivedNotification.Skipped
/// </summary>
internal static string OIDCH_0009_SecurityTokenReceivedNotificationSkipped
{
get { return ResourceManager.GetString("OIDCH_0009_SecurityTokenReceivedNotificationSkipped:"); }
}
/// <summary>
/// OIDCH_0010: Validated Security Token must be a JwtSecurityToken was: '{0}'.
/// </summary>
internal static string OIDCH_0010_ValidatedSecurityTokenNotJwt
{
get { return ResourceManager.GetString("OIDCH_0010_ValidatedSecurityTokenNotJwt"); }
}
/// <summary>
/// OIDCH_0011: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: {0}."
/// </summary>
internal static string OIDCH_0011_UnableToValidateToken
{
get { return ResourceManager.GetString("OIDCH_0011_UnableToValidateToken"); }
}
/// <summary>
/// OIDCH_0012: securityTokenValidatedNotification.HandledResponse
/// </summary>
internal static string OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse
{
get { return ResourceManager.GetString("OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse"); }
}
/// <summary>
/// OIDCH_0013: securityTokenValidatedNotification.Skipped
/// </summary>
internal static string OIDCH_0013_SecurityTokenValidatedNotificationSkipped
{
get { return ResourceManager.GetString("OIDCH_0013_SecurityTokenValidatedNotificationSkipped"); }
}
/// <summary>
/// OIDCH_0014: 'code' received: '{0}'
/// </summary>
internal static string OIDCH_0014_CodeReceived
{
get { return ResourceManager.GetString("OIDCH_0014_CodeReceived"); }
}
/// <summary>
/// OIDCH_0015: codeReceivedNotification.HandledResponse")
/// </summary>
internal static string OIDCH_0015_CodeReceivedNotificationHandledResponse
{
get { return ResourceManager.GetString("OIDCH_0015_CodeReceivedNotificationHandledResponse"); }
}
/// <summary>
/// OIDCH_0016: codeReceivedNotification.Skipped
/// </summary>
internal static string OIDCH_0016_CodeReceivedNotificationSkipped
{
get { return ResourceManager.GetString("OIDCH_0016_CodeReceivedNotificationSkipped"); }
}
/// <summary>
/// OIDCH_0017: Exception occurred while processing message
/// </summary>
internal static string OIDCH_0017_ExceptionOccurredWhileProcessingMessage
{
get { return ResourceManager.GetString("OIDCH_0017_ExceptionOccurredWhileProcessingMessage"); }
}
/// <summary>
/// OIDCH_0018: authenticationFailedNotification.HandledResponse
/// </summary>
internal static string OIDCH_0018_AuthenticationFailedNotificationHandledResponse
{
get { return ResourceManager.GetString("OIDCH_0018_AuthenticationFailedNotificationHandledResponse"); }
}
/// <summary>
/// OIDCH_0019: authenticationFailedNotification.Skipped
/// </summary>
internal static string OIDCH_0019_AuthenticationFailedNotificationSkipped
{
get { return ResourceManager.GetString("OIDCH_0019_AuthenticationFailedNotificationSkipped"); }
}
/// <summary>
/// OIDCH_0020: 'id_token' received: '{0}'
/// </summary>
internal static string OIDCH_0020_IdTokenReceived
{
get { return ResourceManager.GetString("OIDCH_0020_IdTokenReceived"); }
}
}
}

View File

@ -117,16 +117,109 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ArgsException_BackchallelLessThanZero" xml:space="preserve">
<value>BackchannelTimeout cannot be less or equal to TimeSpan.Zero.</value>
<data name="OIDCH_0101_BackChallnelLessThanZero" xml:space="preserve">
<value>OIDCH_0101: BackchannelTimeout cannot be less or equal to TimeSpan.Zero.</value>
</data>
<data name="Exception_OpenIdConnectMessageError" xml:space="preserve">
<value>"OpenIdConnectMessage.Error was not null, indicating an error. Error: '{0}'. Error_Description (may be empty): '{1}'. Error_Uri (may be empty): '{2}'."</value>
<data name="OIDCH_0102_Exception_ValidatorHandlerMismatch" xml:space="preserve">
<value>OIDCH_0102: An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
</data>
<data name="Exception_RedirectUri_LogoutQueryString_IsNotWellFormed" xml:space="preserve">
<value>OIDC_20001: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: '{0}'.</value>
<data name="OIDCH_0051_RedirectUriLogoutIsNotWellFormed" xml:space="preserve">
<value>OIDC_0051: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: '{0}'.</value>
</data>
<data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
<value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
<data name="OIDCH_0026_ApplyResponseChallengeAsync" xml:space="preserve">
<value>OIDCH_0026: Entering: '{0}'</value>
</data>
</root>
<data name="OIDCH_0027_401_ConvertedTo_403" xml:space="preserve">
<value>OIDCH_0027: converted 401 to 403.</value>
</data>
<data name="OIDCH_0028_StatusCodeNot401" xml:space="preserve">
<value>OIDCH_0028: Response.StatusCode != 401, StatusCode: '{0}'.</value>
</data>
<data name="OIDCH_0029_ChallengContextEqualsNull" xml:space="preserve">
<value>OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication</value>
</data>
<data name="OIDCH_0030_Using_Properties_RedirectUri" xml:space="preserve">
<value>OIDCH_0030: using properties.RedirectUri for 'local redirect' post authentication: '{0}'.</value>
</data>
<data name="OIDCH_0031_Using_Options_RedirectUri" xml:space="preserve">
<value>OIDCH_0031: using Options.RedirectUri for 'redirect_uri': '{0}'.</value>
</data>
<data name="OIDCH_0032_UsingCurrentUriRedirectUri" xml:space="preserve">
<value>OIDCH_0032: using the CurrentUri for 'local redirect' post authentication: '{0}'.</value>
</data>
<data name="OIDCH_0033_TryAddNonceFailed" xml:space="preserve">
<value>OIDCH_0033: ProtocolValidator.RequireNonce == true. Options.NonceCache.TryAddNonce returned false. This usually indicates the nonce is not unique or has been used. The nonce is: '{0}'.</value>
</data>
<data name="OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse" xml:space="preserve">
<value>OIDCH_0034: redirectToIdentityProviderNotification.HandledResponse</value>
</data>
<data name="OIDCH_0035_RedirectToIdentityProviderNotificationSkipped" xml:space="preserve">
<value>OIDCH_0035: redirectToIdentityProviderNotification.Skipped</value>
</data>
<data name="OIDCH_0036_UriIsNotWellFormed" xml:space="preserve">
<value>OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}", (redirectUri ?? "null"))</value>
</data>
<data name="OIDCH_0000_AuthenticateCoreAsync" xml:space="preserve">
<value>OIDCH_0000: Entering: '{0}'.</value>
</data>
<data name="OIDCH_0001_MessageReceived" xml:space="preserve">
<value>OIDCH_0001: MessageReceived: '{0}'.</value>
</data>
<data name="OIDCH_0002_MessageReceivedNotificationHandledResponse" xml:space="preserve">
<value>OIDCH_0002: messageReceivedNotification.HandledResponse</value>
</data>
<data name="OIDCH_0003_MessageReceivedNotificationSkipped" xml:space="preserve">
<value>OIDCH_0003: messageReceivedNotification.Skipped</value>
</data>
<data name="OIDCH_0004_MessageStateIsNullOrWhiteSpace" xml:space="preserve">
<value>OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or whitespace. State is required to process the message.</value>
</data>
<data name="OIDCH_0005_MessageStateIsInValid" xml:space="preserve">
<value>OIDCH_0005: unable to unprotect the message.State</value>
</data>
<data name="OIDCH_0006_MessageErrorNotNull" xml:space="preserve">
<value>OIDCH_0006_MessageErrorNotNull: '{0}'.</value>
</data>
<data name="OIDCH_0007_UpdatingConfiguration" xml:space="preserve">
<value>OIDCH_0007: updating configuration</value>
</data>
<data name="OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse" xml:space="preserve">
<value>OIDCH_0008: securityTokenReceivedNotification.HandledResponse</value>
</data>
<data name="OIDCH_0009_SecurityTokenReceivedNotificationSkipped:" xml:space="preserve">
<value>OIDCH_0009: securityTokenReceivedNotification.Skipped</value>
</data>
<data name="OIDCH_0010_ValidatedSecurityTokenNotJwt" xml:space="preserve">
<value>OIDCH_0010: Validated Security Token must be a JwtSecurityToken was: '{0}'.</value>
</data>
<data name="OIDCH_0011_UnableToValidateToken" xml:space="preserve">
<value>OIDCH_0011: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: {0}."</value>
</data>
<data name="OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse" xml:space="preserve">
<value>OIDCH_0012: securityTokenValidatedNotification.HandledResponse</value>
</data>
<data name="OIDCH_0013_SecurityTokenValidatedNotificationSkipped" xml:space="preserve">
<value>OIDCH_0013: securityTokenValidatedNotification.Skipped</value>
</data>
<data name="OIDCH_0014_CodeReceived" xml:space="preserve">
<value>OIDCH_0014: 'code' received: '{0}'</value>
</data>
<data name="OIDCH_0015_CodeReceivedNotificationHandledResponse" xml:space="preserve">
<value>OIDCH_0015: codeReceivedNotification.HandledResponse</value>
</data>
<data name="OIDCH_0016_CodeReceivedNotificationSkipped" xml:space="preserve">
<value>OIDCH_0016: codeReceivedNotification.Skipped</value>
</data>
<data name="OIDCH_0017: Exception occurred while processing message" xml:space="preserve">
<value>OIDCH_0017: Exception occurred while processing message</value>
</data>
<data name="OIDCH_0018_AuthenticationFailedNotificationHandledResponse" xml:space="preserve">
<value>OIDCH_0018: authenticationFailedNotification.HandledResponse</value>
</data>
<data name="OIDCH_0019_AuthenticationNotificationFailedSkipped" xml:space="preserve">
<value>OIDCH_0019: authenticationFailedNotification.Skipped</value>
</data>
<data name="OIDCH_0020_IdTokenReceived" xml:space="preserve">
<value>OIDCH_0020: 'id_token' received: '{0}'</value>
</data>
</root>

View File

@ -0,0 +1,768 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// this controls if the logs are written to the console.
// they can be reviewed for general content.
//#define _Verbose
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Authentication.Notifications;
using Microsoft.AspNet.Authentication.OpenIdConnect;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.DataProtection;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Authentication;
using Microsoft.AspNet.TestHost;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Microsoft.IdentityModel.Protocols;
using Shouldly;
using Xunit;
namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
{
/// <summary>
/// These tests are designed to test OpenIdConnectAuthenticationHandler.
/// </summary>
public class OpenIdConnectHandlerTests
{
static List<LogEntry> CompleteLogEntries;
static Dictionary<string, LogLevel> LogEntries;
static OpenIdConnectHandlerTests()
{
LogEntries =
new Dictionary<string, LogLevel>()
{
{ "OIDCH_0000:", LogLevel.Debug },
{ "OIDCH_0001:", LogLevel.Debug },
{ "OIDCH_0002:", LogLevel.Information },
{ "OIDCH_0003:", LogLevel.Information },
{ "OIDCH_0004:", LogLevel.Error },
{ "OIDCH_0005:", LogLevel.Error },
{ "OIDCH_0006:", LogLevel.Error },
{ "OIDCH_0007:", LogLevel.Error },
{ "OIDCH_0008:", LogLevel.Debug },
{ "OIDCH_0009:", LogLevel.Debug },
{ "OIDCH_0010:", LogLevel.Error },
{ "OIDCH_0011:", LogLevel.Error },
{ "OIDCH_0012:", LogLevel.Debug },
{ "OIDCH_0013:", LogLevel.Debug },
{ "OIDCH_0014:", LogLevel.Debug },
{ "OIDCH_0015:", LogLevel.Debug },
{ "OIDCH_0016:", LogLevel.Debug },
{ "OIDCH_0017:", LogLevel.Error },
{ "OIDCH_0018:", LogLevel.Debug },
{ "OIDCH_0019:", LogLevel.Debug },
{ "OIDCH_0020:", LogLevel.Debug },
{ "OIDCH_0026:", LogLevel.Error },
};
BuildLogEntryList();
}
/// <summary>
/// Builds the complete list of log entries that are available in the runtime.
/// </summary>
private static void BuildLogEntryList()
{
CompleteLogEntries = new List<LogEntry>();
foreach (var entry in LogEntries)
{
CompleteLogEntries.Add(new LogEntry { State = entry.Key, Level = entry.Value });
}
}
/// <summary>
/// Sanity check that logging is filtering, hi / low water marks are checked
/// </summary>
[Fact]
public void LoggingLevel()
{
var logger = new CustomLogger(LogLevel.Debug);
logger.IsEnabled(LogLevel.Critical).ShouldBe<bool>(true);
logger.IsEnabled(LogLevel.Debug).ShouldBe<bool>(true);
logger.IsEnabled(LogLevel.Error).ShouldBe<bool>(true);
logger.IsEnabled(LogLevel.Information).ShouldBe<bool>(true);
logger.IsEnabled(LogLevel.Verbose).ShouldBe<bool>(true);
logger.IsEnabled(LogLevel.Warning).ShouldBe<bool>(true);
logger = new CustomLogger(LogLevel.Critical);
logger.IsEnabled(LogLevel.Critical).ShouldBe<bool>(true);
logger.IsEnabled(LogLevel.Debug).ShouldBe<bool>(false);
logger.IsEnabled(LogLevel.Error).ShouldBe<bool>(false);
logger.IsEnabled(LogLevel.Information).ShouldBe<bool>(false);
logger.IsEnabled(LogLevel.Verbose).ShouldBe<bool>(false);
logger.IsEnabled(LogLevel.Warning).ShouldBe<bool>(false);
}
/// <summary>
/// Test <see cref="OpenIdConnectAuthenticationHandler.AuthenticateCoreAsync"/> produces expected logs.
/// Each call to 'RunVariation' is configured with an <see cref="OpenIdConnectAuthenticationOptions"/> and <see cref="OpenIdConnectMessage"/>.
/// The list of expected log entries is checked and any errors reported.
/// <see cref="CustomLoggerFactory"/> captures the logs so they can be prepared.
/// </summary>
/// <returns></returns>
[Fact]
public async Task AuthenticateCore()
{
//System.Diagnostics.Debugger.Launch();
var propertiesFormatter = new AuthenticationPropertiesFormater();
var protectedProperties = propertiesFormatter.Protect(new AuthenticationProperties());
var state = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + Uri.EscapeDataString(protectedProperties);
var code = Guid.NewGuid().ToString();
var message =
new OpenIdConnectMessage
{
Code = code,
State = state,
};
var errors = new Dictionary<string, List<Tuple<LogEntry, LogEntry>>>();
var logsEntriesExpected = new int[] { 0, 1, 7, 14, 15 };
await RunVariation(LogLevel.Debug, message, CodeReceivedHandledOptions, errors, logsEntriesExpected);
logsEntriesExpected = new int[] { 0, 1, 7, 14, 16 };
await RunVariation(LogLevel.Debug, message, CodeReceivedSkippedOptions, errors, logsEntriesExpected);
logsEntriesExpected = new int[] { 0, 1, 7, 14 };
await RunVariation(LogLevel.Debug, message, DefaultOptions, errors, logsEntriesExpected);
// each message below should return before processing the idtoken
message.IdToken = "invalid_token";
logsEntriesExpected = new int[] { 0, 1, 2 };
await RunVariation(LogLevel.Debug, message, MessageReceivedHandledOptions, errors, logsEntriesExpected);
logsEntriesExpected = new int[]{ 2 };
await RunVariation(LogLevel.Information, message, MessageReceivedHandledOptions, errors, logsEntriesExpected);
logsEntriesExpected = new int[] { 0, 1, 3 };
await RunVariation(LogLevel.Debug, message, MessageReceivedSkippedOptions, errors, logsEntriesExpected);
logsEntriesExpected = new int[] { 3 };
await RunVariation(LogLevel.Information, message, MessageReceivedSkippedOptions, errors, logsEntriesExpected);
logsEntriesExpected = new int[] {0, 1, 7, 20, 8 };
await RunVariation(LogLevel.Debug, message, SecurityTokenReceivedHandledOptions, errors, logsEntriesExpected);
logsEntriesExpected = new int[] {0, 1, 7, 20, 9 };
await RunVariation(LogLevel.Debug, message, SecurityTokenReceivedSkippedOptions, errors, logsEntriesExpected);
#if _Verbose
Console.WriteLine("\n ===== \n");
DisplayErrors(errors);
#endif
errors.Count.ShouldBe(0);
}
/// <summary>
/// Tests that <see cref="OpenIdConnectAuthenticationHandler"/> processes a messaage as expected.
/// The test runs two independant paths: Using <see cref="ConfigureOptions{TOptions}"/> and <see cref="IOptions{TOptions}"/>
/// </summary>
/// <param name="logLevel"><see cref="LogLevel"/> for this variation</param>
/// <param name="message">the <see cref="OpenIdConnectMessage"/> that has arrived</param>
/// <param name="action">the <see cref="OpenIdConnectAuthenticationOptions"/> delegate used for setting the options.</param>
/// <param name="errors">container for propogation of errors.</param>
/// <param name="logsEntriesExpected">the expected log entries</param>
/// <returns>a Task</returns>
private async Task RunVariation(LogLevel logLevel, OpenIdConnectMessage message, Action<OpenIdConnectAuthenticationOptions> action, Dictionary<string, List<Tuple<LogEntry, LogEntry>>> errors, int[] logsEntriesExpected)
{
var expectedLogs = PopulateLogEntries(logsEntriesExpected);
string variation = action.Method.ToString().Substring(5, action.Method.ToString().IndexOf('(') - 5);
#if _Verbose
Console.WriteLine(Environment.NewLine + "=====" + Environment.NewLine + "Variation: " + variation + ", LogLevel: " + logLevel.ToString() + Environment.NewLine + Environment.NewLine + "Expected Logs: ");
DisplayLogs(expectedLogs);
Console.WriteLine(Environment.NewLine + "Logs using ConfigureOptions:");
#endif
var form = new FormUrlEncodedContent(message.Parameters);
var loggerFactory = new CustomLoggerFactory(logLevel);
var server = CreateServer(new CustomConfigureOptions(action), loggerFactory);
await server.CreateClient().PostAsync("http://localhost", form);
CheckLogs(variation + ":ConfigOptions", loggerFactory.Logger.Logs, expectedLogs, errors);
#if _Verbose
Console.WriteLine(Environment.NewLine + "Logs using IOptions:");
#endif
form = new FormUrlEncodedContent(message.Parameters);
loggerFactory = new CustomLoggerFactory(logLevel);
server = CreateServer(new Options(action), loggerFactory);
await server.CreateClient().PostAsync("http://localhost", form);
CheckLogs(variation + ":IOptions", loggerFactory.Logger.Logs, expectedLogs, errors);
}
/// <summary>
/// Populates a list of expected log entries for a test variation.
/// </summary>
/// <param name="items">the index for the <see cref="LogEntry"/> in CompleteLogEntries of interest.</param>
/// <returns>a <see cref="List{LogEntry}"/> that represents the expected entries for a test variation.</returns>
private List<LogEntry> PopulateLogEntries(int[] items)
{
var entries = new List<LogEntry>();
foreach(var item in items)
{
entries.Add(CompleteLogEntries[item]);
}
return entries;
}
private void DisplayLogs(List<LogEntry> logs)
{
foreach (var logentry in logs)
{
Console.WriteLine(logentry.ToString());
}
}
private void DisplayErrors(Dictionary<string, List<Tuple<LogEntry, LogEntry>>> errors)
{
if (errors.Count > 0)
{
foreach (var error in errors)
{
Console.WriteLine("Error in Variation: " + error.Key);
foreach (var logError in error.Value)
{
Console.WriteLine("*Captured*, *Expected* : *" + (logError.Item1?.ToString() ?? "null") + "*, *" + (logError.Item2?.ToString() ?? "null") + "*");
}
Console.WriteLine(Environment.NewLine);
}
}
}
/// <summary>
/// Adds to errors if a variation if any are found.
/// </summary>
/// <param name="variation">if this has been seen before, errors will be appended, test results are easier to understand if this is unique.</param>
/// <param name="capturedLogs">these are the logs the runtime generated</param>
/// <param name="expectedLogs">these are the errors that were expected</param>
/// <param name="errors">the dictionary to record any errors</param>
private void CheckLogs(string variation, List<LogEntry> capturedLogs, List<LogEntry> expectedLogs, Dictionary<string, List<Tuple<LogEntry, LogEntry>>> errors)
{
var localErrors = new List<Tuple<LogEntry, LogEntry>>();
if (capturedLogs.Count >= expectedLogs.Count)
{
for (int i = 0; i < capturedLogs.Count; i++)
{
if (i + 1 > expectedLogs.Count)
{
localErrors.Add(new Tuple<LogEntry, LogEntry>(capturedLogs[i], null));
}
else
{
if (!TestUtilities.AreEqual<LogEntry>(capturedLogs[i], expectedLogs[i]))
{
localErrors.Add(new Tuple<LogEntry, LogEntry>(capturedLogs[i], expectedLogs[i]));
}
}
}
}
else
{
for (int i = 0; i < expectedLogs.Count; i++)
{
if (i + 1 > capturedLogs.Count)
{
localErrors.Add(new Tuple<LogEntry, LogEntry>(null, expectedLogs[i]));
}
else
{
if (!TestUtilities.AreEqual<LogEntry>(expectedLogs[i], capturedLogs[i]))
{
localErrors.Add(new Tuple<LogEntry, LogEntry>(capturedLogs[i], expectedLogs[i]));
}
}
}
}
if (localErrors.Count != 0)
{
if (errors.ContainsKey(variation))
{
foreach (var error in localErrors)
{
errors[variation].Add(error);
}
}
else
{
errors[variation] = localErrors;
}
}
}
#region Configure Options
private static void CodeReceivedHandledOptions(OpenIdConnectAuthenticationOptions options)
{
DefaultOptions(options);
options.Notifications =
new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = (notification) =>
{
notification.HandleResponse();
return Task.FromResult<object>(null);
}
};
}
private static void CodeReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options)
{
DefaultOptions(options);
options.Notifications =
new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = (notification) =>
{
notification.SkipToNextMiddleware();
return Task.FromResult<object>(null);
}
};
}
private static void DefaultOptions(OpenIdConnectAuthenticationOptions options)
{
options.AuthenticationScheme = "OpenIdConnectHandlerTest";
options.ConfigurationManager = ConfigurationManager.DefaultStaticConfigurationManager;
options.StateDataFormat = new AuthenticationPropertiesFormater();
}
private static void MessageReceivedHandledOptions(OpenIdConnectAuthenticationOptions options)
{
DefaultOptions(options);
options.Notifications =
new OpenIdConnectAuthenticationNotifications
{
MessageReceived = (notification) =>
{
notification.HandleResponse();
return Task.FromResult<object>(null);
}
};
}
private static void MessageReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options)
{
DefaultOptions(options);
options.Notifications =
new OpenIdConnectAuthenticationNotifications
{
MessageReceived = (notification) =>
{
notification.SkipToNextMiddleware();
return Task.FromResult<object>(null);
}
};
}
private static void SecurityTokenReceivedHandledOptions(OpenIdConnectAuthenticationOptions options)
{
DefaultOptions(options);
options.Notifications =
new OpenIdConnectAuthenticationNotifications
{
SecurityTokenReceived = (notification) =>
{
notification.HandleResponse();
return Task.FromResult<object>(null);
}
};
}
private static void SecurityTokenReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options)
{
DefaultOptions(options);
options.Notifications =
new OpenIdConnectAuthenticationNotifications
{
SecurityTokenReceived = (notification) =>
{
notification.SkipToNextMiddleware();
return Task.FromResult<object>(null);
}
};
}
private static void SecurityTokenValidatedHandledOptions(OpenIdConnectAuthenticationOptions options)
{
DefaultOptions(options);
options.Notifications =
new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = (notification) =>
{
notification.HandleResponse();
return Task.FromResult<object>(null);
}
};
}
private static void SecurityTokenValidatedSkippedOptions(OpenIdConnectAuthenticationOptions options)
{
DefaultOptions(options);
options.Notifications =
new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = (notification) =>
{
notification.SkipToNextMiddleware();
return Task.FromResult<object>(null);
}
};
}
#endregion
private static TestServer CreateServer(IOptions<OpenIdConnectAuthenticationOptions> options, ILoggerFactory loggerFactory)
{
return TestServer.Create(
app =>
{
app.UseCustomOpenIdConnectAuthentication(options, loggerFactory);
app.Use(async (context, next) =>
{
await next();
});
},
services =>
{
services.AddDataProtection();
}
);
}
private static TestServer CreateServer(CustomConfigureOptions configureOptions, ILoggerFactory loggerFactory)
{
return TestServer.Create(
app =>
{
app.UseCustomOpenIdConnectAuthentication(configureOptions, loggerFactory);
app.Use(async (context, next) =>
{
await next();
});
},
services =>
{
services.AddDataProtection();
}
);
}
}
/// <summary>
/// Extension specifies <see cref="CustomOpenIdConnectAuthenticationMiddleware"/> as the middleware.
/// </summary>
public static class OpenIdConnectAuthenticationExtensions
{
/// <summary>
/// Adds the <see cref="OpenIdConnectAuthenticationMiddleware"/> into the ASP.NET runtime.
/// </summary>
/// <param name="app">The application builder</param>
/// <param name="customConfigureOption">Options which control the processing of the OpenIdConnect protocol and token validation.</param>
/// <param name="loggerFactory">custom loggerFactory</param>
/// <returns>The application builder</returns>
public static IApplicationBuilder UseCustomOpenIdConnectAuthentication(this IApplicationBuilder app, CustomConfigureOptions customConfigureOption, ILoggerFactory loggerFactory)
{
return app.UseMiddleware<CustomOpenIdConnectAuthenticationMiddleware>(customConfigureOption, loggerFactory);
}
/// <summary>
/// Adds the <see cref="OpenIdConnectAuthenticationMiddleware"/> into the ASP.NET runtime.
/// </summary>
/// <param name="app">The application builder</param>
/// <param name="options">Options which control the processing of the OpenIdConnect protocol and token validation.</param>
/// <param name="loggerFactory">custom loggerFactory</param>
/// <returns>The application builder</returns>
public static IApplicationBuilder UseCustomOpenIdConnectAuthentication(this IApplicationBuilder app, IOptions<OpenIdConnectAuthenticationOptions> options, ILoggerFactory loggerFactory)
{
return app.UseMiddleware<CustomOpenIdConnectAuthenticationMiddleware>(options, loggerFactory);
}
}
public class OpenIdConnectAuthenticationContext : IAuthenticateContext
{
public OpenIdConnectAuthenticationContext(string scheme = null)
{
AuthenticationScheme = scheme ?? OpenIdConnectAuthenticationDefaults.AuthenticationScheme;
}
public string AuthenticationScheme
{
get;
set;
}
public void Authenticated(ClaimsPrincipal principal, IDictionary<string, string> properties, IDictionary<string, object> description)
{
}
public void NotAuthenticated()
{
}
}
/// <summary>
/// Provides a Facade over IOptions
/// </summary>
public class Options : IOptions<OpenIdConnectAuthenticationOptions>
{
OpenIdConnectAuthenticationOptions _options;
public Options(Action<OpenIdConnectAuthenticationOptions> action)
{
_options = new OpenIdConnectAuthenticationOptions();
action(_options);
}
OpenIdConnectAuthenticationOptions IOptions<OpenIdConnectAuthenticationOptions>.Options
{
get
{
return _options;
}
}
/// <summary>
/// For now returns _options
/// </summary>
/// <param name="name">configuration to return</param>
/// <returns></returns>
public OpenIdConnectAuthenticationOptions GetNamedOptions(string name)
{
return _options;
}
}
public class CustomConfigureOptions : ConfigureOptions<OpenIdConnectAuthenticationOptions>
{
public CustomConfigureOptions(Action<OpenIdConnectAuthenticationOptions> action)
: base(action)
{
}
public override void Configure(OpenIdConnectAuthenticationOptions options, string name = "")
{
base.Configure(options, name);
return;
}
}
/// <summary>
/// Used to control which methods are handled
/// </summary>
public class CustomOpenIdConnectAuthenticationHandler : OpenIdConnectAuthenticationHandler
{
public CustomOpenIdConnectAuthenticationHandler(ILogger logger)
: base(logger)
{
}
public async Task BaseInitializeAsyncPublic(AuthenticationOptions options, HttpContext context)
{
await base.BaseInitializeAsync(options, context);
}
public override bool ShouldHandleScheme(string authenticationScheme)
{
return true;
}
public override void Challenge(IChallengeContext context)
{
}
protected override void ApplyResponseChallenge()
{
}
protected override async Task ApplyResponseChallengeAsync()
{
var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
};
await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification);
}
}
/// <summary>
/// Used to set <see cref="CustomOpenIdConnectAuthenticationHandler"/> as the AuthenticationHandler
/// which can be configured to handle certain messages.
/// </summary>
public class CustomOpenIdConnectAuthenticationMiddleware : OpenIdConnectAuthenticationMiddleware
{
public CustomOpenIdConnectAuthenticationMiddleware(
RequestDelegate next,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
IOptions<ExternalAuthenticationOptions> externalOptions,
IOptions<OpenIdConnectAuthenticationOptions> options,
ConfigureOptions<OpenIdConnectAuthenticationOptions> configureOptions = null
)
: base(next, dataProtectionProvider, loggerFactory, externalOptions, options, configureOptions)
{
Logger = (loggerFactory as CustomLoggerFactory).Logger;
}
protected override AuthenticationHandler<OpenIdConnectAuthenticationOptions> CreateHandler()
{
return new CustomOpenIdConnectAuthenticationHandler(Logger);
}
public ILogger Logger
{
get;
set;
}
}
public class LogEntry
{
public LogEntry() { }
public int EventId { get; set; }
public Exception Exception { get; set; }
public Func<object, Exception, string> Formatter { get; set; }
public LogLevel Level { get; set; }
public object State { get; set; }
public override string ToString()
{
if (Formatter != null)
{
return Formatter(this.State, this.Exception);
}
else
{
string message = (Formatter != null ? Formatter(State, Exception) : (State?.ToString() ?? "null"));
message += ", LogLevel: " + Level.ToString();
message += ", EventId: " + EventId.ToString();
message += ", Exception: " + (Exception == null ? "null" : Exception.Message);
return message;
}
}
}
public class CustomLogger : ILogger, IDisposable
{
LogLevel _logLevel = 0;
public CustomLogger(LogLevel logLevel = LogLevel.Debug)
{
_logLevel = logLevel;
}
List<LogEntry> logEntries = new List<LogEntry>();
public IDisposable BeginScopeImpl(object state)
{
return this;
}
public void Dispose()
{
}
public bool IsEnabled(LogLevel logLevel)
{
return (logLevel >= _logLevel);
}
public void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func<object, Exception, string> formatter)
{
if (IsEnabled(logLevel))
{
logEntries.Add(
new LogEntry
{
EventId = eventId,
Exception = exception,
Formatter = formatter,
Level = logLevel,
State = state,
});
#if _Verbose
Console.WriteLine(state?.ToString() ?? "state null");
#endif
}
}
public List<LogEntry> Logs { get { return logEntries; } }
}
public class CustomLoggerFactory : ILoggerFactory
{
CustomLogger _logger;
LogLevel _logLevel = LogLevel.Debug;
public CustomLoggerFactory(LogLevel logLevel)
{
_logLevel = logLevel;
_logger = new CustomLogger(_logLevel);
}
public LogLevel MinimumLevel
{
get { return _logLevel; }
set {_logLevel = value; }
}
public void AddProvider(ILoggerProvider provider)
{
}
public ILogger CreateLogger(string categoryName)
{
return _logger;
}
public CustomLogger Logger { get { return _logger; } }
}
/// <summary>
/// Processing a <see cref="OpenIdConnectMessage"/> requires 'unprotecting' the state.
/// This class side-steps that process.
/// </summary>
public class AuthenticationPropertiesFormater : ISecureDataFormat<AuthenticationProperties>
{
public string Protect(AuthenticationProperties data)
{
return "protectedData";
}
AuthenticationProperties ISecureDataFormat<AuthenticationProperties>.Unprotect(string protectedText)
{
return new AuthenticationProperties();
}
}
/// <summary>
/// Used to set up different configurations of metadata for different tests
/// </summary>
public class ConfigurationManager
{
/// <summary>
/// Simple static empty manager.
/// </summary>
static public IConfigurationManager<OpenIdConnectConfiguration> DefaultStaticConfigurationManager
{
get { return new StaticConfigurationManager<OpenIdConnectConfiguration>(new OpenIdConnectConfiguration()); }
}
}
}

View File

@ -0,0 +1,109 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.IdentityModel.Protocols;
using System;
namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
{
/// <summary>
/// These utilities are designed to test openidconnect related flows
/// </summary>
public class TestUtilities
{
public static bool AreEqual<T>(object obj1, object obj2, Func<object, object, bool> comparer = null) where T : class
{
if (obj1 == null && obj2 == null)
{
return true;
}
if (obj1 == null || obj2 == null)
{
return false;
}
if (obj1.GetType() != obj2.GetType())
{
return false;
}
if (obj1.GetType() != typeof(T))
{
return false;
}
if (comparer != null)
{
return comparer(obj1, obj2);
}
if (typeof(T) == typeof(LogEntry))
{
return AreEqual(obj1 as LogEntry, obj2 as LogEntry);
}
else if (typeof(T) == typeof(Exception))
{
return AreEqual(obj1 as Exception, obj2 as Exception);
}
throw new ArithmeticException("Unknown type, no comparer. Type: " + typeof(T).ToString());
}
/// <summary>
/// Never call this method directly, call AreObjectsEqual, as it deals with nulls and types"/>
/// </summary>
/// <param name="logEntry1"></param>
/// <param name="logEntry2"></param>
/// <returns></returns>
private static bool AreEqual(LogEntry logEntry1, LogEntry logEntry2)
{
if (logEntry1.EventId != logEntry2.EventId)
{
return false;
}
if (!AreEqual<Exception>(logEntry1.Exception, logEntry2.Exception))
{
return false;
}
if (logEntry1.State == null && logEntry2.State == null)
{
return true;
}
if (logEntry1.State == null)
{
return false;
}
if (logEntry2.State == null)
{
return false;
}
string logValue1 = logEntry1.Formatter == null ? logEntry1.State.ToString() : logEntry1.Formatter(logEntry1.State, logEntry1.Exception);
string logValue2 = logEntry2.Formatter == null ? logEntry2.State.ToString() : logEntry2.Formatter(logEntry2.State, logEntry2.Exception);
return (logValue1.StartsWith(logValue2) || (logValue2.StartsWith(logValue1)));
}
/// <summary>
/// Never call this method directly, call AreObjectsEqual, as it deals with nulls and types"/>
/// </summary>
/// <param name="exception1"></param>
/// <param name="exception2"></param>
/// <returns></returns>
private static bool AreEqual(Exception exception1, Exception exception2)
{
if (!string.Equals(exception1.Message, exception2.Message))
{
return false;
}
return AreEqual<Exception>(exception1.InnerException, exception2.InnerException);
}
}
}