diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs index 763ef11d3..2e1a80889 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs @@ -9,14 +9,14 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect public static class OpenIdConnectAuthenticationDefaults { /// - /// The default value used for OpenIdConnectAuthenticationOptions.AuthenticationScheme + /// Constant used to identify state in openIdConnect protocol message. /// - public const string AuthenticationScheme = "OpenIdConnect"; + public const string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties"; /// - /// The prefix used to provide a default OpenIdConnectAuthenticationOptions.CookieName + /// The default value used for OpenIdConnectAuthenticationOptions.AuthenticationScheme. /// - public const string CookiePrefix = ".AspNet.OpenIdConnect."; + public const string AuthenticationScheme = "OpenIdConnect"; /// /// The default value for OpenIdConnectAuthenticationOptions.Caption. @@ -24,18 +24,23 @@ public static class OpenIdConnectAuthenticationDefaults public const string Caption = "OpenIdConnect"; /// - /// The prefix used to for the a nonce in the cookie + /// The prefix used to provide a default OpenIdConnectAuthenticationOptions.CookieName. + /// + public const string CookiePrefix = ".AspNet.OpenIdConnect."; + + /// + /// The prefix used to for the a nonce in the cookie. /// public const string CookieNoncePrefix = ".AspNet.OpenIdConnect.Nonce."; /// - /// The property for the RedirectUri that was used when asking for a 'authorizationCode' + /// The property for the RedirectUri that was used when asking for a 'authorizationCode'. /// - public const string RedirectUriUsedForCodeKey = "OpenIdConnect.Code.RedirectUri"; + public const string RedirectUriForCodePropertiesKey = "OpenIdConnect.Code.RedirectUri"; /// - /// Constant used to identify state in openIdConnect protocal message + /// Constant used to identify userstate inside AuthenticationProperties that have been serialized in the 'state' parameter. /// - public const string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties"; + public const string UserstatePropertiesKey = "OpenIdConnect.Userstate"; } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs index 8b9c4683c..f3e381968 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs @@ -28,19 +28,6 @@ public class OpenIdConnectAuthenticationHandler : AuthenticationHandler /// Handles Signout /// @@ -54,7 +41,7 @@ protected override async Task HandleSignOutAsync(SignOutContext signout) _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } - var openIdConnectMessage = new OpenIdConnectMessage() + var message = new OpenIdConnectMessage() { IssuerAddress = _configuration == null ? string.Empty : (_configuration.EndSessionEndpoint ?? string.Empty), RequestType = OpenIdConnectRequestType.LogoutRequest, @@ -66,30 +53,42 @@ protected override async Task HandleSignOutAsync(SignOutContext signout) var properties = new AuthenticationProperties(signout.Properties); if (!string.IsNullOrEmpty(properties.RedirectUri)) { - openIdConnectMessage.PostLogoutRedirectUri = properties.RedirectUri; + message.PostLogoutRedirectUri = properties.RedirectUri; } - else if (!string.IsNullOrWhiteSpace(Options.PostLogoutRedirectUri)) + else if (!string.IsNullOrEmpty(Options.PostLogoutRedirectUri)) { - openIdConnectMessage.PostLogoutRedirectUri = Options.PostLogoutRedirectUri; + message.PostLogoutRedirectUri = Options.PostLogoutRedirectUri; } - var notification = new RedirectToIdentityProviderNotification(Context, Options) + if (Options.Notifications.RedirectToIdentityProvider != null) { - ProtocolMessage = openIdConnectMessage - }; - - await Options.Notifications.RedirectToIdentityProvider(notification); + var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) + { + ProtocolMessage = message + }; - if (!notification.HandledResponse) - { - var redirectUri = notification.ProtocolMessage.CreateLogoutRequestUrl(); - if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); + if (redirectToIdentityProviderNotification.HandledResponse) + { + Logger.LogVerbose(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse); + return; + } + else if (redirectToIdentityProviderNotification.Skipped) { - Logger.LogWarning(Resources.OIDCH_0051_RedirectUriLogoutIsNotWellFormed, redirectUri); + Logger.LogVerbose(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped); + return; } - Response.Redirect(redirectUri); + message = redirectToIdentityProviderNotification.ProtocolMessage; } + + var redirectUri = message.CreateLogoutRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.LogWarning(Resources.OIDCH_0051_RedirectUriLogoutIsNotWellFormed, redirectUri); + } + + Response.Redirect(redirectUri); } } @@ -107,7 +106,7 @@ protected override async Task HandleUnauthorizedAsync([NotNull] ChallengeC // 2. CurrentUri if Options.DefaultToCurrentUriOnRedirect is true) AuthenticationProperties properties = new AuthenticationProperties(context.Properties); - if (!string.IsNullOrWhiteSpace(properties.RedirectUri)) + if (!string.IsNullOrEmpty(properties.RedirectUri)) { Logger.LogDebug(Resources.OIDCH_0030_Using_Properties_RedirectUri, properties.RedirectUri); } @@ -117,15 +116,15 @@ protected override async Task HandleUnauthorizedAsync([NotNull] ChallengeC properties.RedirectUri = CurrentUri; } - if (!string.IsNullOrWhiteSpace(Options.RedirectUri)) + if (!string.IsNullOrEmpty(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)) + if (!string.IsNullOrEmpty(Options.RedirectUri)) { - properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey, Options.RedirectUri); + properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, Options.RedirectUri); } if (_configuration == null && Options.ConfigurationManager != null) @@ -138,13 +137,12 @@ protected override async Task HandleUnauthorizedAsync([NotNull] ChallengeC ClientId = Options.ClientId, IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty, RedirectUri = Options.RedirectUri, - // [brentschmaltz] - this should be a property on RedirectToIdentityProviderNotification not on the OIDCMessage. + // [brentschmaltz] - #215 this should be a property on RedirectToIdentityProviderNotification not on the OIDCMessage. RequestType = OpenIdConnectRequestType.AuthenticationRequest, Resource = Options.Resource, ResponseMode = Options.ResponseMode, ResponseType = Options.ResponseType, - Scope = Options.Scope, - State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + UrlEncoder.UrlEncode(Options.StateDataFormat.Protect(properties)) + Scope = Options.Scope }; if (Options.ProtocolValidator.RequireNonce) @@ -169,24 +167,37 @@ protected override async Task HandleUnauthorizedAsync([NotNull] ChallengeC } } - var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) + if (Options.Notifications.RedirectToIdentityProvider != null) { - ProtocolMessage = message - }; + var redirectToIdentityProviderNotification = + new RedirectToIdentityProviderNotification(Context, Options) + { + ProtocolMessage = message + }; - await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); - if (redirectToIdentityProviderNotification.HandledResponse) - { - Logger.LogInformation(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse); - return true; // REVIEW: Make sure this should stop all other handlers - } - else if (redirectToIdentityProviderNotification.Skipped) - { - Logger.LogInformation(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped); - return false; // REVIEW: Make sure this should not stop all other handlers + await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); + if (redirectToIdentityProviderNotification.HandledResponse) + { + Logger.LogVerbose(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse); + return true; + } + else if (redirectToIdentityProviderNotification.Skipped) + { + Logger.LogVerbose(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped); + return false; + } + + if (!string.IsNullOrEmpty(redirectToIdentityProviderNotification.ProtocolMessage.State)) + { + properties.Items[OpenIdConnectAuthenticationDefaults.UserstatePropertiesKey] = redirectToIdentityProviderNotification.ProtocolMessage.State; + } + + message = redirectToIdentityProviderNotification.ProtocolMessage; } - var redirectUri = redirectToIdentityProviderNotification.ProtocolMessage.CreateAuthenticationRequestUrl(); + message.State = Options.StateDataFormat.Protect(properties); + + var redirectUri = message.CreateAuthenticationRequestUrl(); if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) { Logger.LogWarning(Resources.OIDCH_0036_UriIsNotWellFormed, redirectUri); @@ -246,66 +257,74 @@ public override async Task AuthenticateAsync() await Options.Notifications.MessageReceived(messageReceivedNotification); if (messageReceivedNotification.HandledResponse) { - Logger.LogInformation(Resources.OIDCH_0002_MessageReceivedNotificationHandledResponse); + Logger.LogVerbose(Resources.OIDCH_0002_MessageReceivedNotificationHandledResponse); return messageReceivedNotification.AuthenticationTicket; } if (messageReceivedNotification.Skipped) { - Logger.LogInformation(Resources.OIDCH_0003_MessageReceivedNotificationSkipped); + Logger.LogVerbose(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. - if (string.IsNullOrWhiteSpace(message.State)) + var properties = new AuthenticationProperties(); + + // if state is missing, just log it + if (string.IsNullOrEmpty(message.State)) { - Logger.LogError(Resources.OIDCH_0004_MessageStateIsNullOrWhiteSpace); - return null; + Logger.LogWarning(Resources.OIDCH_0004_MessageStateIsNullOrEmpty); } - - var properties = GetPropertiesFromState(message.State); - if (properties == null) + else { - Logger.LogError(Resources.OIDCH_0005_MessageStateIsInvalid); - return null; + // if state exists and we failed to 'unprotect' this is not a message we should process. + properties = Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(message.State)); + if (properties == null) + { + Logger.LogError(Resources.OIDCH_0005_MessageStateIsInvalid); + return null; + } + + string userstate = null; + properties.Items.TryGetValue(OpenIdConnectAuthenticationDefaults.UserstatePropertiesKey, out userstate); + message.State = userstate; } - // devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users. - if (!string.IsNullOrWhiteSpace(message.Error)) + // if any of the error fields are set, throw error null + if (!string.IsNullOrEmpty(message.Error)) { - Logger.LogError(Resources.OIDCH_0006_MessageErrorNotNull, message.Error); - throw new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0006_MessageErrorNotNull, message.Error)); + Logger.LogError(Resources.OIDCH_0006_MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null"); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0006_MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null")); } - AuthenticationTicket ticket = null; - JwtSecurityToken jwt = null; - if (_configuration == null && Options.ConfigurationManager != null) { - Logger.LogDebug(Resources.OIDCH_0007_UpdatingConfiguration); + Logger.LogVerbose(Resources.OIDCH_0007_UpdatingConfiguration); _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } + AuthenticationTicket ticket = null; + JwtSecurityToken jwt = null; + // OpenIdConnect protocol allows a Code to be received without the id_token - if (!string.IsNullOrWhiteSpace(message.IdToken)) + if (!string.IsNullOrEmpty(message.IdToken)) { Logger.LogDebug(Resources.OIDCH_0020_IdTokenReceived, message.IdToken); var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) { - ProtocolMessage = message + ProtocolMessage = message, }; await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); if (securityTokenReceivedNotification.HandledResponse) { - Logger.LogInformation(Resources.OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse); + Logger.LogVerbose(Resources.OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse); return securityTokenReceivedNotification.AuthenticationTicket; } if (securityTokenReceivedNotification.Skipped) { - Logger.LogInformation(Resources.OIDCH_0009_SecurityTokenReceivedNotificationSkipped); + Logger.LogVerbose(Resources.OIDCH_0009_SecurityTokenReceivedNotificationSkipped); return null; } @@ -313,11 +332,11 @@ public override async Task AuthenticateAsync() var validationParameters = Options.TokenValidationParameters.Clone(); if (_configuration != null) { - if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer)) + if (string.IsNullOrEmpty(validationParameters.ValidIssuer)) { validationParameters.ValidIssuer = _configuration.Issuer; } - else if (!string.IsNullOrWhiteSpace(_configuration.Issuer)) + else if (!string.IsNullOrEmpty(_configuration.Issuer)) { validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(new[] { _configuration.Issuer }) ?? new[] { _configuration.Issuer }; } @@ -336,7 +355,7 @@ public override async Task AuthenticateAsync() if (jwt == null) { Logger.LogError(Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType()); - throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType())); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType())); } } } @@ -344,16 +363,16 @@ public override async Task AuthenticateAsync() if (validatedToken == null) { Logger.LogError(Resources.OIDCH_0011_UnableToValidateToken, message.IdToken); - throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0011_UnableToValidateToken, message.IdToken)); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0011_UnableToValidateToken, message.IdToken)); } ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); - if (!string.IsNullOrWhiteSpace(message.SessionState)) + if (!string.IsNullOrEmpty(message.SessionState)) { ticket.Properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState; } - if (_configuration != null && !string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe)) + if (_configuration != null && !string.IsNullOrEmpty(_configuration.CheckSessionIframe)) { ticket.Properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; } @@ -384,13 +403,13 @@ public override async Task AuthenticateAsync() await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); if (securityTokenValidatedNotification.HandledResponse) { - Logger.LogInformation(Resources.OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse); + Logger.LogVerbose(Resources.OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse); return securityTokenValidatedNotification.AuthenticationTicket; } if (securityTokenValidatedNotification.Skipped) { - Logger.LogInformation(Resources.OIDCH_0013_SecurityTokenValidatedNotificationSkipped); + Logger.LogVerbose(Resources.OIDCH_0013_SecurityTokenValidatedNotificationSkipped); return null; } @@ -416,7 +435,7 @@ public override async Task AuthenticateAsync() var protocolValidationContext = new OpenIdConnectProtocolValidationContext { AuthorizationCode = message.Code, - Nonce = nonce, + Nonce = nonce, }; Options.ProtocolValidator.Validate(jwt, protocolValidationContext); @@ -424,7 +443,7 @@ public override async Task AuthenticateAsync() if (message.Code != null) { - Logger.LogDebug(Resources.OIDCH_0014_CodeReceived, message.Code); + Logger.LogDebug(Resources.OIDCH_0014_AuthorizationCodeReceived, message.Code); if (ticket == null) { ticket = new AuthenticationTicket(properties, Options.AuthenticationScheme); @@ -436,20 +455,20 @@ public override async Task AuthenticateAsync() Code = message.Code, JwtSecurityToken = jwt, ProtocolMessage = message, - RedirectUri = ticket.Properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ? - ticket.Properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : string.Empty, + RedirectUri = ticket.Properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey) ? + ticket.Properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey] : string.Empty, }; await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification); if (authorizationCodeReceivedNotification.HandledResponse) { - Logger.LogInformation(Resources.OIDCH_0015_CodeReceivedNotificationHandledResponse); + Logger.LogVerbose(Resources.OIDCH_0015_AuthorizationCodeReceivedNotificationHandledResponse); return authorizationCodeReceivedNotification.AuthenticationTicket; } if (authorizationCodeReceivedNotification.Skipped) { - Logger.LogInformation(Resources.OIDCH_0016_CodeReceivedNotificationSkipped); + Logger.LogVerbose(Resources.OIDCH_0016_AuthorizationCodeReceivedNotificationSkipped); return null; } } @@ -463,7 +482,11 @@ public override async Task AuthenticateAsync() // 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))) { - Options.ConfigurationManager.RequestRefresh(); + if (Options.ConfigurationManager != null) + { + Logger.LogVerbose(Resources.OIDCH_0021_AutomaticConfigurationRefresh); + Options.ConfigurationManager.RequestRefresh(); + } } var authenticationFailedNotification = @@ -476,13 +499,13 @@ public override async Task AuthenticateAsync() await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); if (authenticationFailedNotification.HandledResponse) { - Logger.LogInformation(Resources.OIDCH_0018_AuthenticationFailedNotificationHandledResponse); + Logger.LogVerbose(Resources.OIDCH_0018_AuthenticationFailedNotificationHandledResponse); return authenticationFailedNotification.AuthenticationTicket; } if (authenticationFailedNotification.Skipped) { - Logger.LogInformation(Resources.OIDCH_0019_AuthenticationFailedNotificationSkipped); + Logger.LogVerbose(Resources.OIDCH_0019_AuthenticationFailedNotificationSkipped); return null; } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs index c999f5999..67c06b96d 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs @@ -93,7 +93,7 @@ internal static string OIDCH_0026_ApplyResponseChallengeAsync } /// - /// OIDCH_0027: converted 401 to 403. + /// OIDCH_0027: Converted 401 to 403. /// internal static string OIDCH_0027_401_ConvertedTo_403 { @@ -117,7 +117,7 @@ internal static string OIDCH_0029_ChallengeContextEqualsNull } /// - /// OIDCH_0030: using properties.RedirectUri for 'local redirect' post authentication: '{0}'. + /// OIDCH_0030: Using properties.RedirectUri for 'local redirect' post authentication: '{0}'. /// internal static string OIDCH_0030_Using_Properties_RedirectUri { @@ -125,7 +125,7 @@ internal static string OIDCH_0030_Using_Properties_RedirectUri } /// - /// OIDCH_0031: using Options.RedirectUri for 'redirect_uri': '{0}'. + /// OIDCH_0031: Using Options.RedirectUri for 'redirect_uri': '{0}'. /// internal static string OIDCH_0031_Using_Options_RedirectUri { @@ -133,7 +133,7 @@ internal static string OIDCH_0031_Using_Options_RedirectUri } /// - /// OIDCH_0032: using the CurrentUri for 'local redirect' post authentication: '{0}'. + /// OIDCH_0032: Using the CurrentUri for 'local redirect' post authentication: '{0}'. /// internal static string OIDCH_0032_UsingCurrentUriRedirectUri { @@ -145,11 +145,11 @@ internal static string OIDCH_0032_UsingCurrentUriRedirectUri /// internal static string OIDCH_0033_NonceAlreadyExists { - get { return ResourceManager.GetString("OIDCH_0033_NonceAlreadyExists"); } + get { return ResourceManager.GetString("OIDCH_0033_NonceAlreadyExists"); } } /// - /// OIDCH_0034: redirectToIdentityProviderNotification.HandledResponse + /// OIDCH_0034: RedirectToIdentityProviderNotification.HandledResponse /// internal static string OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse { @@ -157,7 +157,7 @@ internal static string OIDCH_0034_RedirectToIdentityProviderNotificationHandledR } /// - /// OIDCH_0035: redirectToIdentityProviderNotification.Skipped + /// OIDCH_0035: RedirectToIdentityProviderNotification.Skipped /// internal static string OIDCH_0035_RedirectToIdentityProviderNotificationSkipped { @@ -165,13 +165,21 @@ internal static string OIDCH_0035_RedirectToIdentityProviderNotificationSkipped } /// - /// OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}", (redirectUri ?? "null")) + /// OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}'.) /// internal static string OIDCH_0036_UriIsNotWellFormed { get { return ResourceManager.GetString("OIDCH_0036_UriIsNotWellFormed"); } } + /// + /// OIDCH_0036: RedirectUri is: '{0}'. + /// + internal static string OIDCH_0037_RedirectUri + { + get { return ResourceManager.GetString("OIDCH_0037_RedirectUri"); } + } + /// /// OIDCH_0000: Entering: '{0}'. /// @@ -197,7 +205,7 @@ internal static string FormatOIDCH_0001_MessageReceived(object p0) } /// - /// OIDCH_0002: messageReceivedNotification.HandledResponse + /// OIDCH_0002: MessageReceivedNotification.HandledResponse /// internal static string OIDCH_0002_MessageReceivedNotificationHandledResponse { @@ -205,7 +213,7 @@ internal static string OIDCH_0002_MessageReceivedNotificationHandledResponse } /// - /// OIDCH_0003: messageReceivedNotification.Skipped + /// OIDCH_0003: MessageReceivedNotification.Skipped /// internal static string OIDCH_0003_MessageReceivedNotificationSkipped { @@ -213,15 +221,15 @@ internal static string OIDCH_0003_MessageReceivedNotificationSkipped } /// - /// OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or whitespace. State is required to process the message. + /// OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or empty. /// - internal static string OIDCH_0004_MessageStateIsNullOrWhiteSpace + internal static string OIDCH_0004_MessageStateIsNullOrEmpty { - get { return ResourceManager.GetString("OIDCH_0004_MessageStateIsNullOrWhiteSpace"); } + get { return ResourceManager.GetString("OIDCH_0004_MessageStateIsNullOrEmpty"); } } /// - /// OIDCH_0005: unable to unprotect the message.State + /// OIDCH_0005: Unable to unprotect the message.State /// internal static string OIDCH_0005_MessageStateIsInvalid { @@ -229,15 +237,15 @@ internal static string OIDCH_0005_MessageStateIsInvalid } /// - /// OIDCH_0006_MessageErrorNotNull: '{0}'. + /// OIDCH_0006: Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'. /// - internal static string OIDCH_0006_MessageErrorNotNull + internal static string OIDCH_0006_MessageContainsError { - get { return ResourceManager.GetString("OIDCH_0006_MessageErrorNotNull"); } + get { return ResourceManager.GetString("OIDCH_0006_MessageContainsError"); } } /// - /// OIDCH_0007: updating configuration + /// OIDCH_0007: Updating configuration /// internal static string OIDCH_0007_UpdatingConfiguration { @@ -245,7 +253,7 @@ internal static string OIDCH_0007_UpdatingConfiguration } /// - /// OIDCH_0008: securityTokenReceivedNotification.HandledResponse + /// OIDCH_0008: SecurityTokenReceivedNotification.HandledResponse /// internal static string OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse { @@ -253,7 +261,7 @@ internal static string OIDCH_0008_SecurityTokenReceivedNotificationHandledRespon } /// - /// OIDCH_0009: securityTokenReceivedNotification.Skipped + /// OIDCH_0009: SecurityTokenReceivedNotification.Skipped /// internal static string OIDCH_0009_SecurityTokenReceivedNotificationSkipped { @@ -269,7 +277,7 @@ internal static string OIDCH_0010_ValidatedSecurityTokenNotJwt } /// - /// OIDCH_0011: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: {0}." + /// OIDCH_0011: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'. /// internal static string OIDCH_0011_UnableToValidateToken { @@ -277,7 +285,7 @@ internal static string OIDCH_0011_UnableToValidateToken } /// - /// OIDCH_0012: securityTokenValidatedNotification.HandledResponse + /// OIDCH_0012: SecurityTokenValidatedNotification.HandledResponse /// internal static string OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse { @@ -285,7 +293,7 @@ internal static string OIDCH_0012_SecurityTokenValidatedNotificationHandledRespo } /// - /// OIDCH_0013: securityTokenValidatedNotification.Skipped + /// OIDCH_0013: SecurityTokenValidatedNotification.Skipped /// internal static string OIDCH_0013_SecurityTokenValidatedNotificationSkipped { @@ -293,31 +301,31 @@ internal static string OIDCH_0013_SecurityTokenValidatedNotificationSkipped } /// - /// OIDCH_0014: 'code' received: '{0}' + /// OIDCH_0014: AuthorizationCode received: '{0}' /// - internal static string OIDCH_0014_CodeReceived + internal static string OIDCH_0014_AuthorizationCodeReceived { - get { return ResourceManager.GetString("OIDCH_0014_CodeReceived"); } + get { return ResourceManager.GetString("OIDCH_0014_AuthorizationCodeReceived"); } } /// - /// OIDCH_0015: codeReceivedNotification.HandledResponse") + /// OIDCH_0015: AuthorizationCodeReceivedNotification.HandledResponse /// - internal static string OIDCH_0015_CodeReceivedNotificationHandledResponse + internal static string OIDCH_0015_AuthorizationCodeReceivedNotificationHandledResponse { - get { return ResourceManager.GetString("OIDCH_0015_CodeReceivedNotificationHandledResponse"); } + get { return ResourceManager.GetString("OIDCH_0015_AuthorizationCodeReceivedNotificationHandledResponse"); } } /// /// OIDCH_0016: codeReceivedNotification.Skipped /// - internal static string OIDCH_0016_CodeReceivedNotificationSkipped + internal static string OIDCH_0016_AuthorizationCodeReceivedNotificationSkipped { - get { return ResourceManager.GetString("OIDCH_0016_CodeReceivedNotificationSkipped"); } + get { return ResourceManager.GetString("OIDCH_0016_AuthorizationCodeReceivedNotificationSkipped"); } } /// - /// OIDCH_0017: Exception occurred while processing message + /// OIDCH_0017: Exception occurred while processing message. /// internal static string OIDCH_0017_ExceptionOccurredWhileProcessingMessage { @@ -341,11 +349,20 @@ internal static string OIDCH_0019_AuthenticationFailedNotificationSkipped } /// - /// OIDCH_0020: 'id_token' received: '{0}' + /// OIDCH_0020: 'id_token' received: '{0}'. /// internal static string OIDCH_0020_IdTokenReceived { get { return ResourceManager.GetString("OIDCH_0020_IdTokenReceived"); } } + + /// + /// OIDCH_0021: exception of type 'SecurityTokenSignatureKeyNotFoundException' thrown, Options.ConfigurationManager.RequestRefresh() called. + /// + internal static string OIDCH_0021_AutomaticConfigurationRefresh + { + get { return ResourceManager.GetString("OIDCH_0021_AutomaticConfigurationRefresh"); } + } + } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx index 3f2ad6022..f998d95ad 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx @@ -130,7 +130,7 @@ OIDCH_0026: Entering: '{0}' - OIDCH_0027: converted 401 to 403. + OIDCH_0027: Converted 401 to 403. OIDCH_0028: Response.StatusCode != 401, StatusCode: '{0}'. @@ -139,10 +139,10 @@ OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication - OIDCH_0030: using properties.RedirectUri for 'local redirect' post authentication: '{0}'. + OIDCH_0030: Using properties.RedirectUri for 'local redirect' post authentication: '{0}'. - OIDCH_0031: using Options.RedirectUri for 'redirect_uri': '{0}'. + OIDCH_0031: Using Options.RedirectUri for 'redirect_uri': '{0}'. OIDCH_0032: using the CurrentUri for 'local redirect' post authentication: '{0}'. @@ -151,13 +151,16 @@ OIDCH_0033: ProtocolValidator.RequireNonce == true. The generated nonce already exists: this usually indicates the nonce is not unique or has been used. The nonce is: '{0}'. - OIDCH_0034: redirectToIdentityProviderNotification.HandledResponse + OIDCH_0034: RedirectToIdentityProviderNotification.HandledResponse - OIDCH_0035: redirectToIdentityProviderNotification.Skipped + OIDCH_0035: RedirectToIdentityProviderNotification.Skipped - OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}", (redirectUri ?? "null")) + OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: '{0}'. + + + OIDCH_0037: RedirectUri is: '{0}'. OIDCH_0000: Entering: '{0}'. @@ -166,60 +169,63 @@ OIDCH_0001: MessageReceived: '{0}'. - OIDCH_0002: messageReceivedNotification.HandledResponse + OIDCH_0002: MessageReceivedNotification.HandledResponse - OIDCH_0003: messageReceivedNotification.Skipped + OIDCH_0003: MessageReceivedNotification.Skipped - - OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or whitespace. State is required to process the message. + + OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or empty. - - OIDCH_0005: unable to unprotect the message.State + + OIDCH_0005: Unable to unprotect the message.State. - - OIDCH_0006_MessageErrorNotNull: '{0}'. + + OIDCH_0006: Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'. - OIDCH_0007: updating configuration + OIDCH_0007: Updating configuration - OIDCH_0008: securityTokenReceivedNotification.HandledResponse + OIDCH_0008: SecurityTokenReceivedNotification.HandledResponse - OIDCH_0009: securityTokenReceivedNotification.Skipped + 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_0011: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'." - OIDCH_0012: securityTokenValidatedNotification.HandledResponse + OIDCH_0012: SecurityTokenValidatedNotification.HandledResponse - OIDCH_0013: securityTokenValidatedNotification.Skipped + OIDCH_0013: SecurityTokenValidatedNotification.Skipped - - OIDCH_0014: 'code' received: '{0}' + + OIDCH_0014: AuthorizationCode received: '{0}'. - - OIDCH_0015: codeReceivedNotification.HandledResponse + + OIDCH_0015: AuthorizationCodeReceivedNotification.HandledResponse - - OIDCH_0016: codeReceivedNotification.Skipped + + OIDCH_0016: AuthorizationCodeReceivedNotification.Skipped - - OIDCH_0017: Exception occurred while processing message + + OIDCH_0017: Exception occurred while processing message. - OIDCH_0018: authenticationFailedNotification.HandledResponse + OIDCH_0018: AuthenticationFailedNotification.HandledResponse - OIDCH_0019: authenticationFailedNotification.Skipped + OIDCH_0019: AuthenticationFailedNotification.Skipped OIDCH_0020: 'id_token' received: '{0}' - \ No newline at end of file + + OIDCH_0021: exception of type 'SecurityTokenSignatureKeyNotFoundException' thrown, Options.ConfigurationManager.RequestRefresh() called. + + diff --git a/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs b/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs index a4b2d979e..d8b7dcac9 100644 --- a/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs +++ b/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs @@ -1,16 +1,28 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNet.Http; +using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Authentication.Notifications { + /// + /// When a user configures the to be notified prior to redirecting to an IdentityProvider + /// an instance of is passed to the 'RedirectToIdentityProviderNotification". + /// + /// protocol specific message. + /// protocol specific options. public class RedirectToIdentityProviderNotification : BaseNotification { - public RedirectToIdentityProviderNotification(HttpContext context, TOptions options) : base(context, options) + public RedirectToIdentityProviderNotification([NotNull] HttpContext context, [NotNull] TOptions options) : base(context, options) { } - public TMessage ProtocolMessage { get; set; } + /// + /// Gets or sets the . + /// + /// if 'value' is null. + public TMessage ProtocolMessage { get; [param: NotNull] set; } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/AuthenticationPropertiesFormaterKeyValue.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/AuthenticationPropertiesFormaterKeyValue.cs new file mode 100644 index 000000000..683f03563 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/AuthenticationPropertiesFormaterKeyValue.cs @@ -0,0 +1,62 @@ +// 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 System; +using System.Text; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + /// + /// This formatter creates an easy to read string of the format: "'key1' 'value1' ..." + /// + public class AuthenticationPropertiesFormaterKeyValue : ISecureDataFormat + { + string _protectedString = Guid.NewGuid().ToString(); + + public string Protect(AuthenticationProperties data) + { + if (data == null || data.Items.Count == 0) + { + return "null"; + } + + var encoder = UrlEncoder.Default; + var sb = new StringBuilder(); + foreach(var item in data.Items) + { + sb.Append(encoder.UrlEncode(item.Key) + " " + encoder.UrlEncode(item.Value) + " "); + } + + return sb.ToString(); + } + + AuthenticationProperties ISecureDataFormat.Unprotect(string protectedText) + { + if (string.IsNullOrWhiteSpace(protectedText)) + { + return null; + } + + if (protectedText == "null") + { + return new AuthenticationProperties(); + } + + string[] items = protectedText.Split(' '); + if (items.Length % 2 != 0) + { + return null; + } + + var propeties = new AuthenticationProperties(); + for (int i = 0; i < items.Length - 1; i+=2) + { + propeties.Items.Add(items[i], items[i + 1]); + } + + return propeties; + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/ExpectedQueryValues.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/ExpectedQueryValues.cs new file mode 100644 index 000000000..ddf7a0e7e --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/ExpectedQueryValues.cs @@ -0,0 +1,175 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.Framework.WebEncoders; +using Microsoft.IdentityModel.Protocols; +using Xunit; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + /// + /// This helper class is used to check that query string parameters are as expected. + /// + public class ExpectedQueryValues + { + public ExpectedQueryValues(string authority, OpenIdConnectConfiguration configuration = null) + { + Authority = authority; + Configuration = configuration ?? TestUtilities.DefaultOpenIdConnectConfiguration; + } + + public static ExpectedQueryValues Defaults(string authority) + { + var result = new ExpectedQueryValues(authority); + result.Scope = OpenIdConnectScopes.OpenIdProfile; + result.ResponseType = OpenIdConnectResponseTypes.CodeIdToken; + return result; + } + + public void CheckValues(string query, IEnumerable parameters) + { + var errors = new List(); + if (!query.StartsWith(ExpectedAuthority)) + { + errors.Add("ExpectedAuthority: " + ExpectedAuthority); + } + + foreach(var str in parameters) + { + if (str == OpenIdConnectParameterNames.ClientId) + { + if (!query.Contains(ExpectedClientId)) + errors.Add("ExpectedClientId: " + ExpectedClientId); + + continue; + } + + if (str == OpenIdConnectParameterNames.RedirectUri) + { + if(!query.Contains(ExpectedRedirectUri)) + errors.Add("ExpectedRedirectUri: " + ExpectedRedirectUri); + + continue; + } + + if (str == OpenIdConnectParameterNames.Resource) + { + if(!query.Contains(ExpectedResource)) + errors.Add("ExpectedResource: " + ExpectedResource); + + continue; + } + + if (str == OpenIdConnectParameterNames.ResponseMode) + { + if(!query.Contains(ExpectedResponseMode)) + errors.Add("ExpectedResponseMode: " + ExpectedResponseMode); + + continue; + } + + if (str == OpenIdConnectParameterNames.Scope) + { + if (!query.Contains(ExpectedScope)) + errors.Add("ExpectedScope: " + ExpectedScope); + + continue; + } + + if (str == OpenIdConnectParameterNames.State) + { + if (!query.Contains(ExpectedState)) + errors.Add("ExpectedState: " + ExpectedState); + + continue; + } + } + + if (errors.Count > 0) + { + var sb = new StringBuilder(); + sb.AppendLine("query string not as expected: " + Environment.NewLine + query + Environment.NewLine); + foreach (var str in errors) + { + sb.AppendLine(str); + } + + Debug.WriteLine(sb.ToString()); + Assert.True(false, sb.ToString()); + } + } + + public UrlEncoder Encoder { get; set; } = UrlEncoder.Default; + + public string Authority { get; set; } + + public string ClientId { get; set; } = Guid.NewGuid().ToString(); + + public string RedirectUri { get; set; } = Guid.NewGuid().ToString(); + + public OpenIdConnectRequestType RequestType { get; set; } = OpenIdConnectRequestType.AuthenticationRequest; + + public string Resource { get; set; } = Guid.NewGuid().ToString(); + + public string ResponseMode { get; set; } = OpenIdConnectResponseModes.FormPost; + + public string ResponseType { get; set; } = Guid.NewGuid().ToString(); + + public string Scope { get; set; } = Guid.NewGuid().ToString(); + + public string State { get; set; } = Guid.NewGuid().ToString(); + + public string ExpectedAuthority + { + get + { + if (RequestType == OpenIdConnectRequestType.TokenRequest) + { + return Configuration?.EndSessionEndpoint ?? Authority + @"/oauth2/token"; + } + else if (RequestType == OpenIdConnectRequestType.LogoutRequest) + { + return Configuration?.TokenEndpoint ?? Authority + @"/oauth2/logout"; + } + + return Configuration?.AuthorizationEndpoint ?? Authority + (@"/oauth2/authorize"); + } + } + + public OpenIdConnectConfiguration Configuration { get; set; } + + public string ExpectedClientId + { + get { return OpenIdConnectParameterNames.ClientId + "=" + Encoder.UrlEncode(ClientId); } + } + + public string ExpectedRedirectUri + { + get { return OpenIdConnectParameterNames.RedirectUri + "=" + Encoder.UrlEncode(RedirectUri); } + } + + public string ExpectedResource + { + get { return OpenIdConnectParameterNames.Resource + "=" + Encoder.UrlEncode(Resource); } + } + + public string ExpectedResponseMode + { + get { return OpenIdConnectParameterNames.ResponseMode + "=" + Encoder.UrlEncode(ResponseMode); } + } + + public string ExpectedScope + { + get { return OpenIdConnectParameterNames.Scope + "=" + Encoder.UrlEncode(Scope); } + } + + public string ExpectedState + { + get { return OpenIdConnectParameterNames.State + "=" + Encoder.UrlEncode(State); } + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/InMemoryLogger.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/InMemoryLogger.cs new file mode 100644 index 000000000..67a2668c6 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/InMemoryLogger.cs @@ -0,0 +1,57 @@ +// 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 System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + public class InMemoryLogger : ILogger, IDisposable + { + LogLevel _logLevel = 0; + + public InMemoryLogger(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)) + { + var logEntry = + new LogEntry + { + EventId = eventId, + Exception = exception, + Formatter = formatter, + Level = logLevel, + State = state, + }; + + _logEntries.Add(logEntry); + Debug.WriteLine(logEntry.ToString()); + } + } + + public List Logs { get { return _logEntries; } } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/InMemoryLoggerFactory.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/InMemoryLoggerFactory.cs new file mode 100644 index 000000000..aaa1e975d --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/InMemoryLoggerFactory.cs @@ -0,0 +1,36 @@ +// 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.Framework.Logging; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + public class InMemoryLoggerFactory : ILoggerFactory + { + InMemoryLogger _logger; + LogLevel _logLevel = LogLevel.Debug; + + public InMemoryLoggerFactory(LogLevel logLevel) + { + _logLevel = logLevel; + _logger = new InMemoryLogger(_logLevel); + } + + public LogLevel MinimumLevel + { + get { return _logLevel; } + set { _logLevel = value; } + } + + public void AddProvider(ILoggerProvider provider) + { + } + + public ILogger CreateLogger(string categoryName) + { + return _logger; + } + + public InMemoryLogger Logger { get { return _logger; } } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LogEntry.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LogEntry.cs new file mode 100644 index 000000000..6f4619578 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LogEntry.cs @@ -0,0 +1,41 @@ +// 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. + +using System; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + 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; + } + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LoggingUtilities.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LoggingUtilities.cs new file mode 100644 index 000000000..64f17d967 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LoggingUtilities.cs @@ -0,0 +1,142 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + public class LoggingUtilities + { + static List CompleteLogEntries; + static Dictionary LogEntries; + + static LoggingUtilities() + { + LogEntries = + new Dictionary() + { + { "OIDCH_0000:", LogLevel.Debug }, + { "OIDCH_0001:", LogLevel.Debug }, + { "OIDCH_0002:", LogLevel.Verbose }, + { "OIDCH_0003:", LogLevel.Verbose }, + { "OIDCH_0004:", LogLevel.Warning }, + { "OIDCH_0005:", LogLevel.Error }, + { "OIDCH_0006:", LogLevel.Error }, + { "OIDCH_0007:", LogLevel.Verbose }, + { "OIDCH_0008:", LogLevel.Verbose }, + { "OIDCH_0009:", LogLevel.Verbose }, + { "OIDCH_0010:", LogLevel.Error }, + { "OIDCH_0011:", LogLevel.Error }, + { "OIDCH_0012:", LogLevel.Verbose }, + { "OIDCH_0013:", LogLevel.Verbose }, + { "OIDCH_0014:", LogLevel.Debug }, + { "OIDCH_0015:", LogLevel.Verbose }, + { "OIDCH_0016:", LogLevel.Verbose }, + { "OIDCH_0017:", LogLevel.Error }, + { "OIDCH_0018:", LogLevel.Verbose }, + { "OIDCH_0019:", LogLevel.Verbose }, + { "OIDCH_0020:", LogLevel.Debug }, + { "OIDCH_0021:", LogLevel.Verbose }, + { "OIDCH_0026:", LogLevel.Error }, + }; + + BuildLogEntryList(); + } + + /// + /// Builds the complete list of OpenIdConnect 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 }); + } + } + + /// + /// 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 + public static void CheckLogs(List capturedLogs, List expectedLogs, List> errors) + { + if (capturedLogs.Count >= expectedLogs.Count) + { + for (int i = 0; i < capturedLogs.Count; i++) + { + if (i + 1 > expectedLogs.Count) + { + errors.Add(new Tuple(capturedLogs[i], null)); + } + else + { + if (!TestUtilities.AreEqual(capturedLogs[i], expectedLogs[i])) + { + errors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); + } + } + } + } + else + { + for (int i = 0; i < expectedLogs.Count; i++) + { + if (i + 1 > capturedLogs.Count) + { + errors.Add(new Tuple(null, expectedLogs[i])); + } + else + { + if (!TestUtilities.AreEqual(expectedLogs[i], capturedLogs[i])) + { + errors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); + } + } + } + } + } + + public static string LoggingErrors(List> errors) + { + string loggingErrors = null; + if (errors.Count > 0) + { + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(""); + foreach (var error in errors) + { + stringBuilder.AppendLine("*Captured*, *Expected* : *" + (error.Item1?.ToString() ?? "null") + "*, *" + (error.Item2?.ToString() ?? "null") + "*"); + } + + loggingErrors = stringBuilder.ToString(); + } + + return loggingErrors; + } + + /// + /// 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. + public static List PopulateLogEntries(int[] items) + { + var entries = new List(); + foreach (var item in items) + { + entries.Add(CompleteLogEntries[item]); + } + + return entries; + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationHandlerForTestingAuthenticate.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationHandlerForTestingAuthenticate.cs new file mode 100644 index 000000000..bf24d7791 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationHandlerForTestingAuthenticate.cs @@ -0,0 +1,41 @@ +// 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 System; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.AspNet.Http.Features.Authentication; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + /// + /// Allows for custom processing of ApplyResponseChallenge, ApplyResponseGrant and AuthenticateCore + /// + public class OpenIdConnectAuthenticationHandlerForTestingAuthenticate : OpenIdConnectAuthenticationHandler + { + public OpenIdConnectAuthenticationHandlerForTestingAuthenticate() + : base() + { + } + + protected override async Task HandleUnauthorizedAsync(ChallengeContext context) + { + return await base.HandleUnauthorizedAsync(context); + } + + protected override Task HandleSignInAsync(SignInContext context) + { + return Task.FromResult(0); + } + + protected override Task HandleSignOutAsync(SignOutContext context) + { + return Task.FromResult(0); + } + + //public override bool ShouldHandleScheme(string authenticationScheme) + //{ + // return true; + //} + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationMiddlewareForTestingAuthenticate.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationMiddlewareForTestingAuthenticate.cs new file mode 100644 index 000000000..b88438e0e --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationMiddlewareForTestingAuthenticate.cs @@ -0,0 +1,47 @@ +// 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 System; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + + /// + /// pass a as the AuthenticationHandler + /// configured to handle certain messages. + /// + public class OpenIdConnectAuthenticationMiddlewareForTestingAuthenticate : OpenIdConnectAuthenticationMiddleware + { + OpenIdConnectAuthenticationHandler _handler; + + public OpenIdConnectAuthenticationMiddlewareForTestingAuthenticate( + RequestDelegate next, + IDataProtectionProvider dataProtectionProvider, + ILoggerFactory loggerFactory, + IUrlEncoder encoder, + IServiceProvider services, + IOptions externalOptions, + IOptions options, + ConfigureOptions configureOptions = null, + OpenIdConnectAuthenticationHandler handler = null + ) + : base(next, dataProtectionProvider, loggerFactory, encoder, services, externalOptions, options, configureOptions) + { + _handler = handler; + var customFactory = loggerFactory as InMemoryLoggerFactory; + if (customFactory != null) + Logger = customFactory.Logger; + } + + protected override AuthenticationHandler CreateHandler() + { + return _handler ?? base.CreateHandler(); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs index 8896767ee..a5cf7074c 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs @@ -1,27 +1,25 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// 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.Collections.ObjectModel; +using System.Diagnostics; +using System.IdentityModel.Tokens; +using System.Linq; 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.Http.Features.Authentication; using Microsoft.AspNet.TestHost; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Logging; using Microsoft.Framework.OptionsModel; using Microsoft.Framework.WebEncoders; using Microsoft.IdentityModel.Protocols; +using Moq; using Shouldly; using Xunit; @@ -32,52 +30,9 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect /// 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 }); - } - } + private const string nonceForJwt = "abc"; + private static SecurityToken specCompliantJwt = new JwtSecurityToken("issuer", "audience", new List { new Claim("iat", EpochTime.GetIntDate(DateTime.UtcNow).ToString()), new Claim("nonce", nonceForJwt) }, DateTime.UtcNow, DateTime.UtcNow + TimeSpan.FromDays(1)); + private const string ExpectedStateParameter = "expectedState"; /// /// Sanity check that logging is filtering, hi / low water marks are checked @@ -85,7 +40,7 @@ private static void BuildLogEntryList() [Fact] public void LoggingLevel() { - var logger = new CustomLogger(LogLevel.Debug); + var logger = new InMemoryLogger(LogLevel.Debug); logger.IsEnabled(LogLevel.Critical).ShouldBe(true); logger.IsEnabled(LogLevel.Debug).ShouldBe(true); logger.IsEnabled(LogLevel.Error).ShouldBe(true); @@ -93,7 +48,7 @@ public void LoggingLevel() logger.IsEnabled(LogLevel.Verbose).ShouldBe(true); logger.IsEnabled(LogLevel.Warning).ShouldBe(true); - logger = new CustomLogger(LogLevel.Critical); + logger = new InMemoryLogger(LogLevel.Critical); logger.IsEnabled(LogLevel.Critical).ShouldBe(true); logger.IsEnabled(LogLevel.Debug).ShouldBe(false); logger.IsEnabled(LogLevel.Error).ShouldBe(false); @@ -102,214 +57,224 @@ public void LoggingLevel() 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() + [Theory, MemberData("AuthenticateCoreStateDataSet")] + public async Task AuthenticateCoreState(Action action, OpenIdConnectMessage message) { - //System.Diagnostics.Debugger.Launch(); - - var propertiesFormatter = new AuthenticationPropertiesFormater(); - var protectedProperties = propertiesFormatter.Protect(new AuthenticationProperties()); - var state = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + UrlEncoder.Default.UrlEncode(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); + var handler = new OpenIdConnectAuthenticationHandlerForTestingAuthenticate(); + var server = CreateServer(new ConfigureOptions(action), UrlEncoder.Default, handler); + await server.CreateClient().PostAsync("http://localhost", new FormUrlEncodedContent(message.Parameters.Where(pair => pair.Value != null))); } - /// - /// 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) + public static TheoryData, OpenIdConnectMessage> AuthenticateCoreStateDataSet { - 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); + get + { + var formater = new AuthenticationPropertiesFormaterKeyValue(); + var properties = new AuthenticationProperties(); + var dataset = new TheoryData, OpenIdConnectMessage>(); + + // expected user state is added to the message.Parameters.Items[ExpectedStateParameter] + // Userstate == null + var message = new OpenIdConnectMessage(); + message.State = UrlEncoder.Default.UrlEncode(formater.Protect(properties)); + message.Code = Guid.NewGuid().ToString(); + message.Parameters.Add(ExpectedStateParameter, null); + dataset.Add(SetStateOptions, message); + + // Userstate != null + message = new OpenIdConnectMessage(); + properties.Items.Clear(); + var userstate = Guid.NewGuid().ToString(); + message.Code = Guid.NewGuid().ToString(); + properties.Items.Add(OpenIdConnectAuthenticationDefaults.UserstatePropertiesKey, userstate); + message.State = UrlEncoder.Default.UrlEncode(formater.Protect(properties)); + message.Parameters.Add(ExpectedStateParameter, userstate); + dataset.Add(SetStateOptions, message); + return dataset; + } } - /// - /// 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) + // Setup a notification to check for expected state. + // The state gets set by the runtime after the 'MessageReceivedNotification' + private static void SetStateOptions(OpenIdConnectAuthenticationOptions options) { - var entries = new List(); - foreach(var item in items) + options.AuthenticationScheme = "OpenIdConnectHandlerTest"; + options.ConfigurationManager = TestUtilities.DefaultOpenIdConnectConfigurationManager; + options.ClientId = Guid.NewGuid().ToString(); + options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + options.Notifications = new OpenIdConnectAuthenticationNotifications { - entries.Add(CompleteLogEntries[item]); - } + AuthorizationCodeReceived = notification => + { + if (notification.ProtocolMessage.State == null && !notification.ProtocolMessage.Parameters.ContainsKey(ExpectedStateParameter)) + return Task.FromResult(null); + + if (notification.ProtocolMessage.State == null || !notification.ProtocolMessage.Parameters.ContainsKey(ExpectedStateParameter)) + Assert.True(false, "(notification.ProtocolMessage.State=!= null || !notification.ProtocolMessage.Parameters.ContainsKey(expectedState)"); - return entries; + Assert.Equal(notification.ProtocolMessage.State, notification.ProtocolMessage.Parameters[ExpectedStateParameter]); + return Task.FromResult(null); + } + }; } - private void DisplayLogs(List logs) + [Theory, MemberData("AuthenticateCoreDataSet")] + public async Task AuthenticateCore(LogLevel logLevel, int[] expectedLogIndexes, Action action, OpenIdConnectMessage message) { - foreach (var logentry in logs) - { - Console.WriteLine(logentry.ToString()); - } + var errors = new List>(); + var expectedLogs = LoggingUtilities.PopulateLogEntries(expectedLogIndexes); + var handler = new OpenIdConnectAuthenticationHandlerForTestingAuthenticate(); + var loggerFactory = new InMemoryLoggerFactory(logLevel); + var server = CreateServer(new ConfigureOptions(action), UrlEncoder.Default, loggerFactory, handler); + + await server.CreateClient().PostAsync("http://localhost", new FormUrlEncodedContent(message.Parameters)); + LoggingUtilities.CheckLogs(loggerFactory.Logger.Logs, expectedLogs, errors); + Debug.WriteLine(LoggingUtilities.LoggingErrors(errors)); + Assert.True(errors.Count == 0, LoggingUtilities.LoggingErrors(errors)); } - private void DisplayErrors(Dictionary>> errors) + public static TheoryData, OpenIdConnectMessage> AuthenticateCoreDataSet { - if (errors.Count > 0) + get { - 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); - } + var formater = new AuthenticationPropertiesFormaterKeyValue(); + var dataset = new TheoryData, OpenIdConnectMessage>(); + var properties = new AuthenticationProperties(); + var message = new OpenIdConnectMessage(); + var validState = UrlEncoder.Default.UrlEncode(formater.Protect(properties)); + message.State = validState; + + // MessageReceived - Handled / Skipped + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 2 }, MessageReceivedHandledOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 2 }, MessageReceivedHandledOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, MessageReceivedHandledOptions, message); + + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 3 }, MessageReceivedSkippedOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 3 }, MessageReceivedSkippedOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, MessageReceivedSkippedOptions, message); + + // State - null, empty string, invalid + message = new OpenIdConnectMessage(); + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7 }, StateNullOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 4, 7 }, StateNullOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, StateNullOptions, message); + + message = new OpenIdConnectMessage(); + message.State = string.Empty; + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7 }, StateEmptyOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 4, 7 }, StateEmptyOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, StateEmptyOptions, message); + + message = new OpenIdConnectMessage(); + message.State = Guid.NewGuid().ToString(); + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 5 }, StateInvalidOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 5 }, StateInvalidOptions, message); + dataset.Add(LogLevel.Error, new int[] { 5 }, StateInvalidOptions, message); + + // OpenIdConnectMessage.Error != null + message = new OpenIdConnectMessage(); + message.Error = "Error"; + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 6, 17, 18 }, MessageWithErrorOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 4, 6, 17, 18 }, MessageWithErrorOptions, message); + dataset.Add(LogLevel.Error, new int[] { 6, 17 }, MessageWithErrorOptions, message); + + // SecurityTokenReceived - Handled / Skipped + message = new OpenIdConnectMessage(); + message.IdToken = "invalid"; + message.State = validState; + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 8 }, SecurityTokenReceivedHandledOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 8 }, SecurityTokenReceivedHandledOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, SecurityTokenReceivedHandledOptions, message); + + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 9 }, SecurityTokenReceivedSkippedOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 9 }, SecurityTokenReceivedSkippedOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, SecurityTokenReceivedSkippedOptions, message); + + // SecurityTokenValidation - ReturnsNull, Throws, Validates + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 11, 17, 18 }, SecurityTokenValidatorCannotReadToken, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 11, 17, 18 }, SecurityTokenValidatorCannotReadToken, message); + dataset.Add(LogLevel.Error, new int[] { 11, 17 }, SecurityTokenValidatorCannotReadToken, message); + + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 17, 21, 18 }, SecurityTokenValidatorThrows, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 17, 21, 18 }, SecurityTokenValidatorThrows, message); + dataset.Add(LogLevel.Error, new int[] { 17 }, SecurityTokenValidatorThrows, message); + + message.Nonce = nonceForJwt; + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20 }, SecurityTokenValidatorValidatesAllTokens, message); + dataset.Add(LogLevel.Verbose, new int[] { 7 }, SecurityTokenValidatorValidatesAllTokens, message); + dataset.Add(LogLevel.Error, new int[] { }, SecurityTokenValidatorValidatesAllTokens, message); + + // SecurityTokenValidation - Handled / Skipped + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 12 }, SecurityTokenValidatedHandledOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 12 }, SecurityTokenValidatedHandledOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, SecurityTokenValidatedHandledOptions, message); + + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 13 }, SecurityTokenValidatedSkippedOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 13 }, SecurityTokenValidatedSkippedOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, SecurityTokenValidatedSkippedOptions, message); + + // AuthenticationCodeReceived - Handled / Skipped + message = new OpenIdConnectMessage(); + message.Code = Guid.NewGuid().ToString(); + message.State = validState; + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 14, 15 }, AuthorizationCodeReceivedHandledOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 15 }, AuthorizationCodeReceivedHandledOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, AuthorizationCodeReceivedHandledOptions, message); + + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 14, 16 }, AuthorizationCodeReceivedSkippedOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 16 }, AuthorizationCodeReceivedSkippedOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, AuthorizationCodeReceivedSkippedOptions, message); + + return dataset; } } - /// - /// 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) +#region Configure Options for AuthenticateCore variations + + private static void DefaultOptions(OpenIdConnectAuthenticationOptions options) { - var localErrors = new List>(); + options.AuthenticationScheme = "OpenIdConnectHandlerTest"; + options.SignInScheme = "OpenIdConnectHandlerTest"; + options.ConfigurationManager = TestUtilities.DefaultOpenIdConnectConfigurationManager; + options.ClientId = Guid.NewGuid().ToString(); + options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + } - 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++) + private static void AuthorizationCodeReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications { - if (i + 1 > capturedLogs.Count) - { - localErrors.Add(new Tuple(null, expectedLogs[i])); - } - else + AuthorizationCodeReceived = (notification) => { - if (!TestUtilities.AreEqual(expectedLogs[i], capturedLogs[i])) - { - localErrors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); - } + notification.HandleResponse(); + return Task.FromResult(null); } - } - } + }; + } - if (localErrors.Count != 0) - { - if (errors.ContainsKey(variation)) + private static void AuthorizationCodeReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications { - foreach (var error in localErrors) + AuthorizationCodeReceived = (notification) => { - errors[variation].Add(error); + notification.SkipToNextMiddleware(); + return Task.FromResult(null); } - } - else - { - errors[variation] = localErrors; - } - } + }; } - #region Configure Options - - private static void CodeReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) + private static void AuthenticationErrorHandledOptions(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { - AuthorizationCodeReceived = (notification) => + AuthenticationFailed = (notification) => { notification.HandleResponse(); return Task.FromResult(null); @@ -317,13 +282,13 @@ private static void CodeReceivedHandledOptions(OpenIdConnectAuthenticationOption }; } - private static void CodeReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) + private static void AuthenticationErrorSkippedOptions(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { - AuthorizationCodeReceived = (notification) => + AuthenticationFailed = (notification) => { notification.SkipToNextMiddleware(); return Task.FromResult(null); @@ -331,13 +296,6 @@ private static void CodeReceivedSkippedOptions(OpenIdConnectAuthenticationOption }; } - 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); @@ -366,6 +324,11 @@ private static void MessageReceivedSkippedOptions(OpenIdConnectAuthenticationOpt }; } + private static void MessageWithErrorOptions(OpenIdConnectAuthenticationOptions options) + { + AuthenticationErrorHandledOptions(options); + } + private static void SecurityTokenReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); @@ -394,9 +357,40 @@ private static void SecurityTokenReceivedSkippedOptions(OpenIdConnectAuthenticat }; } - private static void SecurityTokenValidatedHandledOptions(OpenIdConnectAuthenticationOptions options) + private static void SecurityTokenValidatorCannotReadToken(OpenIdConnectAuthenticationOptions options) + { + AuthenticationErrorHandledOptions(options); + var mockValidator = new Mock(); + SecurityToken jwt = null; + mockValidator.Setup(v => v.ValidateToken(It.IsAny(), It.IsAny(), out jwt)).Returns(new ClaimsPrincipal()); + mockValidator.Setup(v => v.CanReadToken(It.IsAny())).Returns(false); + options.SecurityTokenValidators = new Collection { mockValidator.Object }; + } + + private static void SecurityTokenValidatorThrows(OpenIdConnectAuthenticationOptions options) + { + AuthenticationErrorHandledOptions(options); + var mockValidator = new Mock(); + SecurityToken jwt = null; + mockValidator.Setup(v => v.ValidateToken(It.IsAny(), It.IsAny(), out jwt)).Throws(); + mockValidator.Setup(v => v.CanReadToken(It.IsAny())).Returns(true); + options.SecurityTokenValidators = new Collection { mockValidator.Object }; + } + + private static void SecurityTokenValidatorValidatesAllTokens(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); + var mockValidator = new Mock(); + mockValidator.Setup(v => v.ValidateToken(It.IsAny(), It.IsAny(), out specCompliantJwt)).Returns(new ClaimsPrincipal()); + mockValidator.Setup(v => v.CanReadToken(It.IsAny())).Returns(true); + options.SecurityTokenValidators = new Collection { mockValidator.Object }; + options.ProtocolValidator.RequireTimeStampInNonce = false; + options.ProtocolValidator.RequireNonce = false; + } + + private static void SecurityTokenValidatedHandledOptions(OpenIdConnectAuthenticationOptions options) + { + SecurityTokenValidatorValidatesAllTokens(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -410,7 +404,7 @@ private static void SecurityTokenValidatedHandledOptions(OpenIdConnectAuthentica private static void SecurityTokenValidatedSkippedOptions(OpenIdConnectAuthenticationOptions options) { - DefaultOptions(options); + SecurityTokenValidatorValidatesAllTokens(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -422,14 +416,31 @@ private static void SecurityTokenValidatedSkippedOptions(OpenIdConnectAuthentica }; } - #endregion + private static void StateNullOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + } + + private static void StateEmptyOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + } + + private static void StateInvalidOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + } + +#endregion + + private static Task EmptyTask() { return Task.FromResult(0); } - private static TestServer CreateServer(IOptions options, ILoggerFactory loggerFactory) + private static TestServer CreateServer(ConfigureOptions options, IUrlEncoder encoder, OpenIdConnectAuthenticationHandler handler = null) { return TestServer.Create( app => { - app.UseCustomOpenIdConnectAuthentication(options, loggerFactory); + app.UseMiddleware(options, encoder, handler); app.Use(async (context, next) => { await next(); @@ -437,19 +448,18 @@ private static TestServer CreateServer(IOptions { - services.AddAuthentication(); services.AddWebEncoders(); services.AddDataProtection(); } ); } - private static TestServer CreateServer(CustomConfigureOptions configureOptions, ILoggerFactory loggerFactory) + private static TestServer CreateServer(ConfigureOptions configureOptions, IUrlEncoder encoder, ILoggerFactory loggerFactory, OpenIdConnectAuthenticationHandler handler = null) { return TestServer.Create( app => { - app.UseCustomOpenIdConnectAuthentication(configureOptions, loggerFactory); + app.UseMiddleware(configureOptions, encoder, loggerFactory, handler); app.Use(async (context, next) => { await next(); @@ -457,274 +467,10 @@ private static TestServer CreateServer(CustomConfigureOptions configureOptions, }, services => { - services.AddAuthentication(); services.AddWebEncoders(); 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); - } - } - - /// - /// 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 async Task BaseInitializeAsyncPublic(AuthenticationOptions options, HttpContext context, ILogger logger, IUrlEncoder encoder) - { - await base.BaseInitializeAsync(options, context, logger, encoder); - } - - protected override async Task HandleUnauthorizedAsync(ChallengeContext context) - { - var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) - { - }; - - await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); - return true; - } - } - - /// - /// 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, - IUrlEncoder encoder, - IServiceProvider services, - IOptions externalOptions, - IOptions options, - ConfigureOptions configureOptions = null - ) - : base(next, dataProtectionProvider, loggerFactory, encoder, services, externalOptions, options, configureOptions) - { - Logger = (loggerFactory as CustomLoggerFactory).Logger; - } - - protected override AuthenticationHandler CreateHandler() - { - return new CustomOpenIdConnectAuthenticationHandler(); - } - } - - 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/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs index e56da3b90..07182c64a 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs @@ -4,26 +4,22 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Security.Claims; -using System.Text; using System.Threading.Tasks; -using System.Xml; using System.Xml.Linq; using Microsoft.AspNet.Authentication.Cookies; -using Microsoft.AspNet.Authentication.DataHandler; 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.WebEncoders; -using Newtonsoft.Json; +using Microsoft.IdentityModel.Protocols; +using Moq; using Shouldly; using Xunit; @@ -33,25 +29,31 @@ public class OpenIdConnectMiddlewareTests { static string noncePrefix = "OpenIdConnect." + "Nonce."; static string nonceDelimiter = "."; + const string Challenge = "/challenge"; + const string ChallengeWithOutContext = "/challengeWithOutContext"; + const string ChallengeWithProperties = "/challengeWithProperties"; + const string DefaultHost = @"https://example.com"; + const string DefaultAuthority = @"https://example.com/common"; + const string ExpectedAuthorizeRequest = @"https://example.com/common/oauth2/signin"; + const string ExpectedLogoutRequest = @"https://example.com/common/oauth2/logout"; + const string Logout = "/logout"; + const string Signin = "/signin"; + const string Signout = "/signout"; [Fact] - public async Task ChallengeWillTriggerRedirect() + public async Task ChallengeWillSetDefaults() { + var stateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + var queryValues = ExpectedQueryValues.Defaults(DefaultAuthority); + queryValues.State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + stateDataFormat.Protect(new AuthenticationProperties()); var server = CreateServer(options => { - options.Authority = "https://login.windows.net/common"; - options.ClientId = "Test Id"; - options.SignInScheme = OpenIdConnectAuthenticationDefaults.AuthenticationScheme; + SetOptions(options, DefaultParameters(), queryValues); }); - var transaction = await SendAsync(server, "https://example.com/challenge"); + + var transaction = await SendAsync(server, DefaultHost + Challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - var location = transaction.Response.Headers.Location.ToString(); - location.ShouldContain("https://login.windows.net/common/oauth2/authorize?"); - location.ShouldContain("client_id="); - location.ShouldContain("&response_type="); - location.ShouldContain("&scope="); - location.ShouldContain("&state="); - location.ShouldContain("&response_mode="); + queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); } [Fact] @@ -59,95 +61,230 @@ public async Task ChallengeWillSetNonceCookie() { var server = CreateServer(options => { - options.Authority = "https://login.windows.net/common"; + options.Authority = DefaultAuthority; options.ClientId = "Test Id"; + options.Configuration = TestUtilities.DefaultOpenIdConnectConfiguration; }); - var transaction = await SendAsync(server, "https://example.com/challenge"); - transaction.SetCookie.Single().ShouldContain("OpenIdConnect.nonce."); + var transaction = await SendAsync(server, DefaultHost + Challenge); + transaction.SetCookie.Single().ShouldContain(OpenIdConnectAuthenticationDefaults.CookieNoncePrefix); } [Fact] - public async Task ChallengeWillSetDefaultScope() + public async Task ChallengeWillUseOptionsProperties() { + var queryValues = new ExpectedQueryValues(DefaultAuthority); var server = CreateServer(options => { - options.Authority = "https://login.windows.net/common"; - options.ClientId = "Test Id"; + SetOptions(options, DefaultParameters(), queryValues); }); - var transaction = await SendAsync(server, "https://example.com/challenge"); + + var transaction = await SendAsync(server, DefaultHost + Challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - transaction.Response.Headers.Location.Query.ShouldContain("&scope=" + UrlEncoder.Default.UrlEncode("openid profile")); + queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); } - [Fact] - public async Task ChallengeWillUseOptionsProperties() + /// + /// Tests RedirectToIdentityProviderNotification replaces the OpenIdConnectMesssage correctly. + /// + /// Task + [Theory] + [InlineData(Challenge, OpenIdConnectRequestType.AuthenticationRequest)] + [InlineData(Signout, OpenIdConnectRequestType.LogoutRequest)] + public async Task ChallengeSettingMessage(string challenge, OpenIdConnectRequestType requestType) + { + var configuration = new OpenIdConnectConfiguration + { + AuthorizationEndpoint = ExpectedAuthorizeRequest, + EndSessionEndpoint = ExpectedLogoutRequest + }; + + var queryValues = new ExpectedQueryValues(DefaultAuthority, configuration) + { + RequestType = requestType + }; + var server = CreateServer(SetProtocolMessageOptions); + var transaction = await SendAsync(server, DefaultHost + challenge); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, new string[] {}); + } + + private static void SetProtocolMessageOptions(OpenIdConnectAuthenticationOptions options) { + var mockOpenIdConnectMessage = new Mock(); + mockOpenIdConnectMessage.Setup(m => m.CreateAuthenticationRequestUrl()).Returns(ExpectedAuthorizeRequest); + mockOpenIdConnectMessage.Setup(m => m.CreateLogoutRequestUrl()).Returns(ExpectedLogoutRequest); + options.AutomaticAuthentication = true; + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + RedirectToIdentityProvider = (notification) => + { + notification.ProtocolMessage = mockOpenIdConnectMessage.Object; + return Task.FromResult(null); + } + }; + } + + /// + /// Tests for users who want to add 'state'. There are two ways to do it. + /// 1. Users set 'state' (OpenIdConnectMessage.State) in the notification. The runtime appends to that state. + /// 2. Users add to the AuthenticationProperties (notification.AuthenticationProperties), values will be serialized. + /// + /// + /// + [Theory, MemberData("StateDataSet")] + public async Task ChallengeSettingState(string userState, string challenge) + { + var queryValues = new ExpectedQueryValues(DefaultAuthority); + var stateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + var properties = new AuthenticationProperties(); + if (challenge == ChallengeWithProperties) + { + properties.Items.Add("item1", Guid.NewGuid().ToString()); + } + else + { + properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, queryValues.RedirectUri); + } + var server = CreateServer(options => { - options.Authority = "https://login.windows.net/common"; - options.ClientId = "Test Id"; - options.SignInScheme = OpenIdConnectAuthenticationDefaults.AuthenticationScheme; - options.Scope = "https://www.googleapis.com/auth/plus.login"; - options.ResponseType = "id_token"; - }); - var transaction = await SendAsync(server, "https://example.com/challenge"); + SetOptions(options, DefaultParameters(new string[] { OpenIdConnectParameterNames.State }), queryValues, stateDataFormat); + options.AutomaticAuthentication = challenge.Equals(ChallengeWithOutContext); + options.Notifications = new OpenIdConnectAuthenticationNotifications + { + RedirectToIdentityProvider = notification => + { + notification.ProtocolMessage.State = userState; + return Task.FromResult(null); + } + + }; + }, null, properties); + + var transaction = await SendAsync(server, DefaultHost + challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - var query = transaction.Response.Headers.Location.Query; - query.ShouldContain("scope=" + UrlEncoder.Default.UrlEncode("https://www.googleapis.com/auth/plus.login")); - query.ShouldContain("response_type=" + UrlEncoder.Default.UrlEncode("id_token")); + queryValues.State = stateDataFormat.Protect(properties); + queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters(new string[] { OpenIdConnectParameterNames.State })); + } + + public static TheoryData StateDataSet + { + get + { + var dataset = new TheoryData(); + dataset.Add(Guid.NewGuid().ToString(), Challenge); + dataset.Add(null, Challenge); + dataset.Add(Guid.NewGuid().ToString(), ChallengeWithOutContext); + dataset.Add(null, ChallengeWithOutContext); + dataset.Add(Guid.NewGuid().ToString(), ChallengeWithProperties); + dataset.Add(null, ChallengeWithProperties); + + return dataset; + } } [Fact] public async Task ChallengeWillUseNotifications() { - ISecureDataFormat stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + var queryValues = new ExpectedQueryValues(DefaultAuthority); + var queryValuesSetInNotification = new ExpectedQueryValues(DefaultAuthority); var server = CreateServer(options => { - options.Authority = "https://login.windows.net/common"; - options.ClientId = "Test Id"; + SetOptions(options, DefaultParameters(), queryValues); options.Notifications = new OpenIdConnectAuthenticationNotifications { - MessageReceived = notification => - { - notification.ProtocolMessage.Scope = "test openid profile"; - notification.HandleResponse(); - return Task.FromResult(null); - } + RedirectToIdentityProvider = notification => + { + notification.ProtocolMessage.ClientId = queryValuesSetInNotification.ClientId; + notification.ProtocolMessage.RedirectUri = queryValuesSetInNotification.RedirectUri; + notification.ProtocolMessage.Resource = queryValuesSetInNotification.Resource; + notification.ProtocolMessage.Scope = queryValuesSetInNotification.Scope; + return Task.FromResult(null); + } }; }); - var properties = new AuthenticationProperties(); - var state = stateFormat.Protect(properties); - var transaction = await SendAsync(server,"https://example.com/challenge"); + var transaction = await SendAsync(server, DefaultHost + Challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + queryValuesSetInNotification.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); + } + + private void SetOptions(OpenIdConnectAuthenticationOptions options, List parameters, ExpectedQueryValues queryValues, ISecureDataFormat secureDataFormat = null) + { + foreach (var param in parameters) + { + if (param.Equals(OpenIdConnectParameterNames.ClientId)) + options.ClientId = queryValues.ClientId; + else if (param.Equals(OpenIdConnectParameterNames.RedirectUri)) + options.RedirectUri = queryValues.RedirectUri; + else if (param.Equals(OpenIdConnectParameterNames.Resource)) + options.Resource = queryValues.Resource; + else if (param.Equals(OpenIdConnectParameterNames.Scope)) + options.Scope = queryValues.Scope; + } + + options.Authority = queryValues.Authority; + options.Configuration = queryValues.Configuration; + options.StateDataFormat = secureDataFormat ?? new AuthenticationPropertiesFormaterKeyValue(); } + private List DefaultParameters(string[] additionalParams = null) + { + var parameters = + new List + { + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.RedirectUri, + OpenIdConnectParameterNames.Resource, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + }; + + if (additionalParams != null) + parameters.AddRange(additionalParams); + + return parameters; + } + + private static void DefaultChallengeOptions(OpenIdConnectAuthenticationOptions options) + { + options.AuthenticationScheme = "OpenIdConnectHandlerTest"; + options.AutomaticAuthentication = true; + options.ClientId = Guid.NewGuid().ToString(); + options.ConfigurationManager = TestUtilities.DefaultOpenIdConnectConfigurationManager; + options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + } [Fact] public async Task SignOutWithDefaultRedirectUri() { + var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; var server = CreateServer(options => { - options.Authority = "https://login.windows.net/common"; + options.Authority = DefaultAuthority; options.ClientId = "Test Id"; + options.Configuration = configuration; }); - var transaction = await SendAsync(server, "https://example.com/signout"); + var transaction = await SendAsync(server, DefaultHost + Signout); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - transaction.Response.Headers.Location.AbsoluteUri.ShouldBe("https://login.windows.net/common/oauth2/logout"); + transaction.Response.Headers.Location.AbsoluteUri.ShouldBe(configuration.EndSessionEndpoint); } [Fact] public async Task SignOutWithCustomRedirectUri() { + var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; var server = CreateServer(options => { - options.Authority = "https://login.windows.net/common"; + options.Authority = DefaultAuthority; options.ClientId = "Test Id"; + options.Configuration = configuration; options.PostLogoutRedirectUri = "https://example.com/logout"; }); - var transaction = await SendAsync(server, "https://example.com/signout"); + var transaction = await SendAsync(server, DefaultHost + Signout); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); transaction.Response.Headers.Location.AbsoluteUri.ShouldContain(UrlEncoder.Default.UrlEncode("https://example.com/logout")); } @@ -155,10 +292,12 @@ public async Task SignOutWithCustomRedirectUri() [Fact] public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properites() { + var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; var server = CreateServer(options => { - options.Authority = "https://login.windows.net/common"; + options.Authority = DefaultAuthority; options.ClientId = "Test Id"; + options.Configuration = configuration; options.PostLogoutRedirectUri = "https://example.com/logout"; }); @@ -167,30 +306,7 @@ public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properite transaction.Response.Headers.Location.AbsoluteUri.ShouldContain(UrlEncoder.Default.UrlEncode("http://www.example.com/specific_redirect_uri")); } - [Fact] - // Test Cases for calculating the expiration time of cookie from cookie name - public void NonceCookieExpirationTime() - { - DateTime utcNow = DateTime.UtcNow; - - GetNonceExpirationTime(noncePrefix + DateTime.MaxValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MaxValue); - - GetNonceExpirationTime(noncePrefix + DateTime.MinValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue + TimeSpan.FromHours(1)); - - GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(utcNow + TimeSpan.FromHours(1)); - - GetNonceExpirationTime(noncePrefix, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); - - GetNonceExpirationTime("", TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); - - GetNonceExpirationTime(noncePrefix + noncePrefix, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); - - GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(utcNow + TimeSpan.FromHours(1)); - - GetNonceExpirationTime(utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); - } - - private static TestServer CreateServer(Action configureOptions, Func handler = null) + private static TestServer CreateServer(Action configureOptions, Func handler = null, AuthenticationProperties properties = null) { return TestServer.Create(app => { @@ -203,15 +319,25 @@ private static TestServer CreateServer(Action SendAsync(TestServer server, string uri, { request.Headers.Add("Cookie", cookieHeader); } + var transaction = new Transaction { Request = request, Response = await server.CreateClient().SendAsync(request), }; + if (transaction.Response.Headers.Contains("Set-Cookie")) { transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); } - transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); if (transaction.Response.Content != null && transaction.Response.Content.Headers.ContentType != null && transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") { transaction.ResponseElement = XElement.Parse(transaction.ResponseText); } + return transaction; } private class Transaction { public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } public IList SetCookie { get; set; } public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } public string AuthenticationCookieValue @@ -294,57 +425,29 @@ public string AuthenticationCookieValue return null; } } - - public string FindClaimValue(string claimType) - { - XElement claim = ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); - if (claim == null) - { - return null; - } - return claim.Attribute("value").Value; - } - } - private static void Describe(HttpResponse res, ClaimsIdentity identity) - { - res.StatusCode = 200; - res.ContentType = "text/xml"; - var xml = new XElement("xml"); - if (identity != null) - { - xml.Add(identity.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); - } - using (var memory = new MemoryStream()) - { - using (var writer = new XmlTextWriter(memory, Encoding.UTF8)) - { - xml.WriteTo(writer); - } - res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length); - } } - private class TestHttpMessageHandler : HttpMessageHandler + [Fact] + // Test Cases for calculating the expiration time of cookie from cookie name + public void NonceCookieExpirationTime() { - public Func Sender { get; set; } + DateTime utcNow = DateTime.UtcNow; - protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) - { - if (Sender != null) - { - return Task.FromResult(Sender(request)); - } + GetNonceExpirationTime(noncePrefix + DateTime.MaxValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MaxValue); - return Task.FromResult(null); - } - } + GetNonceExpirationTime(noncePrefix + DateTime.MinValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue + TimeSpan.FromHours(1)); - private static HttpResponseMessage ReturnJsonResponse(object content) - { - var res = new HttpResponseMessage(HttpStatusCode.OK); - var text = JsonConvert.SerializeObject(content); - res.Content = new StringContent(text, Encoding.UTF8, "application/json"); - return res; + GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(utcNow + TimeSpan.FromHours(1)); + + GetNonceExpirationTime(noncePrefix, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); + + GetNonceExpirationTime("", TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); + + GetNonceExpirationTime(noncePrefix + noncePrefix, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); + + GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(utcNow + TimeSpan.FromHours(1)); + + GetNonceExpirationTime(utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); } private static DateTime GetNonceExpirationTime(string keyname, TimeSpan nonceLifetime) @@ -373,8 +476,8 @@ private static DateTime GetNonceExpirationTime(string keyname, TimeSpan nonceLif } } } + return nonceTime; } - } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs index 1e6bea793..13479fdce 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.IdentityModel.Protocols; namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect { @@ -10,6 +11,8 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect /// public class TestUtilities { + public const string DefaultHost = @"http://localhost"; + public static bool AreEqual(object obj1, object obj2, Func comparer = null) where T : class { if (obj1 == null && obj2 == null) @@ -63,11 +66,6 @@ private static bool AreEqual(LogEntry logEntry1, LogEntry logEntry2) return false; } - if (!AreEqual(logEntry1.Exception, logEntry2.Exception)) - { - return false; - } - if (logEntry1.State == null && logEntry2.State == null) { return true; @@ -104,5 +102,26 @@ private static bool AreEqual(Exception exception1, Exception exception2) return AreEqual(exception1.InnerException, exception2.InnerException); } + + static public IConfigurationManager DefaultOpenIdConnectConfigurationManager + { + get + { + return new StaticConfigurationManager(DefaultOpenIdConnectConfiguration); + } + } + + static public OpenIdConnectConfiguration DefaultOpenIdConnectConfiguration + { + get + { + return new OpenIdConnectConfiguration() + { + AuthorizationEndpoint = @"https://login.windows.net/common/oauth2/authorize", + EndSessionEndpoint = @"https://login.windows.net/common/oauth2/endsessionendpoint", + TokenEndpoint = @"https://login.windows.net/common/oauth2/token", + }; + } + } } }