diff --git a/samples/IdentitySample.Mvc/Controllers/AccountController.cs b/samples/IdentitySample.Mvc/Controllers/AccountController.cs index 1dcfa4b2e8..cdc6b95407 100644 --- a/samples/IdentitySample.Mvc/Controllers/AccountController.cs +++ b/samples/IdentitySample.Mvc/Controllers/AccountController.cs @@ -87,7 +87,13 @@ namespace IdentitySample.Models { var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Context.Request.Scheme); - await UserManager.SendEmailAsync(user, "Confirm your account", "Please confirm your account by clicking this link: link"); + var email = new IdentityMessage + { + Destination = model.Email, + Subject = "Confirm your account", + Body = "Please confirm your account by clicking this link: link" + }; + await UserManager.SendMessageAsync("Email", email); ViewBag.Link = callbackUrl; return View("DisplayEmail"); } @@ -240,7 +246,13 @@ namespace IdentitySample.Models var code = await UserManager.GeneratePasswordResetTokenAsync(user); var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Context.Request.Scheme); - await UserManager.SendEmailAsync(user, "Reset Password", "Please reset your password by clicking here: link"); + var email = new IdentityMessage + { + Destination = model.Email, + Subject = "Reset Password", + Body = "Please reset your password by clicking here: link" + }; + await UserManager.SendMessageAsync("Email", email); ViewBag.Link = callbackUrl; return View("ForgotPasswordConfirmation"); } diff --git a/samples/IdentitySample.Mvc/Controllers/ManageController.cs b/samples/IdentitySample.Mvc/Controllers/ManageController.cs index 04f75e19cb..e67342fb0a 100644 --- a/samples/IdentitySample.Mvc/Controllers/ManageController.cs +++ b/samples/IdentitySample.Mvc/Controllers/ManageController.cs @@ -95,16 +95,14 @@ namespace IdentitySample return View(model); } // Generate the token and send it - var code = await UserManager.GenerateChangePhoneNumberTokenAsync(await GetCurrentUserAsync(), model.Number); - if (UserManager.SmsService != null) + var user = await GetCurrentUserAsync(); + var code = await UserManager.GenerateChangePhoneNumberTokenAsync(user, model.Number); + var message = new IdentityMessage { - var message = new IdentityMessage - { - Destination = model.Number, - Body = "Your security code is: " + code - }; - await UserManager.SmsService.SendAsync(message); - } + Destination = model.Number, + Body = "Your security code is: " + code + }; + await UserManager.SendMessageAsync("SMS", message); return RedirectToAction("VerifyPhoneNumber", new { PhoneNumber = model.Number }); } diff --git a/samples/IdentitySample.Mvc/IdentitySample.Mvc.kproj b/samples/IdentitySample.Mvc/IdentitySample.Mvc.kproj index 24114c27a2..8b033a6cb1 100644 --- a/samples/IdentitySample.Mvc/IdentitySample.Mvc.kproj +++ b/samples/IdentitySample.Mvc/IdentitySample.Mvc.kproj @@ -1,8 +1,9 @@ - + 14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + 63045 @@ -11,4 +12,4 @@ ..\..\artifacts\bin\$(MSBuildProjectName)\ - + \ No newline at end of file diff --git a/samples/IdentitySample.Mvc/LocalConfig.json b/samples/IdentitySample.Mvc/LocalConfig.json index a57a3e9c9a..0e65b617f5 100644 --- a/samples/IdentitySample.Mvc/LocalConfig.json +++ b/samples/IdentitySample.Mvc/LocalConfig.json @@ -2,9 +2,6 @@ "DefaultAdminUsername": "Administrator", "DefaultAdminPassword": "YouShouldChangeThisPassword1!", "Data": { - "DefaultConnection": { - "Connectionstring": "Server=(localdb)\\mssqllocaldb;Database=IdentitySample-8-12-14;Trusted_Connection=True;MultipleActiveResultSets=true" - }, "IdentityConnection": { "Connectionstring": "Server=(localdb)\\mssqllocaldb;Database=IdentityMvc-8-12-14;Trusted_Connection=True;MultipleActiveResultSets=true" } diff --git a/samples/IdentitySample.Mvc/Startup.cs b/samples/IdentitySample.Mvc/Startup.cs index 80be907d58..39ebd7b7b0 100644 --- a/samples/IdentitySample.Mvc/Startup.cs +++ b/samples/IdentitySample.Mvc/Startup.cs @@ -7,6 +7,8 @@ using Microsoft.AspNet.Routing; using Microsoft.Data.Entity; using Microsoft.Framework.ConfigurationModel; using Microsoft.Framework.DependencyInjection; +using System.Threading.Tasks; +using System.Threading; namespace IdentitySamples { @@ -36,12 +38,11 @@ namespace IdentitySamples options.DefaultAdminPassword = Configuration.Get("DefaultAdminPassword"); }); - services.AddIdentity(Configuration, options => - { - options.SecurityStampValidationInterval = TimeSpan.FromMinutes(20); - }) + services.AddIdentity(Configuration) .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); + .AddDefaultTokenProviders() + .AddMessageProvider() + .AddMessageProvider(); services.ConfigureFacebookAuthentication(options => { @@ -80,5 +81,40 @@ namespace IdentitySamples //Populates the Admin user and role SampleData.InitializeIdentityDatabaseAsync(app.ApplicationServices).Wait(); } + + public class EmailMessageProvider : IIdentityMessageProvider + { + public string Name + { + get + { + return "Email"; + } + } + + public Task SendAsync(IdentityMessage message, CancellationToken cancellationToken = default(CancellationToken)) + { + // Plug in your service + return Task.FromResult(0); + } + } + + public class SmsMessageProvider : IIdentityMessageProvider + { + public string Name + { + get + { + return "SMS"; + } + } + + public Task SendAsync(IdentityMessage message, CancellationToken cancellationToken = default(CancellationToken)) + { + // Plug in your service + return Task.FromResult(0); + } + } + } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/EmailTokenProvider.cs b/src/Microsoft.AspNet.Identity/EmailTokenProvider.cs index 6220037c94..6f80f9fbc0 100644 --- a/src/Microsoft.AspNet.Identity/EmailTokenProvider.cs +++ b/src/Microsoft.AspNet.Identity/EmailTokenProvider.cs @@ -10,6 +10,8 @@ namespace Microsoft.AspNet.Identity { public string Name { get; set; } = Resources.DefaultEmailTokenProviderName; + public string MessageProvider { get; set; } = "Email"; + public string Subject { get; set; } = "Security Code"; /// @@ -48,7 +50,7 @@ namespace Microsoft.AspNet.Identity CancellationToken cancellationToken = default(CancellationToken)) { var email = await manager.GetEmailAsync(user, cancellationToken); - return !String.IsNullOrWhiteSpace(email) && await manager.IsEmailConfirmedAsync(user, cancellationToken); + return !string.IsNullOrWhiteSpace(email) && await manager.IsEmailConfirmedAsync(user, cancellationToken); } /// @@ -72,14 +74,20 @@ namespace Microsoft.AspNet.Identity /// /// /// - public override Task NotifyAsync(string token, UserManager manager, TUser user, + public override async Task NotifyAsync(string token, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { if (manager == null) { - throw new ArgumentNullException("manager"); + throw new ArgumentNullException(nameof(manager)); } - return manager.SendEmailAsync(user, Options.Subject, String.Format(CultureInfo.CurrentCulture, Options.BodyFormat, token), cancellationToken); + var msg = new IdentityMessage + { + Destination = await manager.GetEmailAsync(user, cancellationToken), + Subject = Options.Subject, + Body = string.Format(CultureInfo.CurrentCulture, Options.BodyFormat, token) + }; + await manager.SendMessageAsync(Options.MessageProvider, msg, cancellationToken); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IIdentityMessageService.cs b/src/Microsoft.AspNet.Identity/IIdentityMessageProvider.cs similarity index 52% rename from src/Microsoft.AspNet.Identity/IIdentityMessageService.cs rename to src/Microsoft.AspNet.Identity/IIdentityMessageProvider.cs index 9ae9e30463..0c0ea30bbd 100644 --- a/src/Microsoft.AspNet.Identity/IIdentityMessageService.cs +++ b/src/Microsoft.AspNet.Identity/IIdentityMessageProvider.cs @@ -6,17 +6,10 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Identity { - /// - /// Expose a way to send messages (email/txt) - /// - public interface IIdentityMessageService + public interface IIdentityMessageProvider { - /// - /// This method should send the message - /// - /// - /// - /// + string Name { get; } + Task SendAsync(IdentityMessage message, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityBuilder.cs b/src/Microsoft.AspNet.Identity/IdentityBuilder.cs index 750550ada0..8c9d51332a 100644 --- a/src/Microsoft.AspNet.Identity/IdentityBuilder.cs +++ b/src/Microsoft.AspNet.Identity/IdentityBuilder.cs @@ -19,10 +19,50 @@ namespace Microsoft.AspNet.Identity public Type RoleType { get; private set; } public IServiceCollection Services { get; private set; } + private IdentityBuilder AddScoped(Type serviceType, Type concreteType) + { + Services.AddScoped(serviceType, concreteType); + return this; + } + + public IdentityBuilder AddUserValidator() where T : class + { + return AddScoped(typeof(IUserValidator<>).MakeGenericType(UserType), typeof(T)); + } + + public IdentityBuilder AddRoleValidator() where T : class + { + return AddScoped(typeof(IRoleValidator<>).MakeGenericType(RoleType), typeof(T)); + } + + public IdentityBuilder AddPasswordValidator() where T : class + { + return AddScoped(typeof(IPasswordValidator<>).MakeGenericType(UserType), typeof(T)); + } + + public IdentityBuilder AddUserStore() where T : class + { + return AddScoped(typeof(IUserStore<>).MakeGenericType(UserType), typeof(T)); + } + + public IdentityBuilder AddRoleStore() where T : class + { + return AddScoped(typeof(IRoleStore<>).MakeGenericType(RoleType), typeof(T)); + } + + public IdentityBuilder AddTokenProvider() where TProvider : class + { + return AddTokenProvider(typeof(TProvider)); + } + public IdentityBuilder AddTokenProvider(Type provider) { - Services.AddScoped(typeof(IUserTokenProvider<>).MakeGenericType(UserType), provider); - return this; + return AddScoped(typeof(IUserTokenProvider<>).MakeGenericType(UserType), provider); + } + + public IdentityBuilder AddMessageProvider() where TProvider : IIdentityMessageProvider + { + return AddScoped(typeof(IIdentityMessageProvider), typeof(TProvider)); } public IdentityBuilder AddDefaultTokenProviders() @@ -36,62 +76,5 @@ namespace Microsoft.AspNet.Identity .AddTokenProvider(typeof(PhoneNumberTokenProvider<>).MakeGenericType(UserType)) .AddTokenProvider(typeof(EmailTokenProvider<>).MakeGenericType(UserType)); } - - } - - public class IdentityBuilder : IdentityBuilder where TUser : class where TRole : class - { - public IdentityBuilder(IServiceCollection services) : base(typeof(TUser), typeof(TRole), services) { } - - public IdentityBuilder AddInstance(TService instance) - where TService : class - { - Services.AddInstance(instance); - return this; - } - - public IdentityBuilder AddUserStore(IUserStore store) - { - return AddInstance(store); - } - - public IdentityBuilder AddRoleStore(IRoleStore store) - { - return AddInstance(store); - } - - public IdentityBuilder AddPasswordValidator(IPasswordValidator validator) - { - return AddInstance(validator); - } - - public IdentityBuilder AddUserValidator(IUserValidator validator) - { - return AddInstance(validator); - } - - public IdentityBuilder AddTokenProvider() where TTokenProvider : IUserTokenProvider - { - Services.AddScoped, TTokenProvider>(); - return this; - } - - public IdentityBuilder ConfigureIdentity(Action action, int order = 0) - { - Services.Configure(action, order); - return this; - } - - public IdentityBuilder AddUserManager() where TManager : UserManager - { - Services.AddScoped(); - return this; - } - - public IdentityBuilder AddRoleManager() where TManager : RoleManager - { - Services.AddScoped(); - return this; - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityMessage.cs b/src/Microsoft.AspNet.Identity/IdentityMessage.cs index 655bd63d8b..44c9b3e16f 100644 --- a/src/Microsoft.AspNet.Identity/IdentityMessage.cs +++ b/src/Microsoft.AspNet.Identity/IdentityMessage.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Identity public class IdentityMessage { /// - /// Destination, i.e. To email, or SMS phone number + /// Target for the message, i.e. email or phone number /// public virtual string Destination { get; set; } diff --git a/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs index 3be053cec8..86fd009b9e 100644 --- a/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs @@ -17,12 +17,12 @@ namespace Microsoft.Framework.DependencyInjection return services.Configure(configure); } - public static IdentityBuilder AddIdentity(this IServiceCollection services) + public static IdentityBuilder AddIdentity(this IServiceCollection services) { return services.AddIdentity(); } - public static IdentityBuilder AddIdentity( + public static IdentityBuilder AddIdentity( this IServiceCollection services, IConfiguration identityConfig = null, Action configureOptions = null, @@ -31,7 +31,7 @@ namespace Microsoft.Framework.DependencyInjection return services.AddIdentity(identityConfig, configureOptions, useDefaultSubKey); } - public static IdentityBuilder AddIdentity( + public static IdentityBuilder AddIdentity( this IServiceCollection services, IConfiguration identityConfig = null, Action configureOptions = null, @@ -39,8 +39,6 @@ namespace Microsoft.Framework.DependencyInjection where TUser : class where TRole : class { - services.Add(IdentityServices.GetDefaultServices()); - if (identityConfig != null) { if (useDefaultSubKey) @@ -49,16 +47,34 @@ namespace Microsoft.Framework.DependencyInjection } services.Configure(identityConfig); } + var describe = new ServiceDescriber(identityConfig); + + // Services used by identity + services.AddOptions(identityConfig); + services.AddDataProtection(identityConfig); + + // Identity services + services.TryAdd(describe.Transient, UserValidator>()); + services.TryAdd(describe.Transient, PasswordValidator>()); + services.TryAdd(describe.Transient, PasswordHasher>()); + services.TryAdd(describe.Transient()); + services.TryAdd(describe.Transient, RoleValidator>()); + services.TryAdd(describe.Scoped>()); + services.TryAdd(describe.Scoped, ClaimsIdentityFactory>()); + services.TryAdd(describe.Scoped, UserManager>()); + services.TryAdd(describe.Scoped, SignInManager>()); + services.TryAdd(describe.Scoped, RoleManager>()); + if (configureOptions != null) { services.ConfigureIdentity(configureOptions); } - services.Configure(options => { options.SignInAsAuthenticationType = IdentityOptions.ExternalCookieAuthenticationType; }); + // Configure all of the cookie middlewares services.Configure(options => { options.AuthenticationType = IdentityOptions.ApplicationCookieAuthenticationType; @@ -68,7 +84,6 @@ namespace Microsoft.Framework.DependencyInjection OnValidateIdentity = SecurityStampValidator.ValidateIdentityAsync }; }, IdentityOptions.ApplicationCookieAuthenticationType); - services.Configure(options => { options.AuthenticationType = IdentityOptions.ExternalCookieAuthenticationType; @@ -76,14 +91,12 @@ namespace Microsoft.Framework.DependencyInjection options.CookieName = IdentityOptions.ExternalCookieAuthenticationType; options.ExpireTimeSpan = TimeSpan.FromMinutes(5); }, IdentityOptions.ExternalCookieAuthenticationType); - services.Configure(options => { options.AuthenticationType = IdentityOptions.TwoFactorRememberMeCookieAuthenticationType; options.AuthenticationMode = AuthenticationMode.Passive; options.CookieName = IdentityOptions.TwoFactorRememberMeCookieAuthenticationType; }, IdentityOptions.TwoFactorRememberMeCookieAuthenticationType); - services.Configure(options => { options.AuthenticationType = IdentityOptions.TwoFactorUserIdCookieAuthenticationType; @@ -92,7 +105,7 @@ namespace Microsoft.Framework.DependencyInjection options.ExpireTimeSpan = TimeSpan.FromMinutes(5); }, IdentityOptions.TwoFactorUserIdCookieAuthenticationType); - return new IdentityBuilder(services); + return new IdentityBuilder(typeof(TUser), typeof(TRole), services); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityServices.cs b/src/Microsoft.AspNet.Identity/IdentityServices.cs deleted file mode 100644 index 850c14693d..0000000000 --- a/src/Microsoft.AspNet.Identity/IdentityServices.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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.Collections.Generic; -using Microsoft.Framework.ConfigurationModel; -using Microsoft.Framework.DependencyInjection; - -namespace Microsoft.AspNet.Identity -{ - /// - /// Default services used by UserManager and RoleManager - /// - public class IdentityServices - { - public static IEnumerable GetDefaultServices(IConfiguration config = null) - where TUser : class where TRole : class - { - ServiceDescriber describe; - if (config == null) - { - describe = new ServiceDescriber(); - } - else - { - describe = new ServiceDescriber(config); - } - yield return describe.Transient, UserValidator>(); - yield return describe.Transient, PasswordValidator>(); - yield return describe.Transient, PasswordHasher>(); - yield return describe.Transient(); - yield return describe.Transient, RoleValidator>(); - yield return describe.Scoped>(); - yield return describe.Scoped, ClaimsIdentityFactory>(); - yield return describe.Scoped, UserManager>(); - yield return describe.Scoped, SignInManager>(); - yield return describe.Scoped, RoleManager>(); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/PhoneNumberTokenProvider.cs b/src/Microsoft.AspNet.Identity/PhoneNumberTokenProvider.cs index 2f0ded3145..3116bc5dd3 100644 --- a/src/Microsoft.AspNet.Identity/PhoneNumberTokenProvider.cs +++ b/src/Microsoft.AspNet.Identity/PhoneNumberTokenProvider.cs @@ -10,6 +10,8 @@ namespace Microsoft.AspNet.Identity { public string Name { get; set; } = Resources.DefaultPhoneNumberTokenProviderName; + public string MessageProvider { get; set; } = "SMS"; + /// /// Message contents which should contain a format string which the token will be the only argument /// @@ -50,7 +52,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("manager"); } var phoneNumber = await manager.GetPhoneNumberAsync(user, cancellationToken); - return !String.IsNullOrWhiteSpace(phoneNumber) && await manager.IsPhoneNumberConfirmedAsync(user, cancellationToken); + return !string.IsNullOrWhiteSpace(phoneNumber) && await manager.IsPhoneNumberConfirmedAsync(user, cancellationToken); } /// @@ -78,14 +80,19 @@ namespace Microsoft.AspNet.Identity /// /// /// - public override Task NotifyAsync(string token, UserManager manager, TUser user, + public override async Task NotifyAsync(string token, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { if (manager == null) { throw new ArgumentNullException("manager"); } - return manager.SendSmsAsync(user, String.Format(CultureInfo.CurrentCulture, Options.MessageFormat, token), cancellationToken); + var msg = new IdentityMessage + { + Destination = await manager.GetPhoneNumberAsync(user, cancellationToken), + Body = string.Format(CultureInfo.CurrentCulture, Options.MessageFormat, token) + }; + await manager.SendMessageAsync(Options.MessageProvider, msg, cancellationToken); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs index 240e80680c..9c91fd4bd5 100644 --- a/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs @@ -266,6 +266,22 @@ namespace Microsoft.AspNet.Identity return GetString("LockoutNotEnabled"); } + /// + /// No IUserMessageProvider named '{0}' is registered. + /// + internal static string NoMessageProvider + { + get { return GetString("NoMessageProvider"); } + } + + /// + /// No IUserMessageProvider named '{0}' is registered. + /// + internal static string FormatNoMessageProvider(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("NoMessageProvider"), p0); + } + /// /// No IUserTokenProvider named '{0}' is registered. /// diff --git a/src/Microsoft.AspNet.Identity/Resources.resx b/src/Microsoft.AspNet.Identity/Resources.resx index b51f0157d8..674ae53538 100644 --- a/src/Microsoft.AspNet.Identity/Resources.resx +++ b/src/Microsoft.AspNet.Identity/Resources.resx @@ -181,6 +181,10 @@ Lockout is not enabled for this user. error when lockout is not enabled + + No IUserMessageProvider named '{0}' is registered. + Error when there is no IUserMessageProvider + No IUserTokenProvider named '{0}' is registered. Error when there is no IUserTokenProvider diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs index 2b82d41333..bd52e159c0 100644 --- a/src/Microsoft.AspNet.Identity/UserManager.cs +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -21,6 +21,8 @@ namespace Microsoft.AspNet.Identity { private readonly Dictionary> _tokenProviders = new Dictionary>(); + private readonly Dictionary _msgProviders = + new Dictionary(); private TimeSpan _defaultLockout = TimeSpan.Zero; private bool _disposed; @@ -36,10 +38,14 @@ namespace Microsoft.AspNet.Identity /// /// /// - public UserManager(IUserStore store, IOptions optionsAccessor, - IPasswordHasher passwordHasher, IUserValidator userValidator, - IPasswordValidator passwordValidator, IUserNameNormalizer userNameNormalizer, - IEnumerable> tokenProviders) + public UserManager(IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IUserValidator userValidator, + IPasswordValidator passwordValidator, + IUserNameNormalizer userNameNormalizer, + IEnumerable> tokenProviders, + IEnumerable msgProviders) { if (store == null) { @@ -68,6 +74,15 @@ namespace Microsoft.AspNet.Identity RegisterTokenProvider(tokenProvider); } } + + if (msgProviders != null) + { + foreach (var msgProvider in msgProviders) + { + RegisterMessageProvider(msgProvider); + } + } + } /// @@ -111,16 +126,6 @@ namespace Microsoft.AspNet.Identity /// public IUserNameNormalizer UserNameNormalizer { get; set; } - /// - /// Used to send email - /// - public IIdentityMessageService EmailService { get; set; } - - /// - /// Used to send a sms message - /// - public IIdentityMessageService SmsService { get; set; } - public IdentityOptions Options { get @@ -1500,7 +1505,6 @@ namespace Microsoft.AspNet.Identity /// /// Register a user token provider /// - /// /// public virtual void RegisterTokenProvider(IUserTokenProvider provider) { @@ -1512,6 +1516,20 @@ namespace Microsoft.AspNet.Identity _tokenProviders[provider.Name] = provider; } + /// + /// Register a user message provider + /// + /// + public virtual void RegisterMessageProvider(IIdentityMessageProvider provider) + { + ThrowIfDisposed(); + if (provider == null) + { + throw new ArgumentNullException("provider"); + } + _msgProviders[provider.Name] = provider; + } + /// /// Returns a list of valid two factor providers for a user /// @@ -1664,60 +1682,30 @@ namespace Microsoft.AspNet.Identity return await UpdateAsync(user, cancellationToken); } - // SMS/Email methods + // Messaging methods /// - /// Send an email to the user + /// Send a message to the user using the specified provider /// - /// - /// - /// - /// - /// - public virtual async Task SendEmailAsync(TUser user, string subject, string body, - CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - if (EmailService != null) - { - var msg = new IdentityMessage - { - Destination = await GetEmailAsync(user, cancellationToken), - Subject = subject, - Body = body, - }; - await EmailService.SendAsync(msg, cancellationToken); - } - } - - /// - /// Send a user a sms message - /// - /// + /// /// /// /// - public virtual async Task SendSmsAsync(TUser user, string message, + public virtual async Task SendMessageAsync(string messageProvider, IdentityMessage message, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); - if (user == null) + if (message == null) { - throw new ArgumentNullException("user"); + throw new ArgumentNullException(nameof(message)); } - if (SmsService != null) + if (!_msgProviders.ContainsKey(messageProvider)) { - var msg = new IdentityMessage - { - Destination = await GetPhoneNumberAsync(user, cancellationToken), - Body = message - }; - await SmsService.SendAsync(msg, cancellationToken); + throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, + Resources.NoMessageProvider, messageProvider)); } + await _msgProviders[messageProvider].SendAsync(message, cancellationToken); + return IdentityResult.Success; } // IUserLockoutStore methods diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/EntityInMemoryTestServiceCollectionExtensions.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/EntityInMemoryTestServiceCollectionExtensions.cs index 6d21889873..98c6e6506c 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/EntityInMemoryTestServiceCollectionExtensions.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/EntityInMemoryTestServiceCollectionExtensions.cs @@ -10,12 +10,12 @@ namespace Microsoft.AspNet.Identity { public static class EntityInMemoryTestServiceCollectionExtensions { - public static IdentityBuilder AddIdentityInMemory(this ServiceCollection services, InMemoryContext context) + public static IdentityBuilder AddIdentityInMemory(this ServiceCollection services, InMemoryContext context) { return services.AddIdentityInMemory(context); } - public static IdentityBuilder AddIdentityInMemory(this ServiceCollection services, TDbContext context) + public static IdentityBuilder AddIdentityInMemory(this ServiceCollection services, TDbContext context) where TUser : IdentityUser where TRole : IdentityRole where TDbContext : DbContext diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs index 4c9175b14e..2efc785d71 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs @@ -19,7 +19,8 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test var services = new ServiceCollection(); services.AddEntityFramework().AddInMemoryStore(); var store = new RoleStore(new InMemoryContext()); - services.AddIdentity().AddRoleStore(store); + services.AddIdentity(); + services.AddInstance>(store); var provider = services.BuildServiceProvider(); var manager = provider.GetRequiredService>(); Assert.NotNull(manager); diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/TestIdentityFactory.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/TestIdentityFactory.cs index 50e7d085b4..3112c72c58 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/TestIdentityFactory.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/TestIdentityFactory.cs @@ -1,10 +1,8 @@ // 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.Identity.Test; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.Fallback; -using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test { @@ -25,7 +23,8 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test public static RoleManager CreateRoleManager(InMemoryContext context) { var services = new ServiceCollection(); - services.AddIdentity().AddRoleStore(new RoleStore(context)); + services.AddIdentity(); + services.AddInstance>(new RoleStore(context)); return services.BuildServiceProvider().GetRequiredService>(); } diff --git a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs index 1d29a5cc14..c56a000d74 100644 --- a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs @@ -4,34 +4,62 @@ using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.Fallback; using Microsoft.Framework.OptionsModel; +using System.Collections.Generic; using Xunit; +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using Microsoft.AspNet.Security.DataProtection; namespace Microsoft.AspNet.Identity.Test { public class IdentityBuilderTest { + [Fact] - public void CanSpecifyUserValidatorInstance() + public void CanOverrideUserStore() { var services = new ServiceCollection(); - var validator = new UserValidator(); - services.AddIdentity().AddUserValidator(validator); - Assert.Equal(validator, services.BuildServiceProvider().GetRequiredService>()); + services.AddIdentity().AddUserStore(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); } [Fact] - public void CanSpecifyPasswordValidatorInstance() + public void CanOverrideRoleStore() { var services = new ServiceCollection(); - var validator = new PasswordValidator(); - services.AddIdentity().AddPasswordValidator(validator); - Assert.Equal(validator, services.BuildServiceProvider().GetRequiredService>()); + services.AddIdentity().AddRoleStore(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); } [Fact] - public void CanSpecifyPasswordHasherInstance() + public void CanOverrideRoleValidator() { - CanOverride>(new PasswordHasher(new PasswordHasherOptionsAccessor())); + var services = new ServiceCollection(); + services.AddIdentity().AddRoleValidator(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); + } + + [Fact] + public void CanOverrideUserValidator() + { + var services = new ServiceCollection(); + services.AddIdentity().AddUserValidator(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); + } + + [Fact] + public void CanOverridePasswordValidator() + { + var services = new ServiceCollection(); + services.AddIdentity().AddPasswordValidator(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); } [Fact] @@ -52,12 +80,130 @@ namespace Microsoft.AspNet.Identity.Test Assert.NotNull(hasher); } - private static void CanOverride(TService instance) - where TService : class + [Fact] + public void EnsureDefaultTokenProviders() { var services = new ServiceCollection(); - services.AddIdentity().AddInstance(instance); - Assert.Equal(instance, services.BuildServiceProvider().GetRequiredService()); + services.AddIdentity().AddDefaultTokenProviders(); + services.Add(DataProtectionServices.GetDefaultServices()); + services.Add(OptionsServices.GetDefaultServices()); + + var provider = services.BuildServiceProvider(); + var tokenProviders = provider.GetRequiredService>>(); + Assert.Equal(3, tokenProviders.Count()); + } + + private class MyUberThingy : IUserValidator, IPasswordValidator, IRoleValidator, IUserStore, IRoleStore + { + public Task CreateAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task CreateAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + + public Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetRoleIdAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetRoleNameAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetRoleNameAsync(IdentityRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetUserNameAsync(IdentityUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task ValidateAsync(RoleManager manager, IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task ValidateAsync(UserManager manager, IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task ValidateAsync(IdentityUser user, string password, UserManager manager, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + Task IRoleStore.FindByIdAsync(string roleId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + Task IRoleStore.FindByNameAsync(string roleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } } diff --git a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs index 386520db42..daafaf8b19 100644 --- a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Identity.Test public TestManager(IUserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IUserValidator userValidator, IPasswordValidator passwordValidator) - : base(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null, null) { } + : base(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null, null, null) { } } [Fact] @@ -538,13 +538,13 @@ namespace Microsoft.AspNet.Identity.Test var passwordValidator = new PasswordValidator(); Assert.Throws("store", - () => new UserManager(null, null, null, null, null, null, null)); + () => new UserManager(null, null, null, null, null, null, null, null)); Assert.Throws("optionsAccessor", - () => new UserManager(store, null, null, null, null, null, null)); + () => new UserManager(store, null, null, null, null, null, null, null)); Assert.Throws("passwordHasher", - () => new UserManager(store, optionsAccessor, null, null, null, null, null)); + () => new UserManager(store, optionsAccessor, null, null, null, null, null, null)); - var manager = new UserManager(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null, null); + var manager = new UserManager(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null, null, null); Assert.Throws("value", () => manager.PasswordHasher = null); Assert.Throws("value", () => manager.Options = null); @@ -671,10 +671,6 @@ namespace Microsoft.AspNet.Identity.Test async () => await manager.GetLockoutEndDateAsync(null)); await Assert.ThrowsAsync("user", async () => await manager.IsLockedOutAsync(null)); - await Assert.ThrowsAsync("user", - async () => await manager.SendEmailAsync(null, null, null)); - await Assert.ThrowsAsync("user", - async () => await manager.SendSmsAsync(null, null)); } [Fact] @@ -714,6 +710,7 @@ namespace Microsoft.AspNet.Identity.Test await Assert.ThrowsAsync(() => manager.GenerateEmailConfirmationTokenAsync(null)); await Assert.ThrowsAsync(() => manager.IsEmailConfirmedAsync(null)); await Assert.ThrowsAsync(() => manager.ConfirmEmailAsync(null, null)); + await Assert.ThrowsAsync(() => manager.SendMessageAsync(null, null)); } private class BadPasswordValidator : IPasswordValidator where TUser : class diff --git a/test/Shared/MockHelpers.cs b/test/Shared/MockHelpers.cs index 0eb2418d2c..72790950fb 100644 --- a/test/Shared/MockHelpers.cs +++ b/test/Shared/MockHelpers.cs @@ -42,7 +42,8 @@ namespace Microsoft.AspNet.Identity.Test new UserValidator(), new PasswordValidator(), new UpperInvariantUserNameNormalizer(), - new List>()); + new List>(), + new List()); } public static Mock> MockRoleManager() where TRole : class @@ -61,7 +62,7 @@ namespace Microsoft.AspNet.Identity.Test var options = new OptionsManager(null); var validator = new Mock>(); var userManager = new UserManager(store, options, new PasswordHasher(new PasswordHasherOptionsAccessor()), - validator.Object, new PasswordValidator(), new UpperInvariantUserNameNormalizer(), null); + validator.Object, new PasswordValidator(), new UpperInvariantUserNameNormalizer(), null, null); validator.Setup(v => v.ValidateAsync(userManager, It.IsAny(), CancellationToken.None)) .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); return userManager; diff --git a/test/Shared/TestMessageService.cs b/test/Shared/TestMessageService.cs index b57e606e46..52040cf594 100644 --- a/test/Shared/TestMessageService.cs +++ b/test/Shared/TestMessageService.cs @@ -1,15 +1,18 @@ // 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; using System.Threading.Tasks; namespace Microsoft.AspNet.Identity.Test { - public class TestMessageService : IIdentityMessageService + public class TestMessageService : IIdentityMessageProvider { public IdentityMessage Message { get; set; } + public string Name { get; set; } = "Test"; + public Task SendAsync(IdentityMessage message, CancellationToken cancellationToken = default(CancellationToken)) { Message = message; diff --git a/test/Shared/UserManagerTestBase.cs b/test/Shared/UserManagerTestBase.cs index 6d646ec0c8..a0918c92d7 100644 --- a/test/Shared/UserManagerTestBase.cs +++ b/test/Shared/UserManagerTestBase.cs @@ -11,8 +11,6 @@ using Microsoft.AspNet.Testing; using Xunit; using Microsoft.AspNet.Security.DataProtection; using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.OptionsModel; -using Microsoft.AspNet.Hosting; using Microsoft.Framework.DependencyInjection.Fallback; namespace Microsoft.AspNet.Identity.Test @@ -30,8 +28,8 @@ namespace Microsoft.AspNet.Identity.Test { protected virtual void SetupIdentityServices(IServiceCollection services, object context = null) { - services.Add(OptionsServices.GetDefaultServices()); - services.Add(HostingServices.GetDefaultServices()); + services.AddOptions(); + services.AddHosting(); services.Add(DataProtectionServices.GetDefaultServices()); services.AddIdentity().AddDefaultTokenProviders(); AddUserStore(services, context); @@ -1206,8 +1204,8 @@ namespace Microsoft.AspNet.Identity.Test public async Task CanEmailTwoFactorToken() { var manager = CreateManager(); - var messageService = new TestMessageService(); - manager.EmailService = messageService; + var messageService = new TestMessageService { Name = "Email" }; + manager.RegisterMessageProvider(messageService); const string factorId = "Email"; // default var user = new TUser() { UserName = "EmailCodeTest", Email = "foo@foo.com" }; user.EmailConfirmed = true; @@ -1251,8 +1249,8 @@ namespace Microsoft.AspNet.Identity.Test o.BodyFormat = body; }); var manager = CreateManager(null, services); - var messageService = new TestMessageService(); - manager.EmailService = messageService; + var messageService = new TestMessageService { Name = "Email" }; + manager.RegisterMessageProvider(messageService); var user = CreateTestUser(); user.Email = user.UserName + "@foo.com"; const string password = "password"; @@ -1300,30 +1298,16 @@ namespace Microsoft.AspNet.Identity.Test } [Fact] - public async Task CanSendSms() + public async Task CanSendMessage() { var manager = CreateManager(); - var messageService = new TestMessageService(); - manager.SmsService = messageService; + var messageService = new TestMessageService(); + manager.RegisterMessageProvider(messageService); var user = CreateTestUser(); - user.PhoneNumber = "4251234567"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); - await manager.SendSmsAsync(user, "Hi"); - Assert.NotNull(messageService.Message); - Assert.Equal("Hi", messageService.Message.Body); - } - - [Fact] - public async Task CanSendEmail() - { - var manager = CreateManager(); - var messageService = new TestMessageService(); - manager.EmailService = messageService; - var user = CreateTestUser(); - user.Email = user.UserName + "@foo.com"; - IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); - await manager.SendEmailAsync(user, "Hi", "Body"); + await manager.SendMessageAsync(messageService.Name, new IdentityMessage { Destination = "foo", Subject = "Hi", Body = "Body" }); Assert.NotNull(messageService.Message); + Assert.Equal("foo", messageService.Message.Destination); Assert.Equal("Hi", messageService.Message.Subject); Assert.Equal("Body", messageService.Message.Body); } @@ -1332,8 +1316,8 @@ namespace Microsoft.AspNet.Identity.Test public async Task CanSmsTwoFactorToken() { var manager = CreateManager(); - var messageService = new TestMessageService(); - manager.SmsService = messageService; + var messageService = new TestMessageService { Name = "SMS" }; + manager.RegisterMessageProvider(messageService); const string factorId = "Phone"; // default var user = CreateTestUser(); user.PhoneNumber = "4251234567"; @@ -1360,8 +1344,8 @@ namespace Microsoft.AspNet.Identity.Test o.MessageFormat = "Your code is: {0}"; }); var manager = CreateManager(null, services); - var messageService = new TestMessageService(); - manager.SmsService = messageService; + var messageService = new TestMessageService { Name = "SMS" }; + manager.RegisterMessageProvider(messageService); var user = CreateTestUser(); user.PhoneNumber = "4251234567"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));