Check for suspicious email addresses when registering users

December 02, 2019
ASP.NET Core 3.0
ASP.NET Identity 3.0

If you are running a website that relies on user-generated content such as a forum, review website or some other kind of community website, you may have to deal with lots of spammy content. By spammy content, I mean the type of posts where someone will post a bunch of irrelevant links to their own website to try and boost their ranking, or someone posting a bunch of links to cheap viagra pills.

There are various ways to combat this and you will probably have to implement multiple measures to try and combat this properly. One of the measures you can take is to determine the reputation of an email address up front and preventing users using email addresses with a bad reputation from registering for your application in the first place.

I recently noticed an interesting blog post on the Auth0 blog that demonstrates how to deal with this exact problem by making use of a service called Simple Email Reputation. I was inspired by their blog post to create something similar for ASP.NET Core Identity. Credit goes to Mathias Conradt who wrote the original blog post for Auth0 for inspiring me to create this blog post.

Understanding the Simple Email Reputation service

The Simple Email Reputation application uses several criteria to determine a reputation for a particular email address. From their website:

EmailRep uses hundreds of factors like domain age, traffic rankings, presence on social media sites, professional networking sites, personal connections, public records, deliverability, data breaches, dark web credential leaks, phishing emails, threat actor emails, and more to answer these types of questions

  • Is this email risky?
  • Is this a throwaway account?
  • What kind of online presence does this email have?
  • Is this a trustworthy sender?

They have exposed this functionality behind a simple API endpoint that you can call, passing the email address you want to verify. In the screenshot below, you can see the result when I call this endpoint with my email address. It indicates my email address has a high reputation, and that it does not appear to be suspicious.

Testing the API endpoint with a valid email address

However, if I call the same endpoint with an email address that appears to be a bit dodgy, the response indicates it as suspicious.

Testing the API endpoint with a dodgy email address

Creating an API wrapper

Now that we have a basic understanding of how the API endpoint works let’s implement this in an application so we can prevent users with suspicious email addresses from signing up.

I created an EmailReputationService class which calls the API endpoint with a supplied email address and returns the response from the API endpoint.

public class EmailReputationService
{
    public HttpClient Client { get; }

    public EmailReputationService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://emailrep.io/");
        
        Client = client;
    }

    public async Task<ReputationResponse> GetEmailReputation(string emailAddress)
    {
        var response = await Client.GetAsync(emailAddress);
        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync<ReputationResponse>(responseStream);
    }
}

The ReputationResponse class is a C# class that I use to deserialize the JSON response from the API. I used the Quicktype application to generate this class which you can see in the accompanying source code

Remember to register a typed HTTP Client in the ConfigureServices method of your Startup class.

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddHttpClient<EmailReputationService>();
}

For more information on creating and consuming typed HTTP Clients, please refer to the ASP.NET Core Docs.

Creating a custom UserValidator

The next bit we need is to call the EmailReputationService at some point during the user registration to validate the email address. For this purpose, I created custom user validator by inheriting from the built-in UserValidator<> class.

public class SpammyAddressUserValidator<TUser> : UserValidator<TUser> where TUser : class
{
    private readonly EmailReputationService _emailReputationService;

    public SpammyAddressUserValidator(EmailReputationService emailReputationService)
    {
        _emailReputationService = emailReputationService;
    }

    public override async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user)
    {
        // Run the base validator and return the result if validation failed
        var result = await base.ValidateAsync(manager, user);
        if (!result.Succeeded)
            return result;

        // Check the email address reputation
        var emailAddress = await manager.GetEmailAsync(user);
        var reputation = await _emailReputationService.GetEmailReputation(emailAddress);

        // Fail validation if the email address is suspicious
        if (reputation.Suspicious)
            return IdentityResult.Failed(new IdentityError
            {
                Code = "SuspiciousEmail",
                Description = "The email address supplied appears to be one associated with spamming activity."
            });

        return IdentityResult.Success;
    }
}

In the ValidateAsync method, I call the implementation in the base class to run the standard validation. If any validation errors occur, for example, if it is a duplicate email address, I return those validation errors.

Alternatively, if the base class’ validation rules pass, I call the EmailReputationService to get the reputation of the supplied email address. If the response indicates that the email address is suspicious, I return a validation error. I only take the Suspicious property into account, but you can do something more elaborate by looking at the additional information returned from the API endpoint.

Finally, we need to register the SpammyAddressUserValidator with the dependency injection. Note that the order of registration is very important. You must register the SpammyAddressUserValidator before the call to AddDefaultIdentity. This prevents the default UserValidator class from being registered.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlite(
            Configuration.GetConnectionString("DefaultConnection")));
    
    // IMPORTANT: This line must be registered before the call to AddDefaultIdentity
    services.TryAddScoped<IUserValidator<IdentityUser>, SpammyAddressUserValidator<IdentityUser>>();
    
    services.AddDefaultIdentity<IdentityUser>(options =>
        {
            options.Password.RequireDigit = false;
            options.Password.RequireLowercase = false;
            options.Password.RequireUppercase = false;
            options.Password.RequireNonAlphanumeric = false;
        })
        .AddEntityFrameworkStores<ApplicationDbContext>();
    
    services.AddHttpClient<EmailReputationService>();
    services.AddRazorPages();
}

Putting it to the test

With all of this in place, we can now run the application. As you can see in the screenshot below, the application prevents users with suspicious email addresses from signing up.

The new spammy address validator in action

Conclusion

This blog post demonstrated how you could defend against spammy user-generated content by preventing the users who post this content from registering for your application in the first place. It is unlikely that this will prevent all spam content on your website, but it is a good first line of defence to keep these users out of your system.

Source code for this blog post it at https://github.com/jerriepelser-blog/CheckSpammyEmailAddresses

PS: If you need assistance on any of your ASP.NET Core projects, I am available for hire for freelance work.