From Sending Emails to Building a Passwordless Login System in .NET 8
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:
📦 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:
- Generate 4-digit OTP
- Store it in database with expiry time
- 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.❤️✨





