diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs index 1bde86c83..88253c8dd 100644 --- a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs @@ -23,8 +23,6 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer /// public class OAuthBearerAuthenticationMiddleware : AuthenticationMiddleware { - private readonly ILogger _logger; - /// /// Bearer authentication component which is added to an HTTP pipeline. This constructor is not /// called by application code directly, instead it is added by calling the the IAppBuilder UseOAuthBearerAuthentication diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs index 3ea2a4578..ec87e283f 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs @@ -57,7 +57,7 @@ protected override async Task ApplyResponseGrantAsync() _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, @@ -69,21 +69,20 @@ protected override async Task ApplyResponseGrantAsync() 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)) { - openIdConnectMessage.PostLogoutRedirectUri = Options.PostLogoutRedirectUri; + message.PostLogoutRedirectUri = Options.PostLogoutRedirectUri; } - var notification = new RedirectToIdentityProviderNotification(Context, Options) + var notification = new RedirectToIdentityProviderNotification(Context, Options, message, properties); + if (Options.Notifications.RedirectToIdentityProvider != null) { - ProtocolMessage = openIdConnectMessage - }; - - await Options.Notifications.RedirectToIdentityProvider(notification); + await Options.Notifications.RedirectToIdentityProvider(notification); + } - if (!notification.HandledResponse) + if (!notification.HandledResponse || !notification.Skipped) { var redirectUri = notification.ProtocolMessage.CreateLogoutRequestUrl(); if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) @@ -174,18 +173,36 @@ protected override async Task ApplyResponseChallengeAsync() 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) { message.Nonce = Options.ProtocolValidator.GenerateNonce(); + } + + var redirectToIdentityProviderNotification = + new RedirectToIdentityProviderNotification(Context, Options, message, properties); + + await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); + if (redirectToIdentityProviderNotification.HandledResponse) + { + Logger.LogInformation(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse); + return; + } + else if (redirectToIdentityProviderNotification.Skipped) + { + Logger.LogInformation(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped); + return; + } + + if (!string.IsNullOrWhiteSpace(message.Nonce)) + { if (Options.NonceCache != null) { if (!Options.NonceCache.TryAddNonce(message.Nonce)) @@ -200,21 +217,15 @@ protected override async Task ApplyResponseChallengeAsync() } } - var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) - { - ProtocolMessage = message - }; - - await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); - if (redirectToIdentityProviderNotification.HandledResponse) + // If 'OpenIdConnectMessage.State' is null, then the user never set it in the notification. Just set the state + if (string.IsNullOrWhiteSpace(redirectToIdentityProviderNotification.ProtocolMessage.State)) { - Logger.LogInformation(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse); - return; + redirectToIdentityProviderNotification.ProtocolMessage.State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + Options.StateDataFormat.Protect(properties); } - else if (redirectToIdentityProviderNotification.Skipped) + // the user did set 'OpenIdConnectMessage.State, add a parameter to the state + else { - Logger.LogInformation(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped); - return; + redirectToIdentityProviderNotification.ProtocolMessage.State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + Options.StateDataFormat.Protect(properties) + "&userstate=" + redirectToIdentityProviderNotification.ProtocolMessage.State; } var redirectUri = redirectToIdentityProviderNotification.ProtocolMessage.CreateAuthenticationRequestUrl(); @@ -223,6 +234,8 @@ protected override async Task ApplyResponseChallengeAsync() Logger.LogWarning(Resources.OIDCH_0036_UriIsNotWellFormed, redirectUri); } + Logger.LogDebug(Resources.OIDCH_0037_RedirectUri, redirectUri); + Response.Redirect(redirectUri); } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs index 70e45d0c2..811ba1d97 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -32,6 +32,8 @@ public class OpenIdConnectAuthenticationMiddleware : AuthenticationMiddlewareThe next middleware in the ASP.NET pipeline to invoke. /// provider for creating a data protector. /// factory for creating a . + /// for encoding query strings. + /// /// a instance that will supply /// if configureOptions is null. /// a instance that will be passed to an instance of @@ -39,11 +41,11 @@ public class OpenIdConnectAuthenticationMiddleware : AuthenticationMiddleware options, [NotNull] IDataProtectionProvider dataProtectionProvider, [NotNull] ILoggerFactory loggerFactory, [NotNull] IUrlEncoder encoder, [NotNull] IOptions externalOptions, - [NotNull] IOptions options, ConfigureOptions configureOptions = null) : base(next, options, loggerFactory, encoder, configureOptions) { diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs index a4f15d6e9..baaf8c5e5 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// public class OpenIdConnectAuthenticationNotifications { + Func, Task> _redirectToIdentityProvider; + /// /// Creates a new set of notifications. Each notification has a default no-op behavior unless otherwise documented. /// @@ -44,7 +46,19 @@ public OpenIdConnectAuthenticationNotifications() /// /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge. /// - public Func, Task> RedirectToIdentityProvider { get; set; } + public Func, Task> RedirectToIdentityProvider + { + get { return _redirectToIdentityProvider; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _redirectToIdentityProvider = value; + } + } /// /// Invoked with the security token that has been extracted from the protocol message. @@ -55,6 +69,5 @@ public OpenIdConnectAuthenticationNotifications() /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. /// public Func, Task> SecurityTokenValidated { get; set; } - } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs index b95de8318..fdfe87564 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs @@ -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}'. /// @@ -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 { @@ -341,7 +349,7 @@ internal static string OIDCH_0019_AuthenticationFailedNotificationSkipped } /// - /// OIDCH_0020: 'id_token' received: '{0}' + /// OIDCH_0020: 'id_token' received: '{0}'. /// internal static string OIDCH_0020_IdTokenReceived { diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx index 454dae209..60eb3d5e1 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx @@ -157,7 +157,10 @@ 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}'. diff --git a/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs b/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs index 1c069b4e2..349c51bf8 100644 --- a/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs +++ b/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs @@ -1,16 +1,64 @@ // 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.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Authentication.Notifications { + /// + /// When a user configures the to be notified prior to redirecting to an IdentityProvider + /// and instance of is passed to 'RedirectToIdentityProvider". + /// + /// protocol specific message. + /// protocol specific options. public class RedirectToIdentityProviderNotification : BaseNotification { - public RedirectToIdentityProviderNotification(HttpContext context, TOptions options) : base(context, options) + TMessage _message; + AuthenticationProperties _properties; + + public RedirectToIdentityProviderNotification([NotNull] HttpContext context, [NotNull] TOptions options, [NotNull] TMessage protocolMessage, [NotNull] AuthenticationProperties properties ) : base(context, options) + { + ProtocolMessage = protocolMessage; + AuthenticationProperties = properties; + } + + /// + /// Gets or sets the . + /// + /// if 'value' is null. + public TMessage ProtocolMessage { + get { return _message; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _message = value; + } } - public TMessage ProtocolMessage { get; set; } + /// + /// Gets or sets the . + /// + /// if 'value' is null. + public AuthenticationProperties AuthenticationProperties + { + get { return _properties; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _properties = value; + } + } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/DerivedTypes.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/DerivedTypes.cs new file mode 100644 index 000000000..abc1422de --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/DerivedTypes.cs @@ -0,0 +1,360 @@ +// 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 System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; +using Microsoft.IdentityModel.Protocols; + +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) + { + var propeties = new AuthenticationProperties(); + if (protectedText != "null") + { + string[] items = protectedText.Split(' '); + for (int i = 0; i < items.Length - 1; i+=2) + { + propeties.Items.Add(items[i], items[i + 1]); + } + } + + return propeties; + } + } + + /// + /// This formatter always throws. message and exception type can be set. + /// Defaults: + /// message: "AuthenticationPropertiesFormaterThrows" + /// type: InvalidOperationException + /// + public class AuthenticationPropertiesFormaterThrows : ISecureDataFormat + { + AuthenticationPropertiesFormaterThrows(string message = "AuthenticationPropertiesFormaterThrows", Type exceptionType = null) + { + Message = message; + ExceptionType = exceptionType ?? typeof(InvalidOperationException); + } + + public Type ExceptionType { get; set; } + + public string Message { get; set; } + + public string Protect(AuthenticationProperties data) + { + throw (Exception)Activator.CreateInstance(ExceptionType, Message); + } + + AuthenticationProperties ISecureDataFormat.Unprotect(string protectedText) + { + throw (Exception)Activator.CreateInstance(ExceptionType, Message); + } + } + + /// + /// This formatter returns values passed in the constructor + /// Defaults: + /// Protect: null + /// UnProtect: null + /// + public class AuthenticationPropertiesFormaterSetReturn : ISecureDataFormat + { + string _protect; + AuthenticationProperties _unprotect; + + AuthenticationPropertiesFormaterSetReturn(string protect = null, AuthenticationProperties unprotect = null) + { + _protect = protect; + _unprotect = unprotect; + } + + public string Protect(AuthenticationProperties data) + { + return _protect; + } + + AuthenticationProperties ISecureDataFormat.Unprotect(string protectedText) + { + return _unprotect; + } + } + + public class CustomConfigureOptions : ConfigureOptions + { + public CustomConfigureOptions(Action action) + : base(action) + { + } + + public override void Configure(OpenIdConnectAuthenticationOptions options, string name = "") + { + base.Configure(options, name); + return; + } + } + + 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)) + { + 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; } } + } + + 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; } } + } + + /// + /// Allows for controlling methods and capturing and exposing artifacts. + /// + public class CustomOpenIdConnectAuthenticationHandler : OpenIdConnectAuthenticationHandler + { + private Func _applyResponseChallenge; + private Action _challengeAction; + private Func _shouldHandleScheme; + + public CustomOpenIdConnectAuthenticationHandler(Func applyResponseChallenge = null, Action challengeAction = null, Func shouldHandleScheme = null ) + : base() + { + _applyResponseChallenge = applyResponseChallenge; + _challengeAction = challengeAction; + _shouldHandleScheme = shouldHandleScheme; + } + + protected override void ApplyResponseChallenge() + { + if (_applyResponseChallenge != null) + _applyResponseChallenge(); + else + base.ApplyResponseChallenge(); + } + + protected override async Task ApplyResponseChallengeAsync() + { + if (_applyResponseChallenge != null) + await _applyResponseChallenge(); + else + await base.ApplyResponseChallengeAsync(); + } + + /// + /// Captures Ticket + /// + /// + protected override AuthenticationTicket AuthenticateCore() + { + Ticket = base.AuthenticateCoreAsync().GetAwaiter().GetResult(); + return Ticket; + } + + /// + /// Captures Ticket + /// + protected override async Task AuthenticateCoreAsync() + { + Ticket = await base.AuthenticateCoreAsync(); + return Ticket; + } + + public override void Challenge(ChallengeContext context) + { + if (_challengeAction != null) + _challengeAction(context); + else + base.Challenge(context); + } + + protected override Task InitializeCoreAsync() + { + base.InitializeCoreAsync(); + return Task.FromResult(0); + } + + public override bool ShouldHandleScheme(string authenticationScheme) + { + if (_shouldHandleScheme != null) + return _shouldHandleScheme(authenticationScheme); + else + return base.ShouldHandleScheme(authenticationScheme); + } + + public ILogger LoggerPublic { get { return Logger; } } + + public OpenIdConnectAuthenticationOptions OptionsPublic { get; set; } + + public IUrlEncoder UrlEncoderPublic { get { return UrlEncoder; } } + + public AuthenticationTicket Ticket { get; set; } + } + + /// + /// pass a as the AuthenticationHandler + /// configured to handle certain messages. + /// + public class CustomOpenIdConnectAuthenticationMiddleware : OpenIdConnectAuthenticationMiddleware + { + OpenIdConnectAuthenticationHandler _handler; + + public CustomOpenIdConnectAuthenticationMiddleware( + RequestDelegate next, + IOptions options, + IDataProtectionProvider dataProtectionProvider, + ILoggerFactory loggerFactory, + IUrlEncoder encoder, + IOptions externalOptions, + ConfigureOptions configureOptions = null, + OpenIdConnectAuthenticationHandler handler = null + ) + : base(next, options, dataProtectionProvider, loggerFactory, encoder, externalOptions, configureOptions) + { + _handler = handler; + Logger = (loggerFactory as CustomLoggerFactory).Logger; + var customHandler = _handler as CustomOpenIdConnectAuthenticationHandler; + if (customHandler != null) + { + customHandler.OptionsPublic = Options; + } + } + + protected override AuthenticationHandler CreateHandler() + { + return _handler ?? base.CreateHandler(); + } + } + + public class CustomOpenIdConnectMessage : OpenIdConnectMessage + { + } + + /// + /// 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; + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/Extensions.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/Extensions.cs new file mode 100644 index 000000000..7d852c103 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/Extensions.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 Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.AspNet.Builder; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + /// + /// 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, IUrlEncoder encoder, ILoggerFactory loggerFactory, OpenIdConnectAuthenticationHandler handler = null) + { + return app.UseMiddleware(customConfigureOption, encoder, loggerFactory, handler); + } + + /// + /// 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, IUrlEncoder encoder, ILoggerFactory loggerFactory, OpenIdConnectAuthenticationHandler handler = null) + { + return app.UseMiddleware(options, encoder, loggerFactory, handler); + } + } +} 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..63e71f54e --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LoggingUtilities.cs @@ -0,0 +1,198 @@ +// 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.Diagnostics; +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.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 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(string variation, List capturedLogs, List expectedLogs, Dictionary>> errors) + { + var localErrors = new List>(); + + if (capturedLogs.Count >= expectedLogs.Count) + { + for (int i = 0; i < capturedLogs.Count; i++) + { + if (i + 1 > expectedLogs.Count) + { + localErrors.Add(new Tuple(capturedLogs[i], null)); + } + else + { + if (!TestUtilities.AreEqual(capturedLogs[i], expectedLogs[i])) + { + localErrors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); + } + } + } + } + else + { + for (int i = 0; i < expectedLogs.Count; i++) + { + if (i + 1 > capturedLogs.Count) + { + localErrors.Add(new Tuple(null, expectedLogs[i])); + } + else + { + if (!TestUtilities.AreEqual(expectedLogs[i], capturedLogs[i])) + { + localErrors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); + } + } + } + } + + if (localErrors.Count != 0) + { + if (errors.ContainsKey(variation)) + { + foreach (var error in localErrors) + { + errors[variation].Add(error); + } + } + else + { + errors[variation] = localErrors; + } + } + } + + public static void DebugWriteLineLogs(List logs, string message = null) + { + if (!string.IsNullOrWhiteSpace(message)) + Debug.WriteLine(message); + + foreach (var logentry in logs) + Debug.WriteLine(logentry.ToString()); + } + + public static void DebugWriteLineLoggingErrors(Dictionary>> errors) + { + if (errors.Count > 0) + { + foreach (var error in errors) + { + Debug.WriteLine("Error in Variation: " + error.Key); + foreach (var logError in error.Value) + { + Debug.WriteLine("*Captured*, *Expected* : *" + (logError.Item1?.ToString() ?? "null") + "*, *" + (logError.Item2?.ToString() ?? "null") + "*"); + } + + Debug.WriteLine(Environment.NewLine); + } + } + } + + /// + /// 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; + } + } + + 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/OpenIdConnectHandlerTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs index 1134c7300..46a644837 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs @@ -1,20 +1,13 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// this controls if the logs are written to the console. -// they can be reviewed for general content. -//#define _Verbose - using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net.Http; -using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNet.Authentication.Notifications; using Microsoft.AspNet.Authentication.OpenIdConnect; using Microsoft.AspNet.Builder; -using Microsoft.AspNet.DataProtection; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.TestHost; using Microsoft.Framework.DependencyInjection; @@ -32,53 +25,6 @@ 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 }); - } - } - /// /// Sanity check that logging is filtering, hi / low water marks are checked /// @@ -106,206 +52,99 @@ public void LoggingLevel() /// 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. + /// captures the logs so they can be analyzed. /// /// [Fact] - public async Task AuthenticateCore() + public async Task AuthenticateCoreLogging() { - //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 formater = new AuthenticationPropertiesFormaterKeyValue(); + var message = new OpenIdConnectMessage(); + var properties = new AuthenticationProperties(); - 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"; + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 4 }, Default.Options, message, errors, "V1-"); + message.State = ""; - logsEntriesExpected = new int[] { 0, 1, 2 }; - await RunVariation(LogLevel.Debug, message, MessageReceivedHandledOptions, errors, logsEntriesExpected); + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 4 }, Default.Options, message, errors, "V2-"); - logsEntriesExpected = new int[]{ 2 }; - await RunVariation(LogLevel.Information, message, MessageReceivedHandledOptions, errors, logsEntriesExpected); + message.State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + UrlEncoder.Default.UrlEncode(formater.Protect(properties)); + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 7 }, CodeReceivedHandledOptions, message, errors, "V3-"); - logsEntriesExpected = new int[] { 0, 1, 3 }; - await RunVariation(LogLevel.Debug, message, MessageReceivedSkippedOptions, errors, logsEntriesExpected); + message.State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + UrlEncoder.Default.UrlEncode(formater.Protect(properties)) + "&UserState=foo"; + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 7 }, CodeReceivedHandledOptions, message, errors, "V4-"); - logsEntriesExpected = new int[] { 3 }; - await RunVariation(LogLevel.Information, message, MessageReceivedSkippedOptions, errors, logsEntriesExpected); + properties.RedirectUri = Default.LocalHost; + message.State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + UrlEncoder.Default.UrlEncode(formater.Protect(new AuthenticationProperties())) + "&UserState=bar"; + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 7 }, CodeReceivedHandledOptions, message, errors, "V5-"); - logsEntriesExpected = new int[] {0, 1, 7, 20, 8 }; - await RunVariation(LogLevel.Debug, message, SecurityTokenReceivedHandledOptions, errors, logsEntriesExpected); + message.Code = Guid.NewGuid().ToString(); + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 7, 14, 15 }, CodeReceivedHandledOptions, message, errors, "V6-"); + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 7, 14, 16 }, CodeReceivedSkippedOptions, message, errors, "V7-"); + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 7, 14 }, Default.Options, message, errors, "V8-"); + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 2 }, MessageReceivedHandledOptions, message, errors, "V9-"); + await RunVariation(LogLevel.Information, new int[] { 2 }, MessageReceivedHandledOptions, message, errors, "V10-"); + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 3 }, MessageReceivedSkippedOptions, message, errors, "V11-"); + await RunVariation(LogLevel.Information, new int[] { 3 }, MessageReceivedSkippedOptions, message, errors, "V12-"); - logsEntriesExpected = new int[] {0, 1, 7, 20, 9 }; - await RunVariation(LogLevel.Debug, message, SecurityTokenReceivedSkippedOptions, errors, logsEntriesExpected); + message.IdToken = "invalid"; + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 7, 20, 8 }, SecurityTokenReceivedHandledOptions, message, errors, "V13-"); + await RunVariation(LogLevel.Debug, new int[] { 0, 1, 7, 20, 9 }, SecurityTokenReceivedSkippedOptions, message, errors, "V14-"); -#if _Verbose - Console.WriteLine("\n ===== \n"); - DisplayErrors(errors); -#endif + LoggingUtilities.DebugWriteLineLoggingErrors(errors); errors.Count.ShouldBe(0); } /// - /// Tests that processes a messaage as expected. - /// The test runs two independant paths: Using and + /// Tests that processed a message as expected. + /// The test runs two independant paths: Using and . /// /// for this variation - /// the that has arrived + /// the expected log entries /// 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) + /// Note: create a new for PostAsync. Internal state is maintained this will cause issue. + private async Task RunVariation(LogLevel logLevel, int[] expectedLogIndexes, Action action, OpenIdConnectMessage message, Dictionary>> errors, string testId) { - 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 variation = testId + action.Method.ToString().Substring(5, action.Method.ToString().IndexOf('(') - 5); + var expectedLogs = LoggingUtilities.PopulateLogEntries(expectedLogIndexes); + LoggingUtilities.DebugWriteLineLogs(expectedLogs, "\n======\nVariation: " + variation + ", LogLevel: " + logLevel.ToString() + Environment.NewLine + Environment.NewLine + "Expected Logs: "); + + Debug.WriteLine(Environment.NewLine + "Logs using ConfigureOptions:"); + + var handler = new CustomOpenIdConnectAuthenticationHandler(EmptyTask, EmptyChallenge, ReturnTrue); 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); - } + var server = CreateServer(new CustomConfigureOptions(action), UrlEncoder.Default, loggerFactory, handler); - /// - /// Populates a list of expected log entries for a test variation. - /// - /// the index for the in CompleteLogEntries of interest. - /// a that represents the expected entries for a test variation. - private List PopulateLogEntries(int[] items) - { - var entries = new List(); - foreach(var item in items) - { - entries.Add(CompleteLogEntries[item]); - } + await server.CreateClient().PostAsync("http://localhost", new FormUrlEncodedContent(message.Parameters)); + LoggingUtilities.CheckLogs(variation + ":ConfigOptions, LogLevel: " + logLevel.ToString(), loggerFactory.Logger.Logs, expectedLogs, errors); - return entries; - } + Debug.WriteLine(Environment.NewLine + "Logs using IOptions:"); + handler = new CustomOpenIdConnectAuthenticationHandler(EmptyTask, EmptyChallenge, ReturnTrue); + server = CreateServer(new Options(action), UrlEncoder.Default, new CustomLoggerFactory(logLevel), handler); - private void DisplayLogs(List logs) - { - foreach (var logentry in logs) - { - Console.WriteLine(logentry.ToString()); - } + var response = await server.CreateClient().PostAsync("http://localhost", new FormUrlEncodedContent(message.Parameters)); + LoggingUtilities.CheckLogs(variation + ":IOptions, LogLevel: " + logLevel.ToString(), loggerFactory.Logger.Logs, expectedLogs, errors); } - private void DisplayErrors(Dictionary>> errors) - { - if (errors.Count > 0) - { - foreach (var error in errors) - { - Console.WriteLine("Error in Variation: " + error.Key); - foreach (var logError in error.Value) - { - Console.WriteLine("*Captured*, *Expected* : *" + (logError.Item1?.ToString() ?? "null") + "*, *" + (logError.Item2?.ToString() ?? "null") + "*"); - } - Console.WriteLine(Environment.NewLine); - } - } - } + #region HandlerTasks - /// - /// Adds to errors if a variation if any are found. - /// - /// if this has been seen before, errors will be appended, test results are easier to understand if this is unique. - /// these are the logs the runtime generated - /// these are the errors that were expected - /// the dictionary to record any errors - private void CheckLogs(string variation, List capturedLogs, List expectedLogs, Dictionary>> errors) - { - var localErrors = new List>(); + private static void EmptyChallenge(ChallengeContext context) { } - if (capturedLogs.Count >= expectedLogs.Count) - { - for (int i = 0; i < capturedLogs.Count; i++) - { - if (i + 1 > expectedLogs.Count) - { - localErrors.Add(new Tuple(capturedLogs[i], null)); - } - else - { - if (!TestUtilities.AreEqual(capturedLogs[i], expectedLogs[i])) - { - localErrors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); - } - } - } - } - else - { - for (int i = 0; i < expectedLogs.Count; i++) - { - if (i + 1 > capturedLogs.Count) - { - localErrors.Add(new Tuple(null, expectedLogs[i])); - } - else - { - if (!TestUtilities.AreEqual(expectedLogs[i], capturedLogs[i])) - { - localErrors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); - } - } - } - } + private static Task EmptyTask() { return Task.FromResult(0); } - if (localErrors.Count != 0) - { - if (errors.ContainsKey(variation)) - { - foreach (var error in localErrors) - { - errors[variation].Add(error); - } - } - else - { - errors[variation] = localErrors; - } - } + private static bool ReturnTrue(string authenticationScheme) + { + return true; } + #endregion + #region Configure Options private static void CodeReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) { - DefaultOptions(options); + Default.Options(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -319,7 +158,7 @@ private static void CodeReceivedHandledOptions(OpenIdConnectAuthenticationOption private static void CodeReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) { - DefaultOptions(options); + Default.Options(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -331,16 +170,9 @@ 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); + Default.Options(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -354,7 +186,7 @@ private static void MessageReceivedHandledOptions(OpenIdConnectAuthenticationOpt private static void MessageReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) { - DefaultOptions(options); + Default.Options(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -368,7 +200,7 @@ private static void MessageReceivedSkippedOptions(OpenIdConnectAuthenticationOpt private static void SecurityTokenReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) { - DefaultOptions(options); + Default.Options(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -382,7 +214,7 @@ private static void SecurityTokenReceivedHandledOptions(OpenIdConnectAuthenticat private static void SecurityTokenReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) { - DefaultOptions(options); + Default.Options(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -396,7 +228,7 @@ private static void SecurityTokenReceivedSkippedOptions(OpenIdConnectAuthenticat private static void SecurityTokenValidatedHandledOptions(OpenIdConnectAuthenticationOptions options) { - DefaultOptions(options); + Default.Options(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -410,7 +242,7 @@ private static void SecurityTokenValidatedHandledOptions(OpenIdConnectAuthentica private static void SecurityTokenValidatedSkippedOptions(OpenIdConnectAuthenticationOptions options) { - DefaultOptions(options); + Default.Options(options); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -424,12 +256,12 @@ private static void SecurityTokenValidatedSkippedOptions(OpenIdConnectAuthentica #endregion - private static TestServer CreateServer(IOptions options, ILoggerFactory loggerFactory) + private static TestServer CreateServer(IOptions options, IUrlEncoder encoder, ILoggerFactory loggerFactory, OpenIdConnectAuthenticationHandler handler = null) { return TestServer.Create( app => { - app.UseCustomOpenIdConnectAuthentication(options, loggerFactory); + app.UseCustomOpenIdConnectAuthentication(options, encoder, loggerFactory, handler); app.Use(async (context, next) => { await next(); @@ -443,12 +275,12 @@ private static TestServer CreateServer(IOptions { - app.UseCustomOpenIdConnectAuthentication(configureOptions, loggerFactory); + app.UseCustomOpenIdConnectAuthentication(configureOptions, encoder, loggerFactory, handler); app.Use(async (context, next) => { await next(); @@ -462,278 +294,4 @@ private static TestServer CreateServer(CustomConfigureOptions configureOptions, ); } } - - /// - /// 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); - } - - public override bool ShouldHandleScheme(string authenticationScheme) - { - return true; - } - - public override void Challenge(ChallengeContext context) - { - } - - protected override void ApplyResponseChallenge() - { - } - - protected override async Task ApplyResponseChallengeAsync() - { - var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) - { - }; - - await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); - } - } - - /// - /// Used to set as the AuthenticationHandler - /// which can be configured to handle certain messages. - /// - public class CustomOpenIdConnectAuthenticationMiddleware : OpenIdConnectAuthenticationMiddleware - { - public CustomOpenIdConnectAuthenticationMiddleware( - RequestDelegate next, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - IUrlEncoder encoder, - IOptions externalOptions, - IOptions options, - ConfigureOptions configureOptions = null - ) - : base(next, dataProtectionProvider, loggerFactory, encoder, 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 da290cca1..9a02fcf7b 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs @@ -4,26 +4,21 @@ 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 Shouldly; using Xunit; @@ -33,25 +28,29 @@ 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://login.windows.net/common"; + 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 +58,179 @@ public async Task ChallengeWillSetNonceCookie() { var server = CreateServer(options => { - options.Authority = "https://login.windows.net/common"; + options.Authority = DefaultAuthority; options.ClientId = "Test Id"; + options.Configuration = ConfigurationManager.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 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] + [InlineData(true, Challenge)] + [InlineData(false, Challenge)] + [InlineData(true, ChallengeWithOutContext)] + [InlineData(false, ChallengeWithOutContext)] + [InlineData(true, ChallengeWithProperties)] + [InlineData(false, ChallengeWithProperties)] + public async Task ChallengeSettingState(bool userSetsState, string challenge) { + var queryValues = new ExpectedQueryValues(DefaultAuthority); + var localProperties = new AuthenticationProperties(); + var stateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + AuthenticationProperties challengeProperties = null; + if (challenge == ChallengeWithProperties) + { + challengeProperties = new AuthenticationProperties(); + challengeProperties.Items.Add("item1", Guid.NewGuid().ToString()); + } + 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); + options.AutomaticAuthentication = challenge.Equals(ChallengeWithOutContext); + options.Notifications = new OpenIdConnectAuthenticationNotifications + { + RedirectToIdentityProvider = notification => + { + if (userSetsState) + { + notification.ProtocolMessage.State = queryValues.State; + } + localProperties = new AuthenticationProperties(notification.AuthenticationProperties.Items); + return Task.FromResult(null); + } + + }; + }, null, challengeProperties); + + 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")); + if (userSetsState) + { + queryValues.State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + stateDataFormat.Protect(localProperties) + "&userstate=" + queryValues.State; + } + else + { + queryValues.State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + stateDataFormat.Protect(localProperties); + } + + queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters(new string[] { OpenIdConnectParameterNames.State })); } [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; + } [Fact] public async Task SignOutWithDefaultRedirectUri() { + var configuration = ConfigurationManager.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 = ConfigurationManager.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 +238,12 @@ public async Task SignOutWithCustomRedirectUri() [Fact] public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properites() { + var configuration = ConfigurationManager.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,53 +252,40 @@ 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 => { app.UseCookieAuthentication(options => { - options.AuthenticationScheme = "OpenIdConnect"; + options.AuthenticationScheme = OpenIdConnectAuthenticationDefaults.AuthenticationScheme; }); app.UseOpenIdConnectAuthentication(configureOptions); app.Use(async (context, next) => { var req = context.Request; var res = context.Response; - if (req.Path == new PathString("/challenge")) + + if (req.Path == new PathString(Challenge)) { - context.Authentication.Challenge("OpenIdConnect"); + context.Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationScheme); res.StatusCode = 401; } - else if (req.Path == new PathString("/signin")) + else if (req.Path == new PathString(ChallengeWithProperties)) + { + context.Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationScheme, properties); + res.StatusCode = 401; + } + else if (req.Path == new PathString(ChallengeWithOutContext)) + { + res.StatusCode = 401; + } + else if (req.Path == new PathString(Signin)) { // REVIEW: this used to just be res.SignIn() - context.Authentication.SignIn("OpenIdConnect", new ClaimsPrincipal()); + context.Authentication.SignIn(OpenIdConnectAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal()); } - else if (req.Path == new PathString("/signout")) + else if (req.Path == new PathString(Signout)) { context.Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationScheme); } @@ -273,11 +345,13 @@ private static async Task SendAsync(TestServer server, string uri, 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 @@ -296,57 +370,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) @@ -375,8 +421,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 abb5b8538..de7efe92e 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs @@ -1,8 +1,14 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.IdentityModel.Protocols; using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.Framework.WebEncoders; +using Microsoft.IdentityModel.Protocols; +using Xunit; namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect { @@ -106,4 +112,208 @@ private static bool AreEqual(Exception exception1, Exception exception2) return AreEqual(exception1.InnerException, exception2.InnerException); } } + + public class Default + { + public const string LocalHost = @"http://localhost"; + + public static void Options(OpenIdConnectAuthenticationOptions options) + { + options.AuthenticationScheme = "OpenIdConnectHandlerTest"; + options.ConfigurationManager = ConfigurationManager.DefaultStaticConfigurationManager(); + options.ClientId = Guid.NewGuid().ToString(); + options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + } + } + + /// + /// Used to set up different configurations of metadata for different tests + /// + public class ConfigurationManager + { + /// + /// Default settings for + /// + static public IConfigurationManager DefaultStaticConfigurationManager() + { + return new StaticConfigurationManager(DefaultOpenIdConnectConfiguration()); + } + + /// + /// Default settings for + /// + /// + static public OpenIdConnectConfiguration DefaultOpenIdConnectConfiguration() + { + 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", + }; + } + } + + /// + /// 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 ?? ConfigurationManager.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/project.json b/test/Microsoft.AspNet.Authentication.Test/project.json index c7f594c4f..2b5adade7 100644 --- a/test/Microsoft.AspNet.Authentication.Test/project.json +++ b/test/Microsoft.AspNet.Authentication.Test/project.json @@ -12,6 +12,7 @@ "Microsoft.AspNet.Authentication.Twitter": "1.0.0-*", "Microsoft.AspNet.DataProtection": "1.0.0-*", "Microsoft.AspNet.TestHost": "1.0.0-*", + "Microsoft.IdentityModel.Protocol.Extensions": "2.0.0-beta4-*", "Moq": "4.2.1312.1622", "xunit.runner.aspnet": "2.0.0-aspnet-*" },