Authentication, Authorization and Security
Connected vehicles and diagnostic back-ends are regulated targets (ISO/SAE 21434, UNECE R155 cybersecurity).
Concept
Authentication vs Authorization Authentication — *who are you?* The result is a ClaimsPrincipal with claims (name, roles, plant, scopes).
Authorization — *what may you do?* Decided from the claims: roles (RequireRole("Engineer")), policies (combinations of requirements: role + claim + custom logic), resource-based checks.
Unauthenticated → 401 Unauthorized (with a WWW-Authenticate header); authenticated but not allowed → 403 Forbidden.
Browser apps (server-rendered, Blazor Server); mark them HttpOnly, Secure, SameSite JWT bearer tokens
APIs and SPAs/mobile clients: Authorization: Bearer <token>; signed (not encrypted), short-lived, validated for issuer, audience, lifetime and signature
Delegated *authorization*: a client obtains access tokens from an authorization server OpenID Connect
*Authentication* on top of OAuth 2.0 (ID tokens) — Entra ID, Auth0, Keycloak ASP.NET Core Identity
User store, password hashing, lockout, 2FA, external logins for your own user database Threat / topic Input validation
Validate type, length, range, format at every boundary; reject by default SQL injection
Parameterized queries / EF Core (Module 49) Encode output (HtmlEncoder, Razor encodes by default), Content-Security-Policy
Antiforgery tokens for cookie-authenticated forms; SameSite cookies
Never store plain text or fast hashes; use PasswordHasher<T> (PBKDF2, salted, versioned), Argon2 or bcrypt; constant-time comparison Secrets management
User secrets in development, a vault in production; never in Git, logs or URLs
UseHttpsRedirection, HSTS, valid certificates; TLS 1.2+ Token security
Short lifetimes, refresh tokens, validate everything, never put secrets in JWT payloads (they are readable) Sensitive data & logging
Redact tokens, passwords, personal data before logging dotnet list package --vulnerable, Dependabot, update promptly (Module 46) Least privilege
Minimal DB permissions, scoped tokens, separate service accounts
Why It Matters
Connected vehicles and diagnostic back-ends are regulated targets (ISO/SAE 21434, UNECE R155 cybersecurity). A single injection flaw, leaked key or missing authorization check can let an attacker flash ECUs or read customer data.
Syntax
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o => o.TokenValidationParameters = ...);
builder.Services.AddAuthorizationBuilder().AddPolicy("CanFlash", p => p.RequireRole("Engineer").RequireClaim("plant", "PUNE"));
app.UseAuthentication(); app.UseAuthorization();
app.MapPost("/ecus/{code}/flash", ...).RequireAuthorization("CanFlash");
string hash = new PasswordHasher<User>().HashPassword(user, password);Example
52.1 JWT authentication, roles, policies, and core security techniques
The program uses the Microsoft.AspNetCore.Authentication.JwtBearer package. It issues its own tokens with a random key (in real systems an identity provider issues them), then calls protected endpoints with different tokens.
#:sdk Microsoft.NET.Sdk.Web
#:package [email protected]
#:property PublishAot=false
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
// The signing key would come from a secret store (user-secrets, Key Vault), never from source code
byte[] keyBytes = RandomNumberGenerator.GetBytes(32);
var signingKey = new SymmetricSecurityKey(keyBytes);
const string Issuer = "https://auth.example.local", Audience = "diag-api";
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o => o.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Issuer, ValidAudience = Audience, IssuerSigningKey = signingKey,
ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.Zero,
…Expected Result
No token → 401 with a WWW-Authenticate: Bearer challenge; a valid viewer token → 403 because the CanFlash policy requires the Engineer role.
The expired and tampered tokens were rejected with precise reasons (the expiry time is the run's own clock).
The decoded payload shows that JWT claims are readable by anyone who holds the token — sign, don't hide.
Two hashes of the same password differ because each has a random salt, yet both verify.
GET /public/health (anonymous) -> 200 OK ok
GET /me (no token) -> 401 Unauthorized [Bearer]
GET /me (engineer) -> 200 OK asha roles=[Engineer]
POST flash (engineer) -> 200 OK flashing VCU
POST flash (viewer) -> 403 Forbidden
GET /me (expired token) -> 401 Unauthorized [Bearer error="invalid_token", error_description="The token expired at '09/30/2026 03:06:32'"]
GET /me (tampered signature) -> 401 Unauthorized [Bearer error="invalid_token", error_description="The signature key was not found"]
JWT header: HS256; the payload is only Base64Url-encoded, NOT encrypted - anyone can read the claims:
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name = asha
plant = PUNE
http://schemas.microsoft.com/ws/2008/06/identity/claims/role = Engineer
same password, different hashes (random salt): True; length 84
verify correct: Success, wrong: Failed
PBKDF2 derived 32 bytes; constant-time compare equal: True
HTML-encoded: <script>alert('x')</script>
redacted for logs: Server=db;User Id=diag;Password=***;Common Mistakes
Confusing 401 and 403, or authentication and authorization.
Hard-coded signing keys and connection strings in source code.
Long-lived tokens without revocation; accepting tokens without validating audience/issuer/lifetime.
Storing passwords with SHA-256 or MD5 (fast hashes are brute-forced easily).
Building HTML by concatenating user input (XSS).
Logging full requests including Authorization headers.
Checking authorization only in the UI, not on the server.
Best Practices
Use a proven identity provider (Entra ID, Auth0, Keycloak, Duende) with OpenID Connect; don't invent protocols.
Policies over scattered role checks; authorize on every endpoint by default (FallbackPolicy).
Secrets in vaults, rotated; HTTPS everywhere with HSTS.
Encode output, validate input, parameterize SQL.
Threat-model new features (STRIDE) and scan dependencies continuously.
Interview Relevance
Very high: "authentication vs authorization?", "401 vs 403?", "how does JWT validation work?", "OAuth vs OpenID Connect?", "how should passwords be stored?", "how do you prevent XSS, CSRF and SQL injection?", "where do you store secrets?".
Practice Questions
Add a FallbackPolicy that requires authentication for every endpoint except those marked AllowAnonymous().
Write a custom authorization requirement "may flash only during working hours" with an IAuthorizationHandler.
Add refresh tokens: a long-lived opaque token that can be exchanged for a new short-lived JWT.
Store a connection string with dotnet user-secrets set and read it through configuration.
Quick Quiz
Q1. (Multiple choice) A valid token for a user who lacks the required role gets: A) 400 B) 401 C) 403 D) 404
Answer: C (verified: the viewer received 403 Forbidden).
Q2. (True/False) The claims in a signed JWT are encrypted and cannot be read without the key.
Answer: False. They are only Base64Url-encoded (verified: the payload was decoded without the key). The signature only prevents *modification*.
Q3. (Predict the output) hasher.HashPassword(u, "x") == hasher.HashPassword(u, "x")
Answer: False — each hash has a random salt (verified), but VerifyHashedPassword succeeds for both.
Q4. (Find the bug) app.UseAuthorization(); app.UseAuthentication();
Answer: Authentication must run first; otherwise authorization sees an anonymous user and every protected endpoint returns 401.
Coding Challenge
Write HashPassword(string password) and Verify(string password, string stored) using PBKDF2 (SHA-256, 600 000 iterations, 16-byte salt, 32-byte key) with the format iterations.salt.hash in Base64 and a constant-time comparison.
using System.Security.Cryptography;
string stored = HashPassword("S3cure!Bench");
Console.WriteLine($"format ok: {stored.Split('.').Length == 3}, starts with iterations: {stored.StartsWith("600000.")}");
Console.WriteLine($"correct password: {Verify("S3cure!Bench", stored)}, wrong password: {Verify("s3cure!bench", stored)}");
static string HashPassword(string password)
{
const int iterations = 600_000;
byte[] salt = RandomNumberGenerator.GetBytes(16);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, 32);
return $"{iterations}.{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}";
}
static bool Verify(string password, string stored)
{
string[] parts = stored.Split('.');
int iterations = int.Parse(parts[0]);
byte[] salt = Convert.FromBase64String(parts[1]);
byte[] expected = Convert.FromBase64String(parts[2]);
byte[] actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, expected.Length);
return CryptographicOperations.FixedTimeEquals(actual, expected);
}