Sitemap

From Sending Emails to Building a Passwordless Login System in .NET 8

4 min readMar 5, 2026

--

Press enter or click to view image in full size

When I first built my ASP.NET Core Web API, it could handle requests, talk to the database, and process data efficiently.

But it couldn’t talk to users.

There were no emails.
No notifications.
No password resets.
No verification codes.

That’s when I decided to learn how email works inside a backend system — and that journey eventually led me to build a complete OTP-based login system using SMTP.

In this article, I’ll walk through:

  • How I configured SMTP in .NET 8
  • How I structured a reusable EmailService
  • How I built an OTP-based login system
  • The architecture decisions behind it
  • Security lessons I learned along the way
1. Enable 2-Step Verification in Gmail

Go to Google Account security settings
https://myaccount.google.com/security

Find 2-Step Verification

Turn it ON

You must enable this first before generating an app password.

2. Generate Gmail App Password

Go to App Passwords
https://myaccount.google.com/apppasswords

Select:

App → Mail

Device → Other (Custom name)

Enter a name like:
OTP Project

Click Generate

Google will give a 16-character password, something like:

abcd efgh ijkl mnop

This is your SMTP password.

📧 Step 1: Teaching My API to Send Emails

To send emails in .NET, we use SMTP (Simple Mail Transfer Protocol).

Think of SMTP as the “postman of the internet.”
Your application writes the message, and the SMTP server delivers it.

For development, I used Gmail’s SMTP server.

Instead of hardcoding credentials, I configured everything inside appsettings.json:

"SmtpSettings": {
"Server": "smtp.gmail.com",
"Port": 587,
"SenderName": "My OTP App",
"SenderEmail": "your.email@gmail.com",
"Username": "your.email@gmail.com",
"Password": "your-app-password"
}

⚠ Important: Gmail requires an App Password, not your real account password.

Creating a Clean Email Service

Rather than writing email logic inside controllers, I created a dedicated service:

public interface IEmailService
{
Task SendEmailAsync(string toEmail, string subject, string body);
}

Implementation:

public class EmailService : IEmailService
{
private readonly IConfiguration _configuration;

public EmailService(IConfiguration configuration)
{
_configuration = configuration;
}

public async Task SendEmailAsync(string toEmail, string subject, string body)
{
var smtpSettings = _configuration.GetSection("SmtpSettings");

using var client = new SmtpClient(
smtpSettings["Server"],
int.Parse(smtpSettings["Port"]!))
{
Credentials = new NetworkCredential(
smtpSettings["Username"],
smtpSettings["Password"]),
EnableSsl = true
};

var message = new MailMessage
{
From = new MailAddress(
smtpSettings["SenderEmail"],
smtpSettings["SenderName"]),
Subject = subject,
Body = body,
IsBodyHtml = true
};

message.To.Add(toEmail);

await client.SendMailAsync(message);
}
}

Then I registered it in Program.cs:

builder.Services.AddTransient<IEmailService, EmailService>();

At this point, my API could send emails.That’s when I asked myself:

What if I remove passwords entirely… and use email for authentication?

And that’s how the OTP login system was born.

🔐 Step 2: Building a Passwordless OTP Login System

Instead of username + password, I designed a system where:Authentication = Email + One-Time Password

  • No passwords stored.
  • No password hashing needed.
  • No forgotten password complexity.

🏗 Architecture

I followed a clean layered structure:

Press enter or click to view image in full size
This separation made the project easy to maintain and test.

📦 Data Model

I created a UserOtp entity:

public class UserOtp
{
public Guid Id { get; set; }
public string Email { get; set; }
public string OtpCode { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsUsed { get; set; }
}

Key decisions:

  • OTP expires in 10 minutes
  • OTP is single-use
  • Multiple OTPs per email are allowed
  • The latest valid OTP is used for verification

🔄 Login Flow

1.Request OTP

Endpoint: POST /api/otp/send

Get Nirmalichristy’s stories in your inbox

Join Medium for free to get updates from this writer.

Body:

{
"email": "user@example.com"
}

What happens:

  1. Generate 4-digit OTP
  2. Store it in database with expiry time
  3. Send email using EmailService

2.Verify OTP

Endpoint: POST /api/otp/verify?email=…&otpCode=…

Verification rules:

  • OTP exists
  • OTP not expired
  • OTP not already used
  • Code matches

If valid:

existingOtp.IsUsed = true;
await _otpRepository.UpdateAsync(existingOtp);

This ensures:

  • OTP cannot be reused
  • Expired codes are rejected
  • All verification is server-side

🔐 Security Lessons I Learned

1. Never hardcode credentials

Use:

  • User Secrets
  • Environment variables
  • Secure vaults in production

2. OTP must be single-use

Always mark as used after success.

3. OTP must expire

Time-based validation prevents replay attacks.

4.Separate responsibilities

Email sending ≠ OTP logic ≠ Data access. Clean separation makes your system scalable.

🏁 Final Thoughts

What started as:

“How do I send an email from .NET?”

Became:

“How do I build a secure, passwordless authentication system?”

That’s the beauty of backend development.
Every feature is an opportunity to go deeper.

The complete source code for this project is available on GitHub:
LoginSystem-SMTP
https://github.com/cgnirmali/LoginSystem-SMTP.git

Thanks for reading!
This project started as a simple experiment to learn how email works in backend systems, and it eventually turned into a small OTP-based authentication system. If you found this helpful, feel free to check out the source code on GitHub, try it out, and share your feedback.❤️✨

--

--

Written by Nirmalichristy

Hi, I’m Nirmali. On a journey to explore how technology transforms tomorrow