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.
If you're looking to:
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.
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:
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.
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.
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.com | record type: | value: |
---|---|---|
@ | TXT | mysupersass-verification=Code |
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.
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.
To make our example simple, only two entities will be used.
public class User: IdentityUser
{
public ICollection<Domain> Domains { get; set; }
}
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.
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.
As we are using Minimal API, you can create separate handler methods to make your code cleaner.
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));
}
}
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:
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.
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);
}
}
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.
/api/domains
api/domains/status
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.
Quick Links