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)));
}
}
}