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.