Secure Coding Practices in C#

Mastering Authentication, Encryption, and Data Protection for Modern .NET Applications

Security is no longer a “nice to have” – it is mission-critical.
Every C# application today touches sensitive data in some capacity: user credentials, personal details, payment records, tokens, API keys, session cookies, or configuration secrets.

But developers often unintentionally introduce vulnerabilities through:

  • insecure authentication flows
  • improper password handling
  • weak encryption choices
  • storing sensitive data in plain text
  • leaking secrets through logs
  • unsafe DTOs
  • or poor input validation

Modern .NET (including .NET 8–10) provides a rich, mature security ecosystem, but only if you use it correctly.

This guide shows how to build robust, professional-grade secure code in C#, combining:

  • strong authentication
  • modern password hashing
  • state-of-the-art encryption
  • built-in data protection
  • secure coding patterns
  • and best-practice design principles

All using real, production-ready examples.


🔍 The Problem: Security Is Easy to Get Wrong

Common mistakes C# developers still make:

❌ Storing passwords directly in the database

(even hashed incorrectly with SHA256 instead of slow-hash)

❌ Using simple AES wrappers from StackOverflow

(often with ECB mode — extremely insecure)

❌ Embedding secrets in appsettings.json

(API keys, connection strings, signing keys)

❌ Using unvalidated JWT tokens

(without checking audience or expiry)

❌ Writing sensitive data to logs

(token contents, passwords, error dumps)

❌ Rolling your own crypto

(symmetric key generation, padding, IV reuse)

These patterns lead to:

  • credential leaks
  • token forgery
  • impersonation attacks
  • data exposure
  • broken authentication
  • and compliance failures (GDPR/PCI/SOX)

.NET 10 gives you everything you need to avoid these – but you must follow a secure path.


⚡ The Solution: Built-In Secure Coding Practices in C#

This tutorial walks you through a universal security architecture using official, hardened APIs:

  • ✔ Password hashing with PBKDF2 using PasswordHasher<TUser>
  • ✔ Token-based authentication using JWT with proper validation
  • ✔ AES-256 authenticated encryption using AesGcm
  • ✔ .NET Data Protection API (IDataProtectionProvider)
  • ✔ Secure configuration (user secrets, key vaults, environment variables)
  • ✔ Defensive coding principles throughout

By the end, you’ll be writing battle-tested C# code suitable for production and audits.


🧠 Core Concept: “Never Trust the Input, Never Store Raw Secrets, Never Handle Plain Text Longer Than Necessary”

The golden rule of secure coding is:

All sensitive data must be protected at rest, in transit, and in memory.

This means:

  • Always authenticate
  • Always hash passwords
  • Always encrypt sensitive values
  • Always validate tokens
  • Always minimise exposure
  • Always rotate keys
  • Always secure configurations

🔐 Part 1 — Authentication: Proper Password Handling in C#

❌ Bad example (NEVER do this)

var hash = SHA256.HashData(Encoding.UTF8.GetBytes(password));

SHA256 is fast — meaning attackers can brute-force billions of guesses per second.

✅ Correct approach: PBKDF2 via PasswordHasher

.NET offers a hardened implementation used by ASP.NET Identity.

var hasher = new PasswordHasher<User>();
string hashedPassword = hasher.HashPassword(user, plainPassword);

// Verify
var result = hasher.VerifyHashedPassword(user, hashedPassword, plainPassword);

Features:

  • slow hashing (configurable iteration count)
  • salted
  • tamper-resistant
  • encoded with version metadata

Perfect for any C# application, even outside ASP.NET.


🛡️ Part 2 — Token Security: Safe JWT Validation

Developers often validate only the signature, forgetting:

  • expiration
  • issuer
  • audience
  • token replay

Correct setup:

builder.Services
    .AddAuthentication("Bearer")
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "https://myapi.example.com",
            ValidAudience = "myclient",
            IssuerSigningKey = new SymmetricSecurityKey(secretKeyBytes),
            ClockSkew = TimeSpan.FromSeconds(30)
        };
    });

This ensures:

  • only trusted issuers
  • only expected audiences
  • unexpired tokens
  • signature validation
  • replay protection

🔒 Part 3 — Encryption: Modern C# AES with AesGcm

Most AES examples online are dangerously wrong.

The correct, modern approach is AES-256 GCM, which provides:

  • confidentiality
  • integrity
  • authentication (the ciphertext is self-verifying)

✨ Encrypting

var key = RandomNumberGenerator.GetBytes(32); // 256-bit key
var nonce = RandomNumberGenerator.GetBytes(12); // required size for GCM

using var aes = new AesGcm(key);

var ciphertext = new byte[plaintext.Length];
var tag = new byte[16];

aes.Encrypt(nonce, plaintext, ciphertext, tag);

✨ Decrypting

using var aes = new AesGcm(key);
aes.Decrypt(nonce, ciphertext, tag, plaintextDest);

This is production-grade encryption.
Use it for:

  • sensitive database fields
  • encrypted tokens
  • secure local storage
  • API payloads
  • configuration secrets

🧰 Part 4 — Automatic Data Protection with .NET Data Protection API

This is the API that protects:

  • cookies
  • authentication tokens
  • viewstate
  • anti-forgery tokens

You can use it for your own secure data storage.

var protector = provider.CreateProtector("MyApp.UserSecrets");

string protectedValue = protector.Protect("Sensitive content");
string unprotectedValue = protector.Unprotect(protectedValue);

Benefits:

  • per-machine key ring
  • automatic key rotation
  • AES-256 under the hood
  • tamper protection

Perfect for ASP.NET Core services.


⚙ Secure Configuration Management

Secrets must never be stored in appsettings.json.

Use:

✔ User Secrets (development)

dotnet user-secrets set "Jwt:Key" "supersecret"

✔ Environment variables (production)

export Jwt__Key="supersecret"

✔ Azure Key Vault / AWS Secrets Manager / HashiCorp Vault

❌ NEVER

  • commit secrets
  • log secrets
  • expose stack traces containing secrets

🛡 Defensive Coding Principles in C#

✔ Validate ALL input

Use FluentValidation, DataAnnotations, or custom guards.

✔ Use SecureString?

Not needed – .NET dropped it because OS cannot guarantee memory protection.
Instead: minimise plaintext residency time.

✔ Avoid static keys

Rotate keys frequently.

✔ Do not expose internal exceptions to the client

Wrap everything.

✔ Sanitize logs

Never log:

  • user passwords
  • JWT tokens
  • personal information
  • database connection strings
  • encryption keys

🧩 Real-World Example

Securely Storing and Retrieving Encrypted Customer Data

public class SecureCustomerStore
{
    private readonly byte[] _key;

    public SecureCustomerStore(byte[] key)
    {
        _key = key;
    }

    public string Encrypt(string plaintext)
    {
        var nonce = RandomNumberGenerator.GetBytes(12);
        var textBytes = Encoding.UTF8.GetBytes(plaintext);
        var ciphertext = new byte[textBytes.Length];
        var tag = new byte[16];

        using var aes = new AesGcm(_key);
        aes.Encrypt(nonce, textBytes, ciphertext, tag);

        return Convert.ToBase64String(
            nonce.Concat(ciphertext).Concat(tag).ToArray()
        );
    }

    public string Decrypt(string encrypted)
    {
        var bytes = Convert.FromBase64String(encrypted);

        var nonce = bytes[..12];
        var tag = bytes[^16..];
        var ciphertext = bytes[12..^16];

        var plaintext = new byte[ciphertext.Length];

        using var aes = new AesGcm(_key);
        aes.Decrypt(nonce, ciphertext, tag, plaintext);

        return Encoding.UTF8.GetString(plaintext);
    }
}

This pattern:

  • uses authenticated AES
  • avoids static IV (dangerous!)
  • prevents tampering
  • avoids unnecessary memory copies
  • zero risk of ECB or CBC padding attacks

🧩 Best Practices Checklist

✔ Authentication

  • PBKDF2 password hashing
  • MFA when possible
  • lockout on brute-force attempts

✔ Encryption

  • Use AES-GCM, never AES-ECB
  • Never reuse IVs
  • Generate keys using RNGCryptoServiceProvider/RandomNumberGenerator

✔ Data Protection

  • use Data Protection APIs
  • rotate keys
  • secure key storage

✔ Secure Coding

  • validate everything
  • sanitise logs
  • don’t expose internals
  • avoid timing attacks in comparisons

✔ Deployment

  • HTTPS always
  • No HTTP fallbacks
  • HSTS headers
  • TLS 1.2 minimum
  • CORS restrictions

🧠 Summary

AreaBad Old WayProper Secure C# Way
PasswordsSHA256, custom hashesPBKDF2 via PasswordHasher
JWTOnly signature checkFull validation (issuer, audience, expiry)
EncryptionAES-ECB, unsafe examplesAES-GCM, authenticated encryption
Sensitive DataPlain textProtect using Data Protection API
Secretsappsettings.jsonUser secrets, env vars, vault
InputuncheckedValidation frameworks

Final Thoughts

Secure coding in C# is not difficult — when you follow the right practices.
The .NET platform gives you industrial-grade security primitives, hashing algorithms, token handlers, and protection APIs.

Get authentication right.
Encrypt correctly.
Protect data.
Secure your configuration.
Defend your application.