I’ve been using a simple but effective way to secure my admin login for some time: a one-time PIN that is generated on-demand and emailed directly to me. It’s lightweight, session-scoped, and has worked reliably — and while I’ve been able to test it without HTTPS, I still recommend using HTTPS in production.
1. Login with Username and Password
When I submit my username and password, the server verifies the credentials. If they match, it generates a secure, random PIN using .NET's RandomNumberGenerator
from System.Security.Cryptography
:
// Generate a secure 8-digit PIN
int RandomPin = _AdminHelper.CreateNewAdminPin();
// Email the PIN to the user
SendEmail(UserFound.Email, "www.justingengo.com - One-Time PIN",
string.Format(CultureInfo.InvariantCulture, "Pin: {0}\r\nI.P. Address: {1}", RandomPin, Request.UserHostAddress));
The PIN is stored in the user’s session and expires in 10 minutes. Using a cryptographically secure random number generator ensures the PIN is unpredictable.
2. Entering the PIN
After receiving the email, I enter the PIN on the site. The server checks that the PIN matches the session value and that it has not expired:
// Check that PIN matches and is not expired
if (PinTimeGenerated > DateTime.Now &&
Pin == String.Format(CultureInfo.InvariantCulture, "{0}", _AdminHelper.AdminPin))
{
PinMatches = true;
}
If the PIN is valid, the session clears the stored PIN so it cannot be reused. The user is then logged in with an authentication cookie:
// Clear the PIN so it cannot be reused
_AdminHelper.ClearPinInformation();
// Set the authentication cookie
FormsAuthentication.SetAuthCookie("[email protected]", false);
3. Security Considerations
- Randomness: Using RandomNumberGenerator from System.Security.Cryptography is far more secure than System.Random, preventing guessable PINs.
- Session-scoped storage: The PIN lives only in the user’s session, so it cannot be reused across browsers or devices.
- Expiration: A 10-minute lifetime prevents old PINs from being valid indefinitely.
- Email notifications: Both successful and failed login attempts trigger emails, so I always know what’s happening.
4. Use the Right RandomNumberGenerator — Seriously
Many developers get tripped up because .NET has multiple random number generators, and not all are secure:
- System.Random — NOT secure. Deterministic and predictable if the seed is known.
- System.Security.Cryptography.RandomNumberGenerator — secure. Produces cryptographically strong, unpredictable values. This is the one to use for PINs or security tokens.
Example of wrong vs. correct usage:
// WRONG - Predictable, do not use for PINs
Random rnd = new Random();
int pin = rnd.Next(10000000, 100000000); // NOT secure
// RIGHT - Cryptographically secure PIN
using System.Security.Cryptography;
using (var rng = RandomNumberGenerator.Create())
{
byte[] bytes = new byte[4];
rng.GetBytes(bytes); // fill byte array with secure random data
uint pin = BitConverter.ToUInt32(bytes, 0) & 0x7FFFFFFF; // ensure positive number
// Now 'pin' is unpredictable
}
5. Why I Like This Approach
- It adds a second factor without requiring a separate authenticator app.
- The PIN is easy to generate and check in code, with minimal overhead.
- Even if someone intercepts the PIN, it is short-lived and tied to the session.
6. Lessons Learned
- Double submission can happen with autofill plus manual clicks; disabling the login button or using the PRG (Post-Redirect-Get) pattern can prevent duplicate PIN emails.
- Always use HTTPS to protect credentials in transit — the PIN adds security but does not replace transport-layer encryption.
- Using the wrong RandomNumberGenerator can completely compromise your security; always verify the namespace.
Summary: Using a one-time, session-scoped PIN generated with System.Security.Cryptography.RandomNumberGenerator has made my login process secure and lightweight. The inline tutorial comments show why each step is important and how to avoid common pitfalls.