From 501bd4ff105ee3985219a01d772f737bffc3e563 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Thu, 16 Apr 2015 12:26:42 -0700 Subject: [PATCH] Merge https://github.com/brentschmaltz/Security --- .../INonceCache.cs | 6 +- .../OpenIdConnectAuthenticationDefaults.cs | 4 +- .../OpenIdConnectAuthenticationExtensions.cs | 13 +- .../OpenIdConnectAuthenticationHandler.cs | 390 +++++---- .../OpenIdConnectAuthenticationMiddleware.cs | 22 +- .../OpenIdConnectAuthenticationOptions.cs | 79 +- ...penIdConnectServiceCollectionExtensions.cs | 4 +- .../Resources.Designer.cs | 354 ++++++-- .../Resources.resx | 111 ++- .../OpenIdConnectHandlerTests.cs | 768 ++++++++++++++++++ .../OpenIdConnect/TestUtilities.cs | 109 +++ 11 files changed, 1587 insertions(+), 273 deletions(-) create mode 100644 test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs create mode 100644 test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/INonceCache.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/INonceCache.cs index a1f2d35437..a56117560c 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/INonceCache.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/INonceCache.cs @@ -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); } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs index 21dc57f482..cecc57a74e 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// The prefix used to for the a nonce in the cookie /// - internal const string CookieNoncePrefix = ".AspNet.OpenIdConnect.Nonce."; + public const string CookieNoncePrefix = ".AspNet.OpenIdConnect.Nonce."; /// /// The property for the RedirectUri that was used when asking for a 'authorizationCode' @@ -36,6 +36,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// Constant used to identify state in openIdConnect protocal message /// - internal const string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties"; + public const string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties"; } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs index 989aac48e1..4ced947e2f 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs @@ -26,5 +26,16 @@ namespace Microsoft.AspNet.Builder Name = optionsName }); } + + /// + /// Adds the into the ASP.NET runtime. + /// + /// The application builder + /// Options which control the processing of the OpenIdConnect protocol and token validation. + /// The application builder + public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app, IOptions options) + { + return app.UseMiddleware(options); + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs index 32f7a69c0f..c0efcd0f61 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs @@ -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. /// /// + /// Uses log id's OIDCH-0026 - OIDCH-0050, next num: 37 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(Context, Options) + var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(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. /// /// An if successful. + /// Uses log id's OIDCH-0000 - OIDCH-0025 protected override async Task 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(Context, Options) + if (_logger.IsEnabled(LogLevel.Debug)) { - ProtocolMessage = openIdConnectMessage - }; + _logger.LogDebug(Resources.OIDCH_0001_MessageReceived, message.BuildRedirectUrl()); + } + + var messageReceivedNotification = + new MessageReceivedNotification(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(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(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(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(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(Context, Options) - { - ProtocolMessage = openIdConnectMessage, - Exception = exception - }; + var authenticationFailedNotification = + new AuthenticationFailedNotification(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 /// the nonce to remember. /// is called to add a cookie with the name: 'OpenIdConnectAuthenticationDefaults.Nonce + (nonce)'. /// The value of the cookie is: "N". - private void RememberNonce(string nonce) + private void WriteNonceCookie(string nonce) { if (string.IsNullOrWhiteSpace(nonce)) { @@ -484,13 +562,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// Searches for a matching nonce. /// - /// the nonce that was found in the jwt token. - /// 'nonceExpectedValue' if a cookie is found that matches, null otherwise. + /// the nonce that we are looking for. + /// echos 'nonce' if a cookie is found that matches, null otherwise. /// Examine that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'. /// is used to obtain the actual 'nonce'. If the nonce is found, then is called. - 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) diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs index e58b7fff85..ee8eec0e5a 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -30,9 +30,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// Initializes a /// - /// The next middleware in the ASP.NET pipeline to invoke - /// The ASP.NET application - /// Configuration options for the middleware + /// The next middleware in the ASP.NET pipeline to invoke. + /// provider for creating a data protector. + /// factory for creating a . + /// a instance that will supply + /// if configureOptions is null. + /// a instance that will be passed to an instance of + /// that is retrieved by calling where string == provides runtime configuration. [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 externalOptions, [NotNull] IOptions options, - ConfigureOptions configureOptions) + ConfigureOptions configureOptions = null) : base(next, options, configureOptions) { _logger = loggerFactory.CreateLogger(); - - 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; } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs index f7bfde804b..e5a3793d38 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs @@ -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; } + /// + /// Gets or sets the expected audience for any received JWT token. + /// + /// + /// The expected audience for any received JWT token. + /// + public string Audience { get; set; } + /// /// Gets or sets the Authority to use when making OpenIdConnect calls. /// public string Authority { get; set; } - /// - /// 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. - /// - /// If you set this value, then the 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 -> ... - public PathString CallbackPath { get; set; } - #if DNX451 /// /// 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; } } + /// + /// 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. + /// + /// If you set this value, then the 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 -> ... + public PathString CallbackPath { get; set; } + /// /// Gets or sets the 'client_id'. /// @@ -145,11 +152,16 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect public OpenIdConnectConfiguration Configuration { get; set; } /// - /// 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. /// - public INonceCache NonceCache { get; set; } + public IConfigurationManager ConfigurationManager { get; set; } + + /// + /// 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. + /// + public bool DefaultToCurrentUriOnRedirect { get; set; } /// /// Gets or sets the discovery endpoint for obtaining metadata @@ -157,24 +169,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect public string MetadataAddress { get; set; } /// - /// 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. /// - /// - /// The expected audience for any received JWT token. - /// - public string Audience { get; set; } - - /// - /// Responsible for retrieving, caching, and refreshing the configuration from metadata. - /// If not provided, then one will be created using the MetadataAddress and Backchannel properties. - /// - public IConfigurationManager ConfigurationManager { get; set; } - - /// - /// 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. - /// - public bool RefreshOnIssuerKeyNotFound { get; set; } + public INonceCache NonceCache { get; set; } /// /// Gets or sets the 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; } + /// + /// 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. + /// + public bool RefreshOnIssuerKeyNotFound { get; set; } + /// /// Gets or sets the 'resource'. /// public string Resource { get; set; } + /// + /// Gets or sets the 'response_mode'. + /// + public string ResponseMode { get; private set; } + /// /// Gets or sets the 'response_type'. /// @@ -233,10 +243,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect public string Scope { get; set; } /// - /// 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, is used as a fallback value. + /// Gets or sets the SignInScheme which will be used to set the . /// public string SignInScheme { get; set; } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectServiceCollectionExtensions.cs index 6f55448890..2e55b96fa4 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectServiceCollectionExtensions.cs @@ -15,7 +15,7 @@ namespace Microsoft.Framework.DependencyInjection { public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure) { - return services.ConfigureOpenIdConnectAuthentication(configure, optionsName: ""); + return ConfigureOpenIdConnectAuthentication(services, configure, null); } public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] Action 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) diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs index ee19db0116..b95de83180 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs @@ -1,18 +1,9 @@ -//------------------------------------------------------------------------------ -// -// 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. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.AspNet.Authentication.OpenIdConnect { - using System; +// +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + using System.Globalization; using System.Reflection; - - + using System.Resources; /// /// 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() + { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [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; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [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; } } - + /// - /// 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. /// - 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"); } } - + /// - /// Looks up a localized string similar to "OpenIdConnectMessage.Error was not null, indicating an error. Error: '{0}'. Error_Description (may be empty): '{1}'. Error_Uri (may be empty): '{2}'.". + /// OIDCH0102: An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// - 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"); } } - + /// - /// 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: '{0}'.. + /// OIDCH_0051: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: '{0}'. /// - 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"); } } - + /// - /// 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}' /// - 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"); } + } + + /// + /// OIDCH_0027: converted 401 to 403. + /// + internal static string OIDCH_0027_401_ConvertedTo_403 + { + get { return ResourceManager.GetString("OIDCH_0027_401_ConvertedTo_403"); } + } + + /// + /// OIDCH_0028: Response.StatusCode != 401, StatusCode: '{0}'." + /// + internal static string OIDCH_0028_StatusCodeNot401 + { + get { return ResourceManager.GetString("OIDCH_0028_StatusCodeNot401"); } + } + + /// + /// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication + /// + internal static string OIDCH_0029_ChallengeContextEqualsNull + { + get { return ResourceManager.GetString("OIDCH_0029_ChallengeContextEqualsNull"); } + } + + /// + /// OIDCH_0030: using properties.RedirectUri for 'local redirect' post authentication: '{0}'. + /// + internal static string OIDCH_0030_Using_Properties_RedirectUri + { + get { return ResourceManager.GetString("OIDCH_0030_Using_Properties_RedirectUri"); } + } + + /// + /// OIDCH_0031: using Options.RedirectUri for 'redirect_uri': '{0}'. + /// + internal static string OIDCH_0031_Using_Options_RedirectUri + { + get { return ResourceManager.GetString("OIDCH_0031_Using_Options_RedirectUri"); } + } + + /// + /// OIDCH_0032: using the CurrentUri for 'local redirect' post authentication: '{0}'. + /// + internal static string OIDCH_0032_UsingCurrentUriRedirectUri + { + get { return ResourceManager.GetString("OIDCH_0032_UsingCurrentUriRedirectUri"); } + } + + /// + /// 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}'. + /// + internal static string OIDCH_0033_TryAddNonceFailed + { + get { return ResourceManager.GetString("OIDCH_0033_TryAddNonceFailed"); } + } + + /// + /// OIDCH_0034: redirectToIdentityProviderNotification.HandledResponse + /// + internal static string OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse"); } + } + + /// + /// OIDCH_0035: redirectToIdentityProviderNotification.Skipped + /// + internal static string OIDCH_0035_RedirectToIdentityProviderNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0035_RedirectToIdentityProviderNotificationSkipped"); } + } + + /// + /// OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}", (redirectUri ?? "null")) + /// + internal static string OIDCH_0036_UriIsNotWellFormed + { + get { return ResourceManager.GetString("OIDCH_0036_UriIsNotWellFormed"); } + } + + /// + /// OIDCH_0000: Entering: '{0}'. + /// + internal static string OIDCH_0000_AuthenticateCoreAsync + { + get { return ResourceManager.GetString("OIDCH_0000_AuthenticateCoreAsync"); } + } + + /// + /// OIDCH_0001: MessageReceived: '{0}'. + /// + internal static string OIDCH_0001_MessageReceived + { + get { return ResourceManager.GetString("OIDCH_0001_MessageReceived"); } + } + + /// + /// OIDCH_0001: MessageReceived: '{0}'. + /// + internal static string FormatOIDCH_0001_MessageReceived(object p0) + { + return string.Format(CultureInfo.CurrentCulture, ResourceManager.GetString("OIDCH_0001_MessageReceived"), p0); + } + + /// + /// OIDCH_0002: messageReceivedNotification.HandledResponse + /// + internal static string OIDCH_0002_MessageReceivedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0002_MessageReceivedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0003: messageReceivedNotification.Skipped + /// + internal static string OIDCH_0003_MessageReceivedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0003_MessageReceivedNotificationSkipped"); } + } + + /// + /// OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or whitespace. State is required to process the message. + /// + internal static string OIDCH_0004_MessageStateIsNullOrWhiteSpace + { + get { return ResourceManager.GetString("OIDCH_0004_MessageStateIsNullOrWhiteSpace"); } + } + + /// + /// OIDCH_0005: unable to unprotect the message.State + /// + internal static string OIDCH_0005_MessageStateIsInvalid + { + get { return ResourceManager.GetString("OIDCH_0005_MessageStateIsInvalid"); } + } + + /// + /// OIDCH_0006_MessageErrorNotNull: '{0}'. + /// + internal static string OIDCH_0006_MessageErrorNotNull + { + get { return ResourceManager.GetString("OIDCH_0006_MessageErrorNotNull"); } + } + + /// + /// OIDCH_0007: updating configuration + /// + internal static string OIDCH_0007_UpdatingConfiguration + { + get { return ResourceManager.GetString("OIDCH_0007_UpdatingConfiguration"); } + } + + /// + /// OIDCH_0008: securityTokenReceivedNotification.HandledResponse + /// + internal static string OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0009: securityTokenReceivedNotification.Skipped + /// + internal static string OIDCH_0009_SecurityTokenReceivedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0009_SecurityTokenReceivedNotificationSkipped:"); } + } + + /// + /// OIDCH_0010: Validated Security Token must be a JwtSecurityToken was: '{0}'. + /// + internal static string OIDCH_0010_ValidatedSecurityTokenNotJwt + { + get { return ResourceManager.GetString("OIDCH_0010_ValidatedSecurityTokenNotJwt"); } + } + + /// + /// OIDCH_0011: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: {0}." + /// + internal static string OIDCH_0011_UnableToValidateToken + { + get { return ResourceManager.GetString("OIDCH_0011_UnableToValidateToken"); } + } + + /// + /// OIDCH_0012: securityTokenValidatedNotification.HandledResponse + /// + internal static string OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0013: securityTokenValidatedNotification.Skipped + /// + internal static string OIDCH_0013_SecurityTokenValidatedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0013_SecurityTokenValidatedNotificationSkipped"); } + } + + /// + /// OIDCH_0014: 'code' received: '{0}' + /// + internal static string OIDCH_0014_CodeReceived + { + get { return ResourceManager.GetString("OIDCH_0014_CodeReceived"); } + } + + /// + /// OIDCH_0015: codeReceivedNotification.HandledResponse") + /// + internal static string OIDCH_0015_CodeReceivedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0015_CodeReceivedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0016: codeReceivedNotification.Skipped + /// + internal static string OIDCH_0016_CodeReceivedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0016_CodeReceivedNotificationSkipped"); } + } + + /// + /// OIDCH_0017: Exception occurred while processing message + /// + internal static string OIDCH_0017_ExceptionOccurredWhileProcessingMessage + { + get { return ResourceManager.GetString("OIDCH_0017_ExceptionOccurredWhileProcessingMessage"); } + } + + /// + /// OIDCH_0018: authenticationFailedNotification.HandledResponse + /// + internal static string OIDCH_0018_AuthenticationFailedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0018_AuthenticationFailedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0019: authenticationFailedNotification.Skipped + /// + internal static string OIDCH_0019_AuthenticationFailedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0019_AuthenticationFailedNotificationSkipped"); } + } + + /// + /// OIDCH_0020: 'id_token' received: '{0}' + /// + internal static string OIDCH_0020_IdTokenReceived + { + get { return ResourceManager.GetString("OIDCH_0020_IdTokenReceived"); } } } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx index 7abad90eb0..454dae209b 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx @@ -117,16 +117,109 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - BackchannelTimeout cannot be less or equal to TimeSpan.Zero. + + OIDCH_0101: BackchannelTimeout cannot be less or equal to TimeSpan.Zero. - - "OpenIdConnectMessage.Error was not null, indicating an error. Error: '{0}'. Error_Description (may be empty): '{1}'. Error_Uri (may be empty): '{2}'." + + OIDCH_0102: An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. - - OIDC_20001: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: '{0}'. + + OIDC_0051: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: '{0}'. - - An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + OIDCH_0026: Entering: '{0}' - \ No newline at end of file + + OIDCH_0027: converted 401 to 403. + + + OIDCH_0028: Response.StatusCode != 401, StatusCode: '{0}'. + + + OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication + + + OIDCH_0030: using properties.RedirectUri for 'local redirect' post authentication: '{0}'. + + + OIDCH_0031: using Options.RedirectUri for 'redirect_uri': '{0}'. + + + OIDCH_0032: using the CurrentUri for 'local redirect' post authentication: '{0}'. + + + 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}'. + + + OIDCH_0034: redirectToIdentityProviderNotification.HandledResponse + + + OIDCH_0035: redirectToIdentityProviderNotification.Skipped + + + OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}", (redirectUri ?? "null")) + + + OIDCH_0000: Entering: '{0}'. + + + OIDCH_0001: MessageReceived: '{0}'. + + + OIDCH_0002: messageReceivedNotification.HandledResponse + + + OIDCH_0003: messageReceivedNotification.Skipped + + + OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or whitespace. State is required to process the message. + + + OIDCH_0005: unable to unprotect the message.State + + + OIDCH_0006_MessageErrorNotNull: '{0}'. + + + OIDCH_0007: updating configuration + + + OIDCH_0008: securityTokenReceivedNotification.HandledResponse + + + OIDCH_0009: securityTokenReceivedNotification.Skipped + + + OIDCH_0010: Validated Security Token must be a JwtSecurityToken was: '{0}'. + + + OIDCH_0011: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: {0}." + + + OIDCH_0012: securityTokenValidatedNotification.HandledResponse + + + OIDCH_0013: securityTokenValidatedNotification.Skipped + + + OIDCH_0014: 'code' received: '{0}' + + + OIDCH_0015: codeReceivedNotification.HandledResponse + + + OIDCH_0016: codeReceivedNotification.Skipped + + + OIDCH_0017: Exception occurred while processing message + + + OIDCH_0018: authenticationFailedNotification.HandledResponse + + + OIDCH_0019: authenticationFailedNotification.Skipped + + + OIDCH_0020: 'id_token' received: '{0}' + + diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs new file mode 100644 index 0000000000..5817615f2b --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs @@ -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 +{ + /// + /// These tests are designed to test OpenIdConnectAuthenticationHandler. + /// + public class OpenIdConnectHandlerTests + { + static List CompleteLogEntries; + static Dictionary LogEntries; + + static OpenIdConnectHandlerTests() + { + LogEntries = + new Dictionary() + { + { "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(); + } + + /// + /// Builds the complete list of log entries that are available in the runtime. + /// + private static void BuildLogEntryList() + { + CompleteLogEntries = new List(); + foreach (var entry in LogEntries) + { + CompleteLogEntries.Add(new LogEntry { State = entry.Key, Level = entry.Value }); + } + } + + /// + /// Sanity check that logging is filtering, hi / low water marks are checked + /// + [Fact] + public void LoggingLevel() + { + var logger = new CustomLogger(LogLevel.Debug); + logger.IsEnabled(LogLevel.Critical).ShouldBe(true); + logger.IsEnabled(LogLevel.Debug).ShouldBe(true); + logger.IsEnabled(LogLevel.Error).ShouldBe(true); + logger.IsEnabled(LogLevel.Information).ShouldBe(true); + logger.IsEnabled(LogLevel.Verbose).ShouldBe(true); + logger.IsEnabled(LogLevel.Warning).ShouldBe(true); + + logger = new CustomLogger(LogLevel.Critical); + logger.IsEnabled(LogLevel.Critical).ShouldBe(true); + logger.IsEnabled(LogLevel.Debug).ShouldBe(false); + logger.IsEnabled(LogLevel.Error).ShouldBe(false); + logger.IsEnabled(LogLevel.Information).ShouldBe(false); + logger.IsEnabled(LogLevel.Verbose).ShouldBe(false); + logger.IsEnabled(LogLevel.Warning).ShouldBe(false); + } + + /// + /// Test produces expected logs. + /// Each call to 'RunVariation' is configured with an and . + /// The list of expected log entries is checked and any errors reported. + /// captures the logs so they can be prepared. + /// + /// + [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>>(); + + 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); + } + + /// + /// Tests that processes a messaage as expected. + /// The test runs two independant paths: Using and + /// + /// for this variation + /// the that has arrived + /// the delegate used for setting the options. + /// container for propogation of errors. + /// the expected log entries + /// a Task + private async Task RunVariation(LogLevel logLevel, OpenIdConnectMessage message, Action action, Dictionary>> 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); + } + + /// + /// Populates a list of expected log entries for a test variation. + /// + /// the index for the in CompleteLogEntries of interest. + /// a that represents the expected entries for a test variation. + private List PopulateLogEntries(int[] items) + { + var entries = new List(); + foreach(var item in items) + { + entries.Add(CompleteLogEntries[item]); + } + + return entries; + } + + private void DisplayLogs(List logs) + { + foreach (var logentry in logs) + { + Console.WriteLine(logentry.ToString()); + } + } + + private void DisplayErrors(Dictionary>> 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); + } + } + } + + /// + /// Adds to errors if a variation if any are found. + /// + /// if this has been seen before, errors will be appended, test results are easier to understand if this is unique. + /// these are the logs the runtime generated + /// these are the errors that were expected + /// the dictionary to record any errors + private void CheckLogs(string variation, List capturedLogs, List expectedLogs, Dictionary>> errors) + { + var localErrors = new List>(); + + if (capturedLogs.Count >= expectedLogs.Count) + { + for (int i = 0; i < capturedLogs.Count; i++) + { + if (i + 1 > expectedLogs.Count) + { + localErrors.Add(new Tuple(capturedLogs[i], null)); + } + else + { + if (!TestUtilities.AreEqual(capturedLogs[i], expectedLogs[i])) + { + localErrors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); + } + } + } + } + else + { + for (int i = 0; i < expectedLogs.Count; i++) + { + if (i + 1 > capturedLogs.Count) + { + localErrors.Add(new Tuple(null, expectedLogs[i])); + } + else + { + if (!TestUtilities.AreEqual(expectedLogs[i], capturedLogs[i])) + { + localErrors.Add(new Tuple(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(null); + } + }; + } + + private static void CodeReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + AuthorizationCodeReceived = (notification) => + { + notification.SkipToNextMiddleware(); + return Task.FromResult(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(null); + } + }; + } + + private static void MessageReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + MessageReceived = (notification) => + { + notification.SkipToNextMiddleware(); + return Task.FromResult(null); + } + }; + } + + private static void SecurityTokenReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + SecurityTokenReceived = (notification) => + { + notification.HandleResponse(); + return Task.FromResult(null); + } + }; + } + + private static void SecurityTokenReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + SecurityTokenReceived = (notification) => + { + notification.SkipToNextMiddleware(); + return Task.FromResult(null); + } + }; + } + + private static void SecurityTokenValidatedHandledOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + SecurityTokenValidated = (notification) => + { + notification.HandleResponse(); + return Task.FromResult(null); + } + }; + } + + private static void SecurityTokenValidatedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + SecurityTokenValidated = (notification) => + { + notification.SkipToNextMiddleware(); + return Task.FromResult(null); + } + }; + } + + #endregion + + private static TestServer CreateServer(IOptions 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(); + } + ); + } + } + + /// + /// Extension specifies as the middleware. + /// + public static class OpenIdConnectAuthenticationExtensions + { + /// + /// Adds the into the ASP.NET runtime. + /// + /// The application builder + /// Options which control the processing of the OpenIdConnect protocol and token validation. + /// custom loggerFactory + /// The application builder + public static IApplicationBuilder UseCustomOpenIdConnectAuthentication(this IApplicationBuilder app, CustomConfigureOptions customConfigureOption, ILoggerFactory loggerFactory) + { + return app.UseMiddleware(customConfigureOption, loggerFactory); + } + + /// + /// Adds the into the ASP.NET runtime. + /// + /// The application builder + /// Options which control the processing of the OpenIdConnect protocol and token validation. + /// custom loggerFactory + /// The application builder + public static IApplicationBuilder UseCustomOpenIdConnectAuthentication(this IApplicationBuilder app, IOptions options, ILoggerFactory loggerFactory) + { + return app.UseMiddleware(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 properties, IDictionary description) + { + } + + public void NotAuthenticated() + { + } + } + + /// + /// Provides a Facade over IOptions + /// + public class Options : IOptions + { + OpenIdConnectAuthenticationOptions _options; + + public Options(Action action) + { + _options = new OpenIdConnectAuthenticationOptions(); + action(_options); + } + + OpenIdConnectAuthenticationOptions IOptions.Options + { + get + { + return _options; + } + } + + /// + /// For now returns _options + /// + /// configuration to return + /// + public OpenIdConnectAuthenticationOptions GetNamedOptions(string name) + { + return _options; + } + } + + public class CustomConfigureOptions : ConfigureOptions + { + public CustomConfigureOptions(Action action) + : base(action) + { + } + + public override void Configure(OpenIdConnectAuthenticationOptions options, string name = "") + { + base.Configure(options, name); + return; + } + } + + /// + /// Used to control which methods are handled + /// + 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(Context, Options) + { + }; + + await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); + } + } + + /// + /// Used to set as the AuthenticationHandler + /// which can be configured to handle certain messages. + /// + public class CustomOpenIdConnectAuthenticationMiddleware : OpenIdConnectAuthenticationMiddleware + { + public CustomOpenIdConnectAuthenticationMiddleware( + RequestDelegate next, + IDataProtectionProvider dataProtectionProvider, + ILoggerFactory loggerFactory, + IOptions externalOptions, + IOptions options, + ConfigureOptions configureOptions = null + ) + : base(next, dataProtectionProvider, loggerFactory, externalOptions, options, configureOptions) + { + Logger = (loggerFactory as CustomLoggerFactory).Logger; + } + + protected override AuthenticationHandler 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 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 logEntries = new List(); + + 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 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 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; } } + } + + /// + /// Processing a requires 'unprotecting' the state. + /// This class side-steps that process. + /// + public class AuthenticationPropertiesFormater : ISecureDataFormat + { + public string Protect(AuthenticationProperties data) + { + return "protectedData"; + } + + AuthenticationProperties ISecureDataFormat.Unprotect(string protectedText) + { + return new AuthenticationProperties(); + } + } + + /// + /// Used to set up different configurations of metadata for different tests + /// + public class ConfigurationManager + { + /// + /// Simple static empty manager. + /// + static public IConfigurationManager DefaultStaticConfigurationManager + { + get { return new StaticConfigurationManager(new OpenIdConnectConfiguration()); } + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs new file mode 100644 index 0000000000..abb5b85383 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs @@ -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 +{ + /// + /// These utilities are designed to test openidconnect related flows + /// + public class TestUtilities + { + public static bool AreEqual(object obj1, object obj2, Func 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()); + + } + + /// + /// Never call this method directly, call AreObjectsEqual, as it deals with nulls and types"/> + /// + /// + /// + /// + private static bool AreEqual(LogEntry logEntry1, LogEntry logEntry2) + { + if (logEntry1.EventId != logEntry2.EventId) + { + return false; + } + + if (!AreEqual(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))); + } + + /// + /// Never call this method directly, call AreObjectsEqual, as it deals with nulls and types"/> + /// + /// + /// + /// + private static bool AreEqual(Exception exception1, Exception exception2) + { + if (!string.Equals(exception1.Message, exception2.Message)) + { + return false; + } + + return AreEqual(exception1.InnerException, exception2.InnerException); + } + } +}