Domain name ownership verification in .NET
Laurent Egbakou
Laurent Egbakou
January 31, 2024
4 min
Our work may have helped you ?
Support Us

Table Of Contents

What is ownership verification?
How does it work?
Implementation in using .NET
Final Words

In this article, you will learn how to create a .NET API that allows users to add domain names to their profiles and prove ownership.

What is ownership verification?

If you're looking to:

  • Register your website on Google or Bing for search indexing,
  • Add Google Analytics support for your web app,
  • Get a verified badge on GitHub for your organization,
  • Or use a custom domain in a SaaS app,

You'll need to demonstrate that you own the domain name. This process is called domain name ownership verification.

This is typically required by various services to ensure that you are the legitimate owner and not someone else trying to impersonate or misuse.

How does it work?

The process is simple: you enter your domain name; the app will provide you with instructions. These instructions can be grouped into three categories of actions:

  • Add a meta tag to your website's default/home page
  • Upload a specific file to your web server
  • Add a record (CNAME or TXT) to your Domain Name Server

Once you have the instructions, you need to correctly perform one of the actions. The app will verify in the background whether it's done correctly or not.

Implementation in using .NET

We will use the new Identity API of .NET 8 for registration and authentication, SQLite to store users' data, and the NuGet package DomainVerifier.Extensions. This is a .NET library for domain name ownership verification with Dependency Injection support for .NET 6.0, .NET 7.0, and .NET 8.0.

Project setup

Firstly, you have to create a .NET 8 Minimal Api project named DomainOwnershipSample. Once created, add the following NuGet packages:

Background job processing and ownership verification NuGet packages

dotnet add Quartz.Extensions.Hosting dotnet add DomainVerifier.Extensions

Entity Framework NuGet packages

dotnet tool install --global dotnet-ef dotnet add Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add Microsoft.EntityFrameworkCore.Design dotnet add Microsoft.EntityFrameworkCore.Sqlite

Here are the package references for the project, along with additional NuGet packages included.

<!-- Background Cron Job & Domain name ownership verification --> <PackageReference Include="Quartz.Extensions.Hosting" Version="3.8.0"/> <PackageReference Include="DomainVerifier.Extensions" Version="1.0.0" /> <!-- Minimal Api endpoint extensions--> <PackageReference Include="Carter" Version="8.0.0"/> <!-- Entity Framework Core --> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.00"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <!-- Visual studio container tools --> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6-Preview.2"/> <!-- OpenApi/Swagger --> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />


To use DomainVerifier.Extensions , you must add some configurations to your appsettings.json with the main section named DomainVerifierSettings

{ // ... "DomainVerifierSettings": { "TxtRecordSettings": { "Hostname": "@", // @ but default if not set "RecordAttribute": "mysupersass-verification" // Can be null }, "CnameRecordSettings": { "RecordTarget": "verify.mysupersass.com" // Can not be null } } // ... }


TxtRecordSettings configurations will be useful for generating instructions for users to add a TXT record to its DNS settings. These configurations will also be used by the app to perform DNS Lookup query, confirming ownership. It's strongly recommended not to alter these configurations. If you want custom settings per user, you can skip this part and continue reading the blog post.

For example, if a user has the domain name userdomainexample.com, the DNS settings should resemble the following (based on our configurations):

userdomainexample.comrecord type:value:

Our app will perform a DNS Lookup query at userdomainexample.com, searching for a TXT record with the value mysupersass-verification=Code

Here, "Code" is a unique random verification code, and I will cover how to generate it later.

It's worth noting that the RecordAttribute can be null, and the Hostname may differ from "@." If so, ensure the Hostname is unique to prevent conflicts in user DNS settings.


CnameRecordSettings configurations are designed for CNAME records. While you may not need both configurations, having them provides users with flexibility and options.

Register services and add authentication support

To generate the verification code and perform DNS Lookup, it's essential to register the services provided by the DomainVerifier.Extensions NuGet package. To achieve this, simply invoke the extension method AddDomainVerifierServices on the services collections in the Program.cs file.

var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthorization(); // ... Database, Background services, etc // 👇 Register the DomainVerifier.Extensions services builder.Services.AddDomainVerifierServices(builder.Configuration); // 👇 Authentication builder.Services .AddIdentityApiEndpoints<User>() .AddEntityFrameworkStores<ApplicationDbContext>(); // ... Swagger/OpenAPI builder.Services.AddCarter(); var app = builder.Build(); // HTTP request pipeline ... app.MapGroup("api/users").MapIdentityApi<User>() .WithTags("users"); app.MapCarter(); app.UseHttpsRedirection(); app.UseAuthorization(); app.Run();

The AddDomainVerifierServices method registers two services that you can use later.

Entities modeling

To make our example simple, only two entities will be used.

  • A User that extends the IdentityUser class
public class User: IdentityUser { public ICollection<Domain> Domains { get; set; } }
  • The Domain class looks like follows:
public class Domain { // Value Generated On Add public string DomainId { get; set; } public string DomainName { get; set; } public string VerificationCode { get; set; } // Latest datetime when verification is proceeded public DateTime VerificationDate { get; set; } public bool IsVerified { get; set; } public DateTime? VerificationCompletedDate { get; set; } public string UserId { get; set; } public User User { get; set; } }

💡 The section about entity configurations and DbContext is being omitted here for brevity. Feel free to explore it on our GitHub repository; the link is provided at the end of the blog post.

Create and apply migration

Navigate to the directory where your .csproj file is located. Open a Terminal in that location and enter the following commands:

dotnet ef migrations add "InitialMigration" --output-dir .\Database\Migrations\ dotnet ef database update

💡 Ensure you have installed the dotnet-ef tool, as mentioned in the setup section, before proceeding with the migration command.

Methods that handle the Minimal Api endpoints

As we are using Minimal API, you can create separate handler methods to make your code cleaner.

AddDomain - Enabling Users to Add a Domain Name

When a user adds a domain name, a verification code will be generated, and instructions will be provided to confirm ownership.

using DomainOwnershipSample.Database.Context; using DomainOwnershipSample.Entities; using DomainVerifier.Interfaces; public record AddDomainRequest(string DomainName); public record AddDomainNameResponse(string Txt, string Cname); public static class AddDomain { public static async Task<Results<Created<AddDomainNameResponse>, BadRequest<string>>> Handler( ApplicationDbContext dbContext, ClaimsPrincipal claimsPrincipal, IDnsRecordsGenerator dnsRecordsGenerator, [FromBody] AddDomainRequest request ) { // Get the current user Id var userId = claimsPrincipal.Claims .SingleOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; var existingDomain = await dbContext.Domains .SingleOrDefaultAsync(x => x.DomainName == request.DomainName && x.UserId == userId); // Does the user already add this domain if (existingDomain is not null) { return TypedResults.BadRequest($"Domain name {request.DomainName} already exists"); } // Generate a verification code var verificationCode = dnsRecordsGenerator.GenerateDnsRecord(15); var domain = new Domain { DomainName = request.DomainName, UserId = userId ?? throw new InvalidOperationException("User id is null"), VerificationCode = verificationCode, VerificationDate = DateTime.UtcNow, IsVerified = false, }; dbContext.Domains.Add(domain); await dbContext.SaveChangesAsync(); // Get instructions var cnameInstructions = dnsRecordsGenerator.GetTxtInstructions(request.DomainName, verificationCode); var txtInstructions = dnsRecordsGenerator.GetCnameInstructions(verificationCode); return TypedResults.Created($"{httpContext.Request.Path}/status/{domain.DomainId}", new AddDomainNameResponse(txtInstructions, cnameInstructions)); } }

The background Job that periodically verifies ownership

To implement a background job, you can use the Quartz library, .NET hosted services, or any other background scheduling library. For this blog post, we'll be using Quartz. The goal is to process ownership verification for all unverified domains.

Below is the code for the job:

[DisallowConcurrentExecution] public class ProcessVerificationJob : IJob { private readonly ApplicationDbContext _dbContext; // 👇 Inject the IDnsRecordsVerifier interface from DomainVerifier.Extensions private readonly IDnsRecordsVerifier _dnsRecordsVerifier; public ProcessVerificationJob( ApplicationDbContext dbContext, IDnsRecordsVerifier dnsRecordsVerifier) { _dbContext = dbContext; _dnsRecordsVerifier = dnsRecordsVerifier; } public async Task Execute(IJobExecutionContext context) { var unVerifiedDomains = await _dbContext.Domains .Where(x => !x.IsVerified && !_dbContext.Domains .Any(y => y.IsVerified && y.DomainName == x.DomainName)) .OrderBy(x => x.VerificationDate) .Take(20) .ToListAsync(context.CancellationToken); foreach (var domain in unVerifiedDomains) { domain.VerificationDate = DateTime.UtcNow; // 👇 Verification var isVerificationSucceeded = await _dnsRecordsVerifier.IsTxtRecordValidAsync(domain.DomainName, domain.VerificationCode) || await _dnsRecordsVerifier.IsCnameRecordValidAsync(domain.DomainName, domain.VerificationCode); if (isVerificationSucceeded) { domain.IsVerified = true; domain.VerificationCompletedDate = DateTime.UtcNow; } } _dbContext.UpdateRange(unVerifiedDomains); await _dbContext.SaveChangesAsync(context.CancellationToken); } }

💡 The [DisallowConcurrentExecution] attribute tells Quartz not to execute multiple instances of the job at the same time.

This job performs the following tasks:

  • Periodically loads twenty unverified domains from the database.
  • Verifies ownership status of each domain.
  • Updates the verification status and date in the database.

To register the job and set up a periodicity of 5 minutes, here's how you can do it:

services.AddQuartz(q => { var jobKey = new JobKey(nameof(ProcessVerificationJob)); q.AddJob<ProcessVerificationJob>(opts => opts.WithIdentity(jobKey)); q.AddTrigger(opts => opts .ForJob(jobKey) .WithIdentity($"{nameof(ProcessVerificationJob)}-trigger") .WithCronSchedule("0 0/5 * * * ? *") // Every 5 minutes ); }); services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);

Now that the background service is set up, you can create another endpoint handler that checks the verification status.

GetVerificationStatus - Returns the Ownership Verification Result

The background job is already updating the status in the database; all you need to do is retrieve it from the database.

using DomainOwnershipSample.Database.Context; using DomainVerifier.Interfaces; namespace DomainOwnershipSample.Features.DomainVerifications; public record GetVerificationStatusRequest(string DomainName); public record VerificationStatusResponse( bool IsVerified, DateTime? VerificationDate, DateTime? VerificationCompletedDate); public static class GetVerificationStatus { public static async Task<Results<Ok<VerificationStatusResponse>, BadRequest<string>>> Handler( ApplicationDbContext dbContext, ClaimsPrincipal claimsPrincipal, IDnsRecordsVerifier dnsRecordsVerifier, [AsParameters] GetVerificationStatusRequest request ) { // Get the current user Id var userId = claimsPrincipal.Claims .SingleOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; // Verify if the domain name provided by the user exists var domain = await dbContext.Domains .AsTracking() .SingleOrDefaultAsync(x => x.DomainName == request.DomainName && x.UserId == userId); if (domain is null) { return TypedResults.BadRequest($"Domain name {request.DomainName} not found"); } var response = new VerificationStatusResponse( domain.IsVerified, domain.VerificationDate, domain.VerificationCompletedDate ); return TypedResults.Ok(response); } }

Minimal Api Endpoints

Now that you have defined all handler methods, you can proceed to create the API endpoints.

using Carter; namespace DomainOwnershipSample.Features.DomainVerifications; public class DomainVerificationsModule : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { var group = app.MapGroup("/api/domains") .RequireAuthorization() .WithOpenApi() .WithTags("domains"); group.MapPost("", AddDomain.Handler) .WithName(nameof(AddDomain)) .WithSummary("Registers a domain name for ownership verification"); group.MapGet("status", GetVerificationStatus.Handler) .WithName(nameof(GetVerificationStatus)) .WithSummary("Retrieves the current verification status of a specified domain name"); } }

Two routes are defined, one for registering a domain and another for obtaining the status of the ownership verification.

  • POST /api/domains
  • GET api/domains/status

Final Words

At this point, you have successfully created a fully functional .NET Minimal API designed for domain name ownership verification.

The complete source of this blog post can be found here on GitHub.

If you find this blog post helpful, consider sharing it on your favorite social media platforms. Don't forget to follow me on GitHub and Twitter.




Laurent Egbakou

Laurent Egbakou

Microsoft MVP | Founder

Microsoft MVP | Software Engineer ☁️ | Blogger | Open source contributor


Open Source

Social Media

Microsoft MVPAzure Developer Associate Badge

Related Posts

Resumable File Uploads
Resumable file uploads in .NET
February 21, 2024
6 min
© 2024, All Rights Reserved.
Powered By @lioncoding

French Content 🔜

Quick Links

Advertise with usAbout UsContact UsHire Us

Social Media