using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Polly; namespace gehGassi.Pwned { /// /// An extension class for Pwned.AspNetCore /// [ExcludeFromCodeCoverage] public static class PwnedExtensions { /// /// Default Breach Name for /// public const string DefaultBreachName = nameof(PwnedBreachService); /// /// Default configuration name. /// public const string Pwned = nameof(Pwned); /// /// Default Password Name for /// public const string DefaultPasswordName = nameof(PwnedPasswordService); /// /// Adds and and related services. /// /// /// /// /// public static IServiceCollection AddPwned( this IServiceCollection services, IConfiguration configuration, string sectionName = Pwned) { services.AddPwned(_ => configuration.GetSection(sectionName)); return services; } public static IServiceCollection AddPwned(this IServiceCollection services) { services.AddPwned(_ => new PwnedOptions()); return services; } /// /// Adds and and related services. /// /// /// /// public static IServiceCollection AddPwned( this IServiceCollection services, Action options) { AddPwnedBreach(services, options); AddPwnedPassword(services, options); return services; } /// /// Adds and related services. /// /// /// /// public static IServiceCollection AddPwnedBreach( this IServiceCollection services, Action options) { services.Configure(options); services.AddHttpClient(DefaultBreachName) .AddPolicyHandler(Policy.TimeoutAsync(TimeSpan.FromSeconds(30))) .AddPolicyHandler(ExponentialWaitAndRetry(2)) .AddTypedClient(); return services; } /// /// Adds and related services. /// /// /// public static IServiceCollection AddPwnedPassword(this IServiceCollection services) { return services.AddPwnedPassword(_ => new PwnedOptions()); } /// /// Adds and related services. /// /// /// /// public static IServiceCollection AddPwnedPassword( this IServiceCollection services, Action options) { services.Configure(options); // The pwnedpassword API achieves 99% percentile of <1s, so this should be sufficient services.AddHttpClient(DefaultPasswordName) .AddPolicyHandler(Policy.TimeoutAsync(TimeSpan.FromSeconds(2))) .AddTransientHttpErrorPolicy(p => p.RetryAsync(3)) .AddTypedClient(); return services; } /// /// Adds the and related services to the /// and configures a binding between the and a named /// /// /// /// /// /// /// public static IHttpClientBuilder AddPwnedPasswordHttpClient( this IServiceCollection services, IConfiguration configuration, string name, Action configureClient, string sectionName = Pwned) { services.Configure(name, configuration.GetSection(sectionName)); return services.AddHttpClient(name, configureClient); } /// /// Adds the and related services to the /// and configures a binding between the and an /// named to use the public HaveIBeenPwned API /// at "https://api.pwnedpasswords.com" /// /// /// /// /// public static IHttpClientBuilder AddPwnedPasswordHttpClient( this IServiceCollection services, IConfiguration configuration, string sectionName = Pwned) { return services.AddPwnedPasswordHttpClient(configuration, DefaultPasswordName, _ => { }, sectionName); } /// /// Adds the and related services to the /// and configures a binding between the and an /// named to use the public HaveIBeenPwned API /// at "https://pwnedpasswords.com" /// /// /// /// public static IHttpClientBuilder AddPwnedBreachHttpClient( this IServiceCollection services, IConfiguration configuration) { return services.AddPwnedBreachHttpClient(configuration, DefaultBreachName, _ => { }); } /// /// Adds the and related services to the /// and configures a binding between the and an /// named to use the public HaveIBeenPwned API /// at "https://pwnedpasswords.com" /// /// /// /// /// /// /// public static IHttpClientBuilder AddPwnedBreachHttpClient( this IServiceCollection services, IConfiguration configuration, string name, Action configureClient, string sectionName = Pwned) { services.Configure(name, configuration.GetSection(sectionName)); return services.AddHttpClient(name, configureClient); } /// /// Adds a password validator that checks the password is not a pwned password using the Have I been pwned API /// See https://haveibeenpwned.com/API/v2#PwnedPasswords for details. /// /// /// /// /// public static IdentityBuilder AddPwnedPasswordValidator( this IdentityBuilder builder, IConfiguration configuration) where TUser : class { return builder.AddPwnedPasswordValidator(configure: _ => configuration.GetSection("Pwned")); } /// /// Adds a password validator that checks the password is not a pwned password using the Have I been pwned API /// See https://haveibeenpwned.com/API/v2#PwnedPasswords for details. /// /// /// /// /// public static IdentityBuilder AddPwnedPasswordValidator( this IdentityBuilder builder, Action configure) where TUser : class { if (!builder.Services.Any(x => x.ServiceType == typeof(IPwnedPasswordService))) { builder.Services.AddPwnedPassword(configure); } return builder.AddPasswordValidator>(); } //https://github.com/App-vNext/Polly/issues/414#issuecomment-371932576 public static IAsyncPolicy ExponentialWaitAndRetry(int retry) { return Policy.Handle().OrResult (r => r.StatusCode == (HttpStatusCode)429) // RetryAfter .WaitAndRetryAsync(retry, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); } } }