How to Design an Auth System with OAuth 2.0 & SSO

How to Design an Auth System with OAuth 2.0 & SSO” A Senior+ Guide | Ayodhyya

Identity Federation, Token-Based Authentication, and Enterprise-Grade SSO From Zero to Production

Senior+ System Design Guide C# · Mermaid · Real-World Case Studies

1. Introduction & Why Auth is Hard

Authentication and authorization are the bedrock of every modern application. Yet they remain two of the most misunderstood, misimplemented, and costly-to-migrate components in software engineering. A poorly designed auth system leads to security breaches, frustrated users, failed compliance audits, and expensive rewrites that can take years. Getting auth right " from the ground up " is arguably the single most impactful architectural decision a team can make.

Authentication " verifying who a user is " has evolved dramatically over the past decade. We have moved from password-based authentication with simple session cookies, through token-based systems like OAuth 1.0 and SAML, to the modern landscape dominated by OAuth 2.0, OpenID Connect, and increasingly passwordless authentication with WebAuthn and passkeys. Each evolution brought better security, improved user experience, and new complexity. The industry consensus around OAuth 2.0 as the authorization framework and OpenID Connect as the identity layer has standardized how applications handle auth across web, mobile, and API contexts.

Authorization " verifying what a user is allowed to do " has evolved in parallel. Simple role-based access control (RBAC) has given way to attribute-based access control (ABAC), relationship-based access control (ReBAC), and policy-based systems using OPA (Open Policy Agent) or Cedar. Modern authorization must handle multi-tenant isolation, fine-grained resource-level permissions, and dynamic policy evaluation that adapts to context (time of day, location, device posture, risk score).

The complexity multiplies when you introduce Single Sign-On (SSO). SSO promises users the convenience of a single login across multiple applications, but it introduces the problem of identity federation " how does Application A trust that a user authenticated by Identity Provider B is who they claim to be? This trust is established through cryptographic signatures, SAML assertions, or OpenID Connect ID tokens, each with its own token format, validation rules, and protocol quirks.

Key Insight: The most common auth failure is not a security vulnerability in the protocol itself " it is a misconfiguration in the implementation. Misconfigured redirect URIs, expired signing certificates, missing token validation (signature, audience, issuer, expiration), and weak secret management account for the vast majority of auth-related breaches. A well-designed auth system bakes these validations into the framework so developers cannot accidentally skip them.

Real-World Case Studies

CompanySystemScaleKey Innovation
Auth0 / OktaIdentity-as-a-Service100M+ users, millions of tenantsUniversal Login, Actions (custom rules), multi-protocol
GoogleGoogle Identity PlatformBillions of accountsOne Tap, passkeys, risk-based auth, reCAPTCHA Enterprise
MicrosoftAzure AD / Entra ID500M+ enterprise usersConditional Access, Identity Protection, seamless SSO
GitHubOAuth & GitHub Apps100M+ developersFine-grained PATs, OAuth app scopes, token expiration
AmazonCognitoHundreds of thousands of appsFederation across social + enterprise IdPs, adaptive auth

Auth0's Universal Login is particularly instructive: rather than embedding login UI in each application, Auth0 hosts a centralized login page that applications redirect to. This means security updates (like adding MFA support, CAPTCHA, or passwordless options) are deployed once for the auth server, and apply to all client applications automatically. Enterprise SSO tenants get SAML or OIDC federation on top of the same login page. This architecture " the centralized authorization server " is the pattern we adopt throughout this guide.

Microsoft's Conditional Access demonstrates the power of risk-based policies: a login from a known device on the corporate network during business hours might require only a password, while a login from a new device in a different country on a weekend requires MFA and device compliance checks. The auth system must support this kind of dynamic policy evaluation " it is not enough to decide "is the password correct?" " the system must evaluate the entire context of the authentication request and apply appropriate policies.

This guide covers every aspect of designing a production-grade authentication and authorization system. We start with the fundamental distinction between authentication and authorization, then dive deep into each protocol and technology: OAuth 2.0 flows (authorization code, client credentials, device code), PKCE, OpenID Connect, SAML 2.0, JWT tokens, token rotation, session management, MFA with TOTP and WebAuthn, password hashing with Argon2id, account lockout, RBAC and ABAC, API key management, rate limiting, audit logging, social login, multi-tenant SSO, SCIM provisioning, security protections (CSRF, XSS, token theft), compliance (SOC 2, GDPR), monitoring, cost estimation, API design, and testing strategy. Each section includes production-ready C# code, Mermaid diagrams, tables, and real-world case studies.

2. Functional & Non-Functional Requirements

Functional Requirements

  1. User Registration & Login: Email/password, social login (Google, GitHub, Microsoft, Apple), SSO via SAML/OIDC, and passwordless options (magic links, WebAuthn).
  2. Multi-Factor Authentication: TOTP (time-based one-time passwords), SMS/email codes, WebAuthn (FIDO2 hardware keys, platform authenticators), and backup codes.
  3. Session Management: Short-lived access tokens (15 minutes), longer-lived refresh tokens (7-90 days), session revocation, and device management.
  4. Authorization: Role-based access control (RBAC) with hierarchical roles, attribute-based access control (ABAC) with dynamic policy evaluation, resource-level permissions.
  5. API Security: API key management with granular scopes, rate limiting per tenant/user/IP, audit logging of all API calls.
  6. User Management: Admin UI for user CRUD, group management, role assignment, account lockout/unlock, password reset.
  7. Enterprise SSO: SAML 2.0 and OpenID Connect federation, SCIM provisioning for user/group sync, just-in-time provisioning.
  8. Audit & Compliance: Immutable audit log of authentication events, admin actions, token issuance/revocation. SOC 2 and GDPR compliance support.

Non-Functional Requirements

RequirementTargetRationale
Authentication Latency (P99)< 500ms (including IdP calls)Users abandon login flows that take too long
Token Validation Latency (P99)< 10ms (local JWKS cache)API gateways validate every request " must be fast
Availability99.99% (52 minutes downtime/year)Auth is a critical dependency for all other services
Token Issuance Throughput100K tokens/secondPeak login demand (Monday morning, Black Friday)
User Scale10 million registered usersMid-to-large SaaS platform
Tenant Scale100,000 tenants (multi-tenant)Enterprise SaaS with many customers
Concurrent Active Sessions50 millionUsers on multiple devices
Brute Force ProtectionLockout after 5 failed attempts in 15 minutesIndustry standard (OWASP recommendation)
Password Hash StrengthArgon2id, time cost ≥ 3, memory cost ≥ 64MBResistant to GPU/ASIC attacks
Session Timeout (idle)30 minutesOWASP session management best practice
Audit Log Retention1 year hot, 7 years cold archiveSOC 2 and GDPR compliance requirements
Recovery Point ObjectiveZero data lossAuth data loss means permanent user lockout
Recovery Time Objective< 5 minutesAuth downtime stops all application access

Key Design Tradeoffs

TradeoffOption AOption BOur Choice
Token StorageServer-side sessions (revocable, stateful)Self-contained JWT (stateless, fast)JWT for access tokens, DB for refresh tokens
Password Storagebcrypt (well-audited, slower iteration)Argon2id (modern, tunable)Argon2id (PHC winner)
Auth ProtocolProprietary token systemOAuth 2.0 + OIDC (standardized)OAuth 2.0 + OIDC (interoperability)
SSO ProtocolSAML 2.0 (enterprise, XML-based)OIDC (modern, JSON-based)Both (enterprise requires SAML)
MFA ApproachTOTP (standard, app-based)WebAuthn (passwordless, strong)Both layered; WebAuthn as primary
Authorization ModelRBAC (simple, role hierarchy)ABAC (fine-grained, dynamic)RBAC by default, ABAC for complex

3. Authentication vs Authorization " The Foundation

Authentication (AuthN) answers the question: "Who are you?" It is the process of verifying a user's identity through credentials: something you know (password), something you have (phone, hardware key), or something you are (biometric). Authentication is about establishing identity " the system confirms that the user presenting credentials is indeed who they claim to be.

Authorization (AuthZ) answers the question: "What are you allowed to do?" It is the process of determining whether an authenticated user has permission to access a specific resource or perform a specific action. Authorization always depends on authentication " you cannot authorize an unauthenticated user. But the reverse is not true: you can authenticate a user and then deny them authorization for a particular action.

This distinction is critical because different protocols and technologies handle each concern differently. OAuth 2.0 is fundamentally an authorization framework " it issues tokens that grant access to resources. OpenID Connect is an authentication layer built on top of OAuth 2.0 " it adds an ID token that proves the user's identity. SAML 2.0 handles both authentication (via assertions about user identity) and authorization (via attributes in the assertion) in a single protocol.

graph TB subgraph AuthN["Authentication - Who are you?"] A1["Password"] A2["TOTP / MFA"] A3["WebAuthn / Passkey"] A4["Social Login"] A5["SSO / SAML / OIDC"] end subgraph AuthZ["Authorization - What can you do?"] B1["RBAC - Role Based"] B2["ABAC - Attribute Based"] B3["ReBAC - Relationship Based"] B4["Policy Engine (OPA/Cedar)"] end AuthN --> AuthZ AuthZ --> C["Access Granted/Denied"]

The Complete Auth Pipeline

The auth pipeline follows this sequence: (1) User presents credentials, (2) Authentication service validates credentials, (3) Auth service issues a token containing user identity and claims, (4) User presents token to application, (5) Application validates token, (6) Authorization service evaluates policies against token claims, (7) Access granted or denied. Each step has specific requirements for security, performance, and reliability.

A common antipattern is embedding authorization logic inside the authentication service. The auth service should authenticate and issue tokens containing identity claims (user ID, roles, groups, attributes). An independent authorization service or policy engine should evaluate those claims against resource-specific policies. This separation allows the authorization model to evolve independently of the authentication mechanism.

Architectural Principle: Authentication is a cross-cutting concern that should be centralized (a single source of truth for identity). Authorization is an application-specific concern that should be decentralized (each service knows its own permission rules). Attempting to centralize all authorization logic leads to an unmaintainable monolith that requires coordinated releases across all services to change any permission rule.

4. OAuth 2.0 Deep Dive " Authorization Framework

OAuth 2.0 (RFC 6749) is the industry-standard protocol for authorization. It enables applications to obtain limited access to user accounts on an HTTP service. It works by delegating user authentication to the service that hosts the user account, and authorizing third-party applications to access that user's data. OAuth 2.0 provides authorization flows for web, desktop, mobile, and server-side applications.

OAuth 2.0 defines four roles: the Resource Owner (the user who owns the data), the Resource Server (the API that hosts the data), the Client (the application requesting access), and the Authorization Server (the server that issues tokens). The authorization flow involves the client requesting authorization from the resource owner, obtaining an authorization grant, exchanging it for an access token at the authorization server, and using the access token to access the resource server.

Authorization Code Flow (Most Secure)

The Authorization Code flow is the most secure OAuth 2.0 flow and is recommended for server-side web applications. The client never sees the user's credentials " it receives an authorization code after the user authenticates, then exchanges that code for an access token using a client secret. This two-step process ensures that the access token is never exposed to the user's browser or any intermediate party.

sequenceDiagram participant User as User (Resource Owner) participant Browser as Browser participant App as Web App (Client) participant Auth as Auth Server participant API as Resource Server User->>Browser: Click "Login with OAuth" Browser->>App: Request resource App->>Browser: Redirect to /authorize Browser->>Auth: GET /authorize?client_id&redirect_uri&response_type=code Auth->>User: Login page (authenticate) User->>Auth: Submit credentials Auth->>Browser: Redirect to redirect_uri?code=AUTH_CODE Browser->>App: GET redirect_uri?code=AUTH_CODE App->>Auth: POST /token (code + client_id + client_secret) Auth->>App: { access_token, refresh_token, expires_in } App->>API: GET /resource (Authorization: Bearer access_token) API->>API: Validate token (signature, expiry, scope) API->>App: { resource data } App->>Browser: Display page

Client Credentials Flow (Machine-to-Machine)

The Client Credentials flow is used for server-to-server communication where no user is involved. The client authenticates directly with the authorization server using its client ID and client secret (or a signed JWT assertion) and receives an access token. This flow is commonly used for backend services, cron jobs, and API integrations. The token scope is determined by the client's permissions, not a user's permissions.

C#
public class ClientCredentialsService
{
    private readonly HttpClient _client;
    private readonly TokenCache _cache = new();
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    public ClientCredentialsService(HttpClient client) { _client = client; }

    public async Task<string> GetTokenAsync(string endpoint, string id, string secret, string[] scopes)
    {
        var cacheKey = $"{id}:{string.Join(",", scopes)}";
        if (_cache.TryGet(cacheKey, out string cachedToken)) return cachedToken;

        await _semaphore.WaitAsync();
        try
        {
            if (_cache.TryGet(cacheKey, out cachedToken)) return cachedToken;

            var req = new HttpRequestMessage(HttpMethod.Post, endpoint)
            {
                Content = new FormUrlEncodedContent(new[]
                {
                    new KeyValuePair<string, string>("grant_type", "client_credentials"),
                    new KeyValuePair<string, string>("client_id", id),
                    new KeyValuePair<string, string>("client_secret", secret),
                    new KeyValuePair<string, string>("scope", string.Join(" ", scopes))
                })
            };

            var resp = await _client.SendAsync(req);
            resp.EnsureSuccessStatusCode();
            var json = await resp.Content.ReadFromJsonAsync<TokenResponse>();

            _cache.Add(cacheKey, json.AccessToken, TimeSpan.FromSeconds(json.ExpiresIn - 60));
            return json.AccessToken;
        }
        finally { _semaphore.Release(); }
    }
}

public record TokenResponse(string AccessToken, int ExpiresIn, string TokenType);

Device Authorization Flow (Input-Constrained Devices)

The Device Authorization flow (RFC 8628) is designed for devices with limited input capabilities " smart TVs, IoT devices, CLI tools, and game consoles. The device shows a code and URL, the user visits that URL on another device to authenticate, and the device polls the authorization server until the user completes authentication. This flow is increasingly important as IoT and streaming devices proliferate.

C#
public class DeviceAuthorizationFlow
{
    private readonly HttpClient _http;
    private readonly string _deviceAuthEndpoint;
    private readonly string _tokenEndpoint;

    public async Task StartAsync(string clientId, CancellationToken ct)
    {
        // Step 1: Request device code
        var req = new HttpRequestMessage(HttpMethod.Post, _deviceAuthEndpoint)
        {
            Content = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("client_id", clientId),
                new KeyValuePair<string, string>("scope", "openid profile email")
            })
        };

        var resp = await _http.SendAsync(req, ct);
        resp.EnsureSuccessStatusCode();
        var deviceResp = await resp.Content.ReadFromJsonAsync<DeviceAuthResponse>(ct);

        Console.WriteLine($"Visit {deviceResp.VerificationUri}");
        Console.WriteLine($"Enter code: {deviceResp.UserCode}");

        // Step 2: Poll for token
        while (!ct.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromSeconds(deviceResp.Interval), ct);
            var tokenResp = await _http.PostAsync(_tokenEndpoint,
                new FormUrlEncodedContent(new[]
                {
                    new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
                    new KeyValuePair<string, string>("device_code", deviceResp.DeviceCode),
                    new KeyValuePair<string, string>("client_id", clientId)
                }), ct);

            if (tokenResp.IsSuccessStatusCode)
            {
                var token = await tokenResp.Content.ReadFromJsonAsync<TokenResponse>(ct);
                Console.WriteLine($"Token: {token.AccessToken}");
                return;
            }
            var error = await tokenResp.Content.ReadFromJsonAsync<OAuthError>(ct);
            if (error?.Error == "access_denied") throw new Exception("User denied");
            if (error?.Error == "expired_token") throw new Exception("Code expired");
            // authorization_pending -> continue polling
        }
    }
}

OAuth 2.0 Grant Type Comparison

Grant TypeUse CaseSecret NeededRefresh TokenSecurity
Authorization CodeServer-side web appsYesYesHighest
Authorization Code + PKCEMobile, SPA, NativeNoYesHigh
Client CredentialsM2M, backend servicesYesNoHigh
Device AuthorizationTVs, CLI, IoTOptionalYesMedium
Implicit (Deprecated)SPA (legacy)NoNoLow
Password (Deprecated)LegacyYesYesLow
Important: OAuth 2.1 (consolidating RFC 6819, 8252, 8628, 9700) removes the Implicit and Password flow entirely. PKCE is required for all public clients. All new implementations should target OAuth 2.1 compliance.

5. PKCE " Securing Public Clients

Proof Key for Code Exchange (PKCE, pronounced "pixy") is an OAuth 2.0 extension (RFC 7636) that prevents authorization code interception attacks. Public clients (mobile apps, SPAs, native applications) cannot store secrets securely " an attacker can reverse-engineer the app and extract embedded secrets. Without PKCE, an attacker who intercepts the authorization code (via a compromised redirect URI, man-in-the-middle on mobile networks, or malicious app on the same device) can exchange it for an access token.

The PKCE flow: (1) The client generates a random code_verifier. (2) The client computes the SHA-256 hash to produce the code_challenge. (3) During authorization, the client sends the code_challenge and method (S256). (4) When exchanging the code for tokens, the client sends the original code_verifier. (5) The server hashes the verifier and compares it with the stored challenge. If they match, the exchange proceeds. An attacker who intercepts the code cannot complete the exchange without the verifier.

C#
public class PkceService
{
    private const int VerifierBytes = 64;
    private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create();

    public (string verifier, string challenge) GeneratePKCE()
    {
        var bytes = new byte[VerifierBytes];
        Rng.GetBytes(bytes);
        var verifier = Base64UrlEncode(bytes);
        var challenge = Base64UrlEncode(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));
        return (verifier, challenge);
    }

    public string BuildAuthorizationUrl(string authorizeEndpoint, string clientId,
        string redirectUri, string challenge, string state, string scope)
    {
        var query = QueryHelpers.AddQueryString(authorizeEndpoint, new Dictionary<string, string>
        {
            ["response_type"] = "code", ["client_id"] = clientId,
            ["redirect_uri"] = redirectUri, ["scope"] = scope,
            ["state"] = state, ["code_challenge"] = challenge,
            ["code_challenge_method"] = "S256"
        });
        return query;
    }

    public async Task<TokenResponse> ExchangeCodeAsync(string tokenEndpoint,
        string clientId, string redirectUri, string code, string verifier)
    {
        var resp = await new HttpClient().PostAsync(tokenEndpoint,
            new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("grant_type", "authorization_code"),
                new KeyValuePair<string, string>("code", code),
                new KeyValuePair<string, string>("client_id", clientId),
                new KeyValuePair<string, string>("redirect_uri", redirectUri),
                new KeyValuePair<string, string>("code_verifier", verifier)
            }));
        resp.EnsureSuccessStatusCode();
        return await resp.Content.ReadFromJsonAsync<TokenResponse>();
    }

    private static string Base64UrlEncode(byte[] data) =>
        Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
Best Practice: PKCE is mandatory for all public clients under OAuth 2.1. Even server-side apps benefit from adding it as defense-in-depth. The overhead is minimal (one extra HTTP parameter) and the security benefit is substantial. Only the Client Credentials flow is exempt (no authorization code involved).

6. OpenID Connect " Identity on Top of OAuth

OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0. Defined by the OpenID Foundation, OIDC adds an ID token " a JSON Web Token (JWT) that contains claims about the authenticated user's identity. OIDC standardizes how clients obtain and verify user identity, providing a simple, RESTful approach to authentication that works across web, mobile, and native applications.

OIDC introduces three key elements: the openid scope (signals the client wants an ID token), the ID Token (a signed JWT containing identity claims like sub, iss, aud, exp, iat, and optional profile claims), and the UserInfo endpoint (a standardized REST API for fetching additional user profile claims). OIDC also defines discovery metadata at /.well-known/openid-configuration.

C#
public class OidcAuthenticationHandler
{
    private readonly IConfigurationManager<OpenIdConnectConfiguration> _configManager;

    public OidcAuthenticationHandler(string authority)
    {
        _configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            $"{authority}/.well-known/openid-configuration",
            new OpenIdConnectConfigurationRetriever(),
            new HttpClient())
        {
            AutomaticRefreshInterval = TimeSpan.FromHours(1)
        };
    }

    public async Task<ClaimsPrincipal> ValidateIdTokenAsync(string idToken, string clientId)
    {
        var config = await _configManager.GetConfigurationAsync();
        var handler = new JsonWebTokenHandler();

        var result = await handler.ValidateTokenAsync(idToken, new TokenValidationParameters
        {
            ValidIssuer = config.Issuer,
            ValidAudience = clientId,
            IssuerSigningKeys = config.SigningKeys,
            ValidateIssuer = true, ValidateAudience = true,
            ValidateLifetime = true, ValidateIssuerSigningKey = true,
            ClockSkew = TimeSpan.FromMinutes(2)
        });

        if (!result.IsValid)
            throw new SecurityTokenException($"Token validation failed: {result.Exception?.Message}");

        return new ClaimsPrincipal(result.ClaimsIdentity);
    }

    public async Task<UserInfo> FetchUserInfoAsync(string authority, string accessToken)
    {
        var config = await _configManager.GetConfigurationAsync();
        var req = new HttpRequestMessage(HttpMethod.Get, config.UserInfoEndpoint);
        req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var resp = await new HttpClient().SendAsync(req);
        resp.EnsureSuccessStatusCode();
        var json = await resp.Content.ReadFromJsonAsync<UserInfo>();
        return json;
    }
}

public record UserInfo(
    [property: JsonPropertyName("sub")] string Subject,
    [property: JsonPropertyName("name")] string? Name,
    [property: JsonPropertyName("email")] string? Email,
    [property: JsonPropertyName("email_verified")] bool? EmailVerified,
    [property: JsonPropertyName("picture")] string? Picture);

OIDC vs SAML 2.0

FeatureOpenID ConnectSAML 2.0
Token FormatJWT (JSON, compact, web-friendly)SAML Assertion (XML, verbose)
TransportHTTP REST (JSON over HTTPS)HTTP POST Redirect, SOAP bindings
Discovery/.well-known/openid-configurationFederation metadata XML (larger)
Mobile/SPA SupportExcellent (native JSON parsing)Poor (complex XML parsing on mobile)
API IntegrationNatural (access tokens + ID tokens)Awkward (SAML assertions for API auth)
Maturity2014 (widely adopted)2005 (deep enterprise penetration)
Enterprise SupportGrowing (Azure AD, Okta, Google)Deep (ADFS, Okta, OneLogin, Shibboleth)
Logout (SLO)Session management + RP-initiated logoutStandardized SLO with backchannel
Attribute ExchangeUserInfo endpoint (claims)SAML attribute statements
Guidance: For B2C applications, use OIDC exclusively. For B2B, support both OIDC and SAML " many large enterprises require SAML for their identity federation. Abstract the protocol behind a common interface so downstream services don't care how the user authenticated.

7. SAML 2.0 " Enterprise SSO Protocol

SAML 2.0 is an XML-based standard for exchanging authentication and authorization data between security domains. SAML is the dominant protocol for enterprise Single Sign-On " it is how employees log into hundreds of SaaS applications using their corporate credentials. Despite its age (2005), SAML remains a mandatory protocol for any enterprise-facing auth system.

SAML defines three roles: the Principal (user), the Identity Provider (IdP, e.g., Azure AD, Okta, ADFS), and the Service Provider (SP, the application). The flow: user attempts to access an SP resource, SP generates an AuthnRequest and redirects the user to the IdP, the user authenticates at the IdP, the IdP generates a SAML assertion and POSTs it to the SP's ACS URL, and the SP validates the assertion and establishes a session.

sequenceDiagram participant User as User participant App as App (Service Provider) participant IdP as Enterprise IdP User->>App: Request resource App->>App: Generate AuthnRequest App->>User: Redirect to IdP SSO URL User->>IdP: POST SAML Request IdP->>IdP: Authenticate (password, MFA) IdP->>User: POST SAML Response User->>App: POST SAML Response to ACS URL App->>App: Validate assertion App->>User: Resource

SAML Assertion Validation

C#
public class SamlValidator
{
    private readonly X509Certificate2 _idpCert;
    private readonly string _expectedAudience;
    private readonly string _expectedIssuer;
    private readonly TimeSpan _clockSkew = TimeSpan.FromMinutes(3);

    public SamlValidator(X509Certificate2 cert, string audience, string issuer)
    {
        _idpCert = cert; _expectedAudience = audience; _expectedIssuer = issuer;
    }

    public (string nameId, IReadOnlyDictionary<string, string> attributes) Validate(string samlResponseXml)
    {
        var doc = new XmlDocument { PreserveWhitespace = true };
        doc.LoadXml(samlResponseXml);

        var ns = new XmlNamespaceManager(doc.NameTable);
        ns.AddNamespace("saml2p", "urn:oasis:names:tc:SAML:2.0:protocol");
        ns.AddNamespace("saml2", "urn:oasis:names:tc:SAML:2.0:assertion");
        ns.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");

        // Verify XML Digital Signature
        var signedXml = new SignedXml(doc);
        var sigNode = doc.SelectSingleNode("//ds:Signature", ns);
        if (sigNode == null) throw new Exception("Missing signature");
        signedXml.LoadXml(sigNode as XmlElement);
        if (!signedXml.CheckSignature(_idpCert, true))
            throw new Exception("Invalid signature");

        // Extract assertion
        var assertion = doc.SelectSingleNode("//saml2:Assertion", ns);
        if (assertion == null) throw new Exception("Missing assertion");

        // Validate issuer
        var issuer = assertion.SelectSingleNode("saml2:Issuer", ns)?.InnerText;
        if (issuer != _expectedIssuer) throw new Exception($"Issuer mismatch: {issuer}");

        // Validate audience
        var audience = assertion.SelectSingleNode("//saml2:Audience", ns)?.InnerText;
        if (audience != _expectedAudience) throw new Exception($"Audience mismatch: {audience}");

        // Validate timestamps
        var conditions = assertion.SelectSingleNode("saml2:Conditions", ns);
        if (conditions != null)
        {
            var notBefore = DateTime.Parse(conditions.Attributes["NotBefore"].Value);
            var notOnOrAfter = DateTime.Parse(conditions.Attributes["NotOnOrAfter"].Value);
            var now = DateTime.UtcNow;
            if (now < notBefore.Add(-_clockSkew)) throw new Exception("Assertion not yet valid");
            if (now >= notOnOrAfter.Add(_clockSkew)) throw new Exception("Assertion expired");
        }

        // Extract attributes
        var attrs = new Dictionary<string, string>();
        var attrNodes = assertion.SelectNodes("//saml2:Attribute", ns);
        if (attrNodes != null)
            foreach (XmlNode attr in attrNodes)
            {
                var name = attr.Attributes["Name"].Value;
                var value = attr.SelectSingleNode("saml2:AttributeValue", ns)?.InnerText;
                if (value != null) attrs[name] = value;
            }

        var nameId = assertion.SelectSingleNode("//saml2:NameID", ns)?.InnerText
            ?? throw new Exception("Missing NameID");

        return (nameId, attrs);
    }
}
SAML Security Checklist: (1) Always verify the XML Digital Signature. (2) Validate NotBefore/NotOnOrAfter with clock skew. (3) Check the Audience restriction. (4) Verify the Issuer. (5) Use Subject Confirmation. (6) Reject duplicate assertions by storing AssertionID. (7) Use HTTPS. (8) Rotate IdP certificates before expiration.

8. JWT " Access & Refresh Tokens

JSON Web Tokens (JWT, RFC 7519) are the backbone of modern token-based authentication. A JWT consists of three base64url-encoded parts separated by dots: the header (signing algorithm + key ID), the payload (claims), and the signature (cryptographic protection). Access tokens are short-lived bearer tokens (15 minutes by default); refresh tokens are long-lived credentials used to obtain new access tokens without requiring the user to re-authenticate.

C#
public class TokenService
{
    private readonly SigningCredentials _credentials;
    private readonly string _issuer, _audience;
    private readonly TimeSpan _accessTtl = TimeSpan.FromMinutes(15);
    private readonly TimeSpan _refreshTtl = TimeSpan.FromDays(90);
    private readonly IRefreshTokenStore _rtStore;
    private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create();

    public TokenService(string issuer, string audience, SigningCredentials creds, IRefreshTokenStore rtStore)
    {
        _issuer = issuer; _audience = audience; _credentials = creds; _rtStore = rtStore;
    }

    public async Task<(string accessToken, string refreshToken, DateTime expires)>
        IssueTokensAsync(string userId, string tenantId, string[] roles, string sessionId)
    {
        var now = DateTime.UtcNow;
        var exp = now.Add(_accessTtl);
        var handler = new JwtSecurityTokenHandler();

        var token = handler.CreateJwtSecurityToken(
            issuer: _issuer, audience: _audience,
            subject: new ClaimsIdentity(new Claim[]
            {
                new(JwtRegisteredClaimNames.Sub, userId),
                new("tenant_id", tenantId),
                new("session_id", sessionId),
                new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            }.Concat(roles.Select(r => new Claim("role", r)))),
            notBefore: now, expires: exp,
            signingCredentials: _credentials);

        var at = handler.WriteToken(token);
        var rt = await IssueRefreshTokenAsync(userId, sessionId);
        return (at, rt, exp);
    }

    private async Task<string> IssueRefreshTokenAsync(string userId, string sessionId)
    {
        var raw = Convert.ToBase64String(Rng.GetBytes(32));
        var hash = ComputeHash(raw);
        var token = $"{hash}.{userId}.{sessionId}";
        await _rtStore.SaveAsync(hash, userId, sessionId, _refreshTtl);
        return token;
    }

    public ClaimsPrincipal ValidateAccessToken(string accessToken)
    {
        var handler = new JwtSecurityTokenHandler();
        return handler.ValidateToken(accessToken, new TokenValidationParameters
        {
            ValidIssuer = _issuer, ValidAudience = _audience,
            IssuerSigningKey = _credentials.Key,
            ValidateIssuer = true, ValidateAudience = true,
            ValidateLifetime = true, ValidateIssuerSigningKey = true,
            ClockSkew = TimeSpan.Zero
        }, out _);
    }

    private static string ComputeHash(string raw) =>
        Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(raw)));
}

JWT Best Practices

PracticeRationale
Use asymmetric signing (RS256/ES256)Resource servers can validate without talking to auth server
Short access token lifetime (15 min)Limit window of vulnerability if token is stolen
Always validate signature, exp, nbf, iss, audPrevent forged tokens and token reuse across services
Include jti (token ID)Enable token blacklisting for immediate revocation
Never accept unsigned tokensJWTs must always be signed; never use alg=none
Rotate JWKS keys periodicallyLimit impact of key compromise; rotate every 90 days
Keep JWT payload minimalLarge headers degrade performance; keep under 8KB
Security Note: JWTs are bearer tokens " anyone in possession of the token can use it. Always transmit over HTTPS, store access tokens in memory (not localStorage), store refresh tokens in httpOnly cookies, and implement token rotation. Never accept the "alg": "none" header " this is a common JWT vulnerability where an attacker modifies the algorithm to "none" and removes the signature.

9. Token Rotation & Revocation

Token rotation is a critical security practice. Refresh token rotation ensures that every time a refresh token is used, a new refresh token is returned and the old one is invalidated. This limits the window of vulnerability: if an attacker steals a refresh token, using it will fail (because the legitimate user already rotated it) and trigger a security alert. This automatic theft detection is the primary reason to implement rotation.

C#
public class RefreshTokenManager
{
    private readonly IDatabase _redis;
    private readonly ILogger _logger;

    public RefreshTokenManager(IConnectionMultiplexer redis, ILogger logger)
    { _redis = redis.GetDatabase(); _logger = logger; }

    public async Task<string> RotateAsync(string oldRefreshToken, string userId, string sessionId, TimeSpan ttl)
    {
        var parts = oldRefreshToken.Split('.');
        if (parts.Length != 3) throw new SecurityException("Invalid refresh token format");
        var oldHash = parts[0];

        var newRaw = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
        var newHash = ComputeHash(newRaw);
        var newToken = $"{newHash}.{userId}.{sessionId}";

        var oldKey = $"rt:{userId}:{oldHash}";
        var newKey = $"rt:{userId}:{newHash}";

        var tran = _redis.CreateTransaction();
        var existed = tran.KeyDeleteAsync(oldKey);
        _ = tran.StringSetAsync(newKey, sessionId, ttl);
        var committed = await tran.ExecuteAsync();

        if (!committed || !(await existed))
        {
            _logger.LogWarning("Token reuse detected for user {UserId}, session {SessionId}", userId, sessionId);
            throw new SecurityException("Refresh token reuse detected " possible token theft");
        }

        return newToken;
    }

    public async Task RevokeAllAsync(string userId)
    {
        var server = _redis.Multiplexer.GetServer(...);
        var keys = server.Keys(pattern: $"rt:{userId}:*");
        foreach (var key in keys) await _redis.KeyDeleteAsync(key);
    }

    private static string ComputeHash(string input) =>
        Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
}
Token Theft Detection: If a refresh token is used twice (attacker and legitimate user), the first rotation invalidates the old token; the second use fails. The system logs a "token reuse" event, revokes ALL sessions for that user, and notifies security. This is the single best reason to implement rotation.

Revocation Strategies

StrategyLatencyStorage CostComplexity
JTI Blacklist (Redis)Real-timeLow (key per revoked token)Low
Token Version NumberNext requestLow (one field per user)Low
Short Expiration15 min maxNoneNone
Session ID in JWTReal-timeMedium (session store)Medium
Combined (All of above)Real-timeMediumMedium

10. Session Management

Session management ties together authentication, token issuance, and user activity. Each session represents a user's authenticated presence on a device. Sessions contain device info, IP, creation time, last activity, and associated tokens. The lifecycle: create on login, maintain via refresh token usage, expire on logout or timeout. A session cleanup background service runs every 5 minutes, evicting idle sessions (30 min for web, 7 days for mobile).

C#
public class SessionManager
{
    private readonly IDatabase _redis;

    public SessionManager(IConnectionMultiplexer redis) { _redis = redis.GetDatabase(); }

    public async Task<Session> CreateAsync(string userId, string tenantId, string device, string ip)
    {
        var sessionId = Guid.NewGuid().ToString();
        var session = new Session(sessionId, userId, tenantId, device, ip, DateTime.UtcNow, DateTime.UtcNow, true);
        var json = JsonSerializer.Serialize(session);
        await _redis.StringSetAsync($"session:{sessionId}", json, TimeSpan.FromDays(30));
        await _redis.SetAddAsync($"user_sessions:{userId}", sessionId);
        return session;
    }

    public async Task<Session?> GetAsync(string sessionId)
    {
        var json = await _redis.StringGetAsync($"session:{sessionId}");
        return json.HasValue ? JsonSerializer.Deserialize<Session>(json!) : null;
    }

    public async Task TouchAsync(string sessionId)
    {
        var json = await _redis.StringGetAsync($"session:{sessionId}");
        if (!json.HasValue) return;
        var session = JsonSerializer.Deserialize<Session>(json!)!;
        session = session with { LastActivity = DateTime.UtcNow };
        await _redis.StringSetAsync($"session:{sessionId}", JsonSerializer.Serialize(session), TimeSpan.FromDays(30));
    }

    public async Task RevokeAsync(string sessionId)
    {
        var json = await _redis.StringGetAsync($"session:{sessionId}");
        if (!json.HasValue) return;
        var session = JsonSerializer.Deserialize<Session>(json!)!;
        await _redis.KeyDeleteAsync($"session:{sessionId}");
        await _redis.SetRemoveAsync($"user_sessions:{session.UserId}", sessionId);
    }

    public async Task RevokeAllAsync(string userId)
    {
        var sessionIds = await _redis.SetMembersAsync($"user_sessions:{userId}");
        foreach (var sid in sessionIds) await _redis.KeyDeleteAsync($"session:{sid}");
        await _redis.KeyDeleteAsync($"user_sessions:{userId}");
    }
}

public record Session(string SessionId, string UserId, string TenantId,
    string DeviceInfo, string Ip, DateTime CreatedAt, DateTime LastActivity, bool IsActive);
Session Security: (1) Bind sessions to user-agent and IP. (2) Require re-authentication for sensitive actions. (3) Limit concurrent sessions per user. (4) Show a "sessions" page with device info and allow remote revocation. (5) Implement idle timeout (30 min web, 7 days mobile) and absolute timeout (24 hours web, 30 days mobile).

11. Multi-Factor Authentication " TOTP & WebAuthn

MFA adds a second layer of security beyond passwords. Our system supports TOTP (time-based one-time passwords via authenticator apps), WebAuthn/FIDO2 (hardware keys and platform authenticators like Face ID, Touch ID, Windows Hello), and backup codes (10 single-use codes for recovery).

TOTP Implementation

C#
public class TotpService
{
    private const int CodeLen = 6;
    private const int Period = 30;

    public (string secret, string url) GenerateSecret(string email, string issuer)
    {
        var secret = RandomNumberGenerator.GetBytes(20);
        var b32 = Base32Encoding.ToString(secret);
        var url = $"otpauth://totp/{Uri.EscapeDataString(issuer)}:{Uri.EscapeDataString(email)}" +
                  $"?secret={b32}&issuer={Uri.EscapeDataString(issuer)}&algorithm=SHA1&digits=6&period=30";
        return (b32, url);
    }

    public bool Validate(string secret, string code)
    {
        var secretBytes = Base32Encoding.ToBytes(secret);
        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        var window = now / Period;
        for (long i = window - 1; i <= window + 1; i++)
            if (ComputeTotp(secretBytes, i) == code) return true;
        return false;
    }

    private string ComputeTotp(byte[] secret, long window)
    {
        var time = BitConverter.GetBytes(window);
        if (BitConverter.IsLittleEndian) Array.Reverse(time);
        using var hmac = new HMACSHA1(secret);
        var hash = hmac.ComputeHash(time);
        var offset = hash[hash.Length - 1] & 0x0F;
        var binary = (hash[offset] & 0x7F) << 24 | (hash[offset+1] & 0xFF) << 16 |
                     (hash[offset+2] & 0xFF) << 8 | (hash[offset+3] & 0xFF);
        var otp = binary % (int)Math.Pow(10, CodeLen);
        return otp.ToString(new string('0', CodeLen));
    }
}

WebAuthn

WebAuthn (W3C standard) uses public-key cryptography for passwordless authentication. The device generates a key pair; the private key stays on the device (secured by TPM, Secure Enclave, biometrics); the public key is registered with the server. Authentication uses a cryptographic challenge-response: the server sends a challenge, the device signs it with the private key, and the server verifies with the public key. WebAuthn is phishing-resistant " the challenge is bound to the origin URL, so a fake site cannot relay the challenge to the real authenticator.

MethodPhishing ResistantUXCostRecovery
TOTPMediumGood (6-digit code)FreeBackup codes
SMS/EmailLowPoor (wait for SMS)$0.01/SMSFallback channel
WebAuthn HardwareHighGood (tap key)$25-$70Multiple keys
WebAuthn PlatformHighExcellent (Face ID / Touch ID)Free (built-in)iCloud Keychain sync
MFA Strategy: Offer at least two methods. Require MFA for admin users and high-risk operations. Generate 10 backup codes during MFA enrollment. Encourage WebAuthn as the primary method " it is phishing-resistant and provides the best user experience.

12. Password Hashing " Argon2id & bcrypt

Password hashing is the most critical security control for credential auth. The hash function must be computationally expensive, memory-hard (GPU/ASIC resistant), and salted per user. Argon2id is the winner of the Password Hashing Competition (PHC) and the recommended algorithm. bcrypt is acceptable for legacy systems.

C#
public class PasswordHasher
{
    private const int TimeCost = 4;
    private const int MemoryCost = 65536;  // 64 MB in KiB
    private const int Parallelism = 8;
    private const int SaltBytes = 16;
    private const int HashBytes = 32;

    public string Hash(string password)
    {
        var salt = RandomNumberGenerator.GetBytes(SaltBytes);
        var hash = Argon2id.HashPassword(
            Encoding.UTF8.GetBytes(password), salt,
            new Argon2idConfig
            {
                TimeCost = TimeCost, MemoryCost = MemoryCost,
                Parallelism = Parallelism, HashLength = HashBytes
            });

        var salt64 = Convert.ToBase64String(salt);
        var hash64 = Convert.ToBase64String(hash);
        return $"$argon2id$v=19$m={MemoryCost},t={TimeCost},p={Parallelism}${salt64}${hash64}";
    }

    public bool Verify(string password, string hashedPassword)
    {
        var parts = hashedPassword.Split('$');
        if (parts.Length != 6 || parts[1] != "argon2id")
            throw new FormatException("Invalid hash format");

        var memory = int.Parse(parts[3].Split(',')[0].Split('=')[1]);
        var time = int.Parse(parts[3].Split(',')[1].Split('=')[1]);
        var parallelism = int.Parse(parts[3].Split(',')[2].Split('=')[1]);
        var salt = Convert.FromBase64String(parts[4]);
        var expected = Convert.FromBase64String(parts[5]);

        var computed = Argon2id.HashPassword(
            Encoding.UTF8.GetBytes(password), salt,
            new Argon2idConfig
            {
                TimeCost = time, MemoryCost = memory,
                Parallelism = parallelism, HashLength = expected.Length
            });

        return CryptographicOperations.FixedTimeEquals(expected, computed);
    }
}

Algorithm Comparison

AlgorithmMemory-HardGPU/ASIC ResistantRecommended For
Argon2idYesExcellentNew systems
bcryptMinimalModerateLegacy systems
PBKDF2NoPoorBackward compat only
scryptYesGoodAlternative to Argon2
SHA-256 (plain)NoTrivial to crackNever use
NEVER: (1) Use unsalted hashes. (2) Use fast hash functions (SHA-*, MD5). (3) Roll your own crypto. (4) Store passwords in plaintext. (5) Truncate passwords (support up to 128 chars). (6) Email passwords. (7) Log passwords. (8) Send passwords in URLs. These mistakes cause the majority of credential breaches.

13. Account Lockout & Brute Force Protection

Account lockout prevents brute-force attacks by temporarily disabling accounts after a threshold of failed attempts. We track failed attempts per user and per IP to mitigate both targeted and distributed attacks. Progressive delays and CAPTCHA are added after multiple failures.

C#
public class LockoutService
{
    private readonly IDatabase _redis;
    private const int MaxPerUser = 5;
    private const int MaxPerIp = 20;
    private const int Window = 15; // minutes
    private const int LockoutDuration = 30; // minutes

    public LockoutService(IConnectionMultiplexer redis) { _redis = redis.GetDatabase(); }

    public async Task<LockoutResult> CheckAsync(string userId, string ip)
    {
        var userFails = (int)await _redis.StringGetAsync($"fail:{userId}");
        var ipFails = (int)await _redis.StringGetAsync($"fail:ip:{ip}");
        var remainingUser = Math.Max(0, MaxPerUser - userFails);
        var remainingIp = Math.Max(0, MaxPerIp - ipFails);

        if (userFails >= MaxPerUser) return LockoutResult.Locked($"Account locked for {LockoutDuration} min");
        if (ipFails >= MaxPerIp) return LockoutResult.Locked($"IP blocked for {LockoutDuration} min");
        return LockoutResult.Allowed(remainingUser, remainingIp);
    }

    public async Task RecordFailureAsync(string userId, string ip)
    {
        var expiry = TimeSpan.FromMinutes(Window);
        await _redis.StringIncrementAsync($"fail:{userId}");
        await _redis.KeyExpireAsync($"fail:{userId}", expiry);
        await _redis.StringIncrementAsync($"fail:ip:{ip}");
        await _redis.KeyExpireAsync($"fail:ip:{ip}", expiry);
    }

    public async Task ResetAsync(string userId, string ip)
    {
        await _redis.KeyDeleteAsync($"fail:{userId}");
        await _redis.KeyDeleteAsync($"fail:ip:{ip}");
    }
}

public class LockoutResult
{
    public bool IsLocked { get; set; }
    public string Message { get; set; }
    public int RemainingAttempts { get; set; }
    public static LockoutResult Allowed(int user, int ip) =>
        new() { IsLocked = false, RemainingAttempts = Math.Min(user, ip) };
    public static LockoutResult Locked(string msg) =>
        new() { IsLocked = true, Message = msg };
}

Additional Protections

  • CAPTCHA: After 3 failed attempts, require CAPTCHA to distinguish humans from bots.
  • Rate Limiting: 10 login requests/second/IP at the API gateway.
  • Anomaly Detection: Flag impossible travel (NYC to Tokyo in under an hour), new devices, unusual locations.
  • Credential Stuffing: Check against Have I Been Pwned API during registration and password change.
  • Progressive Delay: After 3 failures, add 2s → 4s → 8s artificial delays before responding.
Policy: Never reveal whether the username exists " use "Invalid credentials" for all errors. Allow admin unlock. Auto-unlock after lockout period. Separate counters for password vs MFA code vs backup code failures.

14. RBAC & ABAC " Authorization Models

Authorization decides whether an authenticated user can access a resource. Role-Based Access Control (RBAC) assigns users to roles, and roles have permissions. Attribute-Based Access Control (ABAC) evaluates policies against user, resource, action, and environmental attributes. RBAC is simpler; ABAC is necessary for fine-grained, context-aware authorization.

RBAC Implementation

C#
public class RbacService
{
    private readonly IRoleRepository _repo;
    private readonly ICache _cache;

    public async Task<bool> IsAuthorizedAsync(string userId, string tenantId, string permission, string? resourceId = null)
    {
        var roles = await GetRolesAsync(userId, tenantId);
        foreach (var role in roles)
        {
            bool hasPermission = role.Permissions.Contains(permission) || role.Permissions.Contains("*");
            bool hasResourceAccess = resourceId == null || resourceId.StartsWith(role.ResourcePattern);
            if (hasPermission && hasResourceAccess) return true;
        }
        return false;
    }

    private async Task<List<Role>> GetRolesAsync(string userId, string tenantId)
    {
        var key = $"rbac:{tenantId}:{userId}";
        var cached = await _cache.GetAsync<List<Role>>(key);
        if (cached != null) return cached;

        var roles = await _repo.GetUserRolesAsync(userId, tenantId);
        var all = new List<Role>(roles);
        // Resolve role inheritance
        foreach (var r in roles.Where(r => !string.IsNullOrEmpty(r.ParentId)))
            all.AddRange(await GetInheritedRolesAsync(r.ParentId));
        return await _cache.SetAsync(key, all.Distinct().ToList(), TimeSpan.FromMinutes(15));
    }
}

public record Role(string Id, string Name, string? ParentId, string ResourcePattern, List<string> Permissions);

ABAC with Policy Engine

ABAC uses policies that evaluate the full context of each request: user attributes, resource attributes, action type, and environment (time, location, device, risk score). We use a policy engine inspired by OPA (Open Policy Agent) with rule-based policies stored in the database.

ModelGranularityComplexityBest For
RBACCoarse (role)LowMost B2B SaaS
Hierarchical RBACCoarse, inheritedMediumOrg hierarchies, projects
ABACFine-grainedHighHealthcare, finance
ReBACResource-specificHighGitHub, Figma, Drive
Policy (OPA/Cedar)Very fineVery highMulti-region, compliance
Recommendation: Start with RBAC " it covers 80% of use cases. Introduce ABAC only when you need fine-grained or context-aware policies. Use a policy engine (OPA, Cedar) when policies need to change without code deploys.

15. API Key Management

API keys provide non-user authentication for programs, scripts, and integrations. Keys are long-lived credentials that must be scoped to minimal permissions, revocable, and never stored in plaintext. Our system: generates a key on enrollment, shows it once, stores only the hash (SHA-256) and an encrypted copy for admin recovery.

C#
public class ApiKeyService
{
    private readonly IApiKeyRepository _repo;
    private readonly IEncryption _encryptor;

    public ApiKeyService(IApiKeyRepository repo, IEncryption encryptor)
    { _repo = repo; _encryptor = encryptor; }

    public ApiKeyCreated Generate(string userId, string tenantId, string label, string[] scopes, TimeSpan? expiresIn = null)
    {
        var rawKey = $"{Guid.NewGuid():N}{Guid.NewGuid():N}";
        var hash = HashKey(rawKey);
        var encrypted = _encryptor.Encrypt(rawKey);

        var key = new ApiKey
        {
            Id = Guid.NewGuid(), UserId = userId, TenantId = tenantId,
            KeyHash = hash, EncryptedKey = encrypted,
            Label = label, Scopes = scopes, IsActive = true,
            CreatedAt = DateTime.UtcNow, ExpiresAt = expiresIn.HasValue ? DateTime.UtcNow.Add(expiresIn.Value) : null
        };
        _repo.SaveAsync(key);
        return new ApiKeyCreated(key.Id, rawKey, rawKey[..8], key.CreatedAt);
    }

    public bool Validate(string rawKey, string requiredScope)
    {
        var hash = HashKey(rawKey);
        var key = _repo.GetByHash(hash);
        if (key == null || !key.IsActive) return false;
        if (key.ExpiresAt.HasValue && DateTime.UtcNow > key.ExpiresAt.Value) return false;
        if (!key.Scopes.Contains(requiredScope) && !key.Scopes.Contains("*")) return false;
        return true;
    }

    private static string HashKey(string raw) =>
        Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(raw)));
}

public record ApiKey(Guid Id, string UserId, string TenantId, string KeyHash, string EncryptedKey,
    string Label, string[] Scopes, bool IsActive, DateTime CreatedAt, DateTime? ExpiresAt);
API Key Best Practices: (1) Show the key only once during creation. (2) Never store in plaintext " use SHA-256 hash + AES-256 encryption. (3) Support key rotation with overlapping validity windows. (4) Log every API key usage with timestamp, IP, endpoint. (5) Send notifications on first use and unusual usage patterns.

16. Rate Limiting & Throttling

Rate limiting protects the auth system from abuse: brute-force password guessing, DDoS on endpoints, and API key misuse. We implement a three-tier scheme: per user/API key, per IP, and global. The sliding window algorithm using Redis sorted sets provides precise rate limiting without the boundary problems of fixed windows.

C#
public class SlidingWindowRateLimiter
{
    private readonly IDatabase _redis;

    public SlidingWindowRateLimiter(IConnectionMultiplexer redis) { _redis = redis.GetDatabase(); }

    public async Task<RateLimitResult> CheckAsync(string key, int limit, TimeSpan window)
    {
        var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        var windowStart = now - (long)window.TotalMilliseconds;

        // Redis sorted set: score = timestamp, member = unique request ID
        var script = @"
            redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])
            local count = redis.call('ZCARD', KEYS[1])
            if count < tonumber(ARGV[2]) then
                redis.call('ZADD', KEYS[1], ARGV[3], ARGV[4])
                redis.call('EXPIRE', KEYS[1], ARGV[5])
                return {0, count + 1, ARGV[5]}
            else
                local oldest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES')
                return {1, count, math.ceil(oldest[2] - ARGV[1])}
            end
        ";
        var requestId = Guid.NewGuid().ToString();
        var result = (int[])await _redis.ScriptEvaluateAsync(script,
            new RedisKey[] { $"rl:{key}" },
            new RedisValue[] { windowStart.ToString(), limit.ToString(), now.ToString(), requestId, ((int)window.TotalSeconds).ToString() });

        return new RateLimitResult(result[0] == 0, result[1], now + result[2]);
    }

    public async Task<bool> CheckLoginRateAsync(string userId, string ip)
    {
        // Per-user: 10 requests/minute
        // Per-IP: 100 requests/minute
        var userOk = (await CheckAsync($"login:user:{userId}", 10, TimeSpan.FromMinutes(1))).IsAllowed;
        var ipOk = (await CheckAsync($"login:ip:{ip}", 100, TimeSpan.FromMinutes(1))).IsAllowed;
        return userOk && ipOk;
    }
}

public record RateLimitResult(bool IsAllowed, int CurrentCount, long ResetsAtMs);
Rate Limiting Strategy: (1) Different limits for different endpoints (login: strict, token refresh: moderate, public keys: relaxed). (2) Return Retry-After headers for rate-limited responses. (3) Rate limit at the API gateway and at the application layer for defense-in-depth. (4) Allow burst up to 2x the limit before blocking. (5) Send the rate limit status in response headers (X-RateLimit-Remaining, X-RateLimit-Reset).

17. Audit Logging & Observability

Audit logging is essential for security, compliance (SOC 2, GDPR), and debugging. Every authentication event, token issuance, role change, MFA enrollment, and admin action must be logged in an append-only, immutable audit trail. Logs are written to a dedicated audit database and streamed to an enterprise SIEM.

C#
public class AuditLogger
{
    private readonly IDatabase _redis;
    private readonly ILogger _logger;
    private static readonly ConcurrentQueue<AuditEvent> _buffer = new();

    public AuditLogger(IConnectionMultiplexer redis, ILogger logger)
    { _redis = redis.GetDatabase(); _logger = logger; }

    public void LogEvent(string userId, string tenantId, string eventType,
        string description, string? ip = null, Dictionary<string, string>? metadata = null)
    {
        var evt = new AuditEvent
        {
            Id = Guid.NewGuid(), Timestamp = DateTime.UtcNow,
            UserId = userId, TenantId = tenantId, EventType = eventType,
            Description = description, Ip = ip, Metadata = metadata
        };
        _buffer.Enqueue(evt);

        if (_buffer.Count >= 100)
        {
            var batch = new List<AuditEvent>();
            while (_buffer.TryDequeue(out var e)) batch.Add(e);
            FlushBatchAsync(batch);
        }
    }

    private async Task FlushBatchAsync(List<AuditEvent> events)
    {
        foreach (var e in events)
        {
            var key = $"audit:{e.TenantId}:{e.Timestamp:yyyyMMdd}";
            await _redis.StreamAddAsync(key, new[]
            {
                new NameValueEntry("id", e.Id.ToString()),
                new NameValueEntry("ts", e.Timestamp.ToString("O")),
                new NameValueEntry("user", e.UserId),
                new NameValueEntry("type", e.EventType),
                new NameValueEntry("desc", e.Description),
                new NameValueEntry("ip", e.Ip ?? "")
            });
        }
        _logger.LogInformation("Flushed {Count} audit events", events.Count);
    }
}

public record AuditEvent
{
    public Guid Id { get; init; }
    public DateTime Timestamp { get; init; }
    public string UserId { get; init; }
    public string TenantId { get; init; }
    public string EventType { get; init; }
    public string Description { get; init; }
    public string? Ip { get; init; }
    public Dictionary<string, string>? Metadata { get; init; }
}

Audit Event Types

CategoryEventsRetention
Authenticationlogin_success, login_failure, logout, mfa_challenge, password_change7 years
Tokentoken_issued, token_refreshed, token_revoked, token_reuse_detected1 year
Adminuser_created, user_deleted, role_assigned, role_removed, permission_changed7 years
MFAmfa_enrolled, mfa_disabled, backup_codes_generated, webauthn_key_registered1 year
Securityip_blocked, rate_limit_exceeded, suspicious_login, impossible_travel1 year
Compliancedata_export, data_deletion, consent_granted, consent_revoked7 years
Audit Best Practices: (1) Make audit logs append-only never allow update or delete. (2) Use a write-ahead log or database that guarantees immutability. (3) Ensure log entries cannot be modified by attackers even with DB access. (4) Include correlation IDs to trace events across services. (5) Stream audit events to a SIEM (Splunk, Datadog, Elastic) for real-time alerting. (6) Test audit log integrity periodically.

18. Social Login " Google, GitHub, Microsoft

Social login allows users to authenticate using their existing accounts from Google, GitHub, Microsoft, Apple, or other providers. This improves conversion rates (fewer passwords to remember), reduces support burden (password resets), and provides verified identity data (email, name, avatar). The implementation uses OAuth 2.0 / OIDC with each provider, abstracted behind a common social login provider interface.

C#
public interface ISocialLoginProvider
{
    string Name { get; }
    string BuildAuthorizationUrl(string state, string redirectUri);
    Task<SocialUser> HandleCallbackAsync(string code, string redirectUri);
}

public class GoogleSocialLoginProvider : ISocialLoginProvider
{
    private readonly string _clientId, _clientSecret;
    private readonly HttpClient _http = new();

    public string Name => "Google";

    public GoogleSocialLoginProvider(string clientId, string clientSecret)
    { _clientId = clientId; _clientSecret = clientSecret; }

    public string BuildAuthorizationUrl(string state, string redirectUri) =>
        $"https://accounts.google.com/o/oauth2/v2/auth?client_id={_clientId}&redirect_uri={Uri.EscapeDataString(redirectUri)}&response_type=code&scope=openid%20email%20profile&state={state}";

    public async Task<SocialUser> HandleCallbackAsync(string code, string redirectUri)
    {
        var tokenResp = await _http.PostAsync("https://oauth2.googleapis.com/token",
            new FormUrlEncodedContent(new[] {
                new KeyValuePair<string, string>("code", code),
                new KeyValuePair<string, string>("client_id", _clientId),
                new KeyValuePair<string, string>("client_secret", _clientSecret),
                new KeyValuePair<string, string>("redirect_uri", redirectUri),
                new KeyValuePair<string, string>("grant_type", "authorization_code")
            }));
        tokenResp.EnsureSuccessStatusCode();
        var tokenJson = await tokenResp.Content.ReadFromJsonAsync<Dictionary<string, dynamic>>();

        var userResp = await _http.GetAsync($"https://www.googleapis.com/oauth2/v2/userinfo?access_token={tokenJson["access_token"]}");
        userResp.EnsureSuccessStatusCode();
        var user = await userResp.Content.ReadFromJsonAsync<GoogleUser>();

        return new SocialUser(user.Id, user.Email, user.Name, user.Picture, user.EmailVerified, "google");
    }
}

public record SocialUser(string ProviderId, string Email, string Name, string AvatarUrl, bool EmailVerified, string Provider);
public record GoogleUser(string Id, string Email, string Name, string Picture, bool VerifiedEmail);
Social Login Strategy: (1) Use the provider's OIDC discovery URL for dynamic configuration. (2) Handle account linking " same email from Google and GitHub should map to the same user. (3) Verify email_verified claim from the provider. (4) Support Apple Sign In (mandatory for iOS apps). (5) Implement automatic account creation for first-time social logins (JIT provisioning). (6) Allow users to link multiple social accounts to their profile.

19. Multi-Tenant SSO Architecture

Multi-tenant SSO allows each customer (tenant) to configure their own identity provider. Tenant A might use Azure AD with SAML, Tenant B uses Okta with OIDC, and Tenant C uses Google Workspace. The auth system must isolate tenant configurations, route authentication requests to the correct IdP, and manage tenant-specific signing certificates, metadata, and user provisioning.

graph TB TenantA["Tenant A (Azure AD SAML)"] --> Auth["Auth Server"] TenantB["Tenant B (Okta OIDC)"] --> Auth TenantC["Tenant C (Google)"] --> Auth Auth --> U1["User A \n SSO via Azure AD"] Auth --> U2["User B \n SSO via Okta"] Auth --> U3["User C \n Social Login"] Auth --> DB[(Tenant Config DB)] Auth --> Cache[(Redis Cache)]
C#
public class MultiTenantSsoService
{
    private readonly ITenantRepository _tenants;
    private readonly Dictionary<string, ISamlService> _samlServices = new();
    private readonly Dictionary<string, IOidcService> _oidcServices = new();

    public async Task<TenantConfig> GetTenantConfigAsync(string tenantId)
    {
        var tenant = await _tenants.GetByIdAsync(tenantId);
        if (tenant.IdentityProvider.Type == "saml" && !_samlServices.ContainsKey(tenantId))
        {
            _samlServices[tenantId] = new SamlService(tenant.IdentityProvider.MetadataUrl, tenant.IdentityProvider.Certificate);
        }
        else if (tenant.IdentityProvider.Type == "oidc" && !_oidcServices.ContainsKey(tenantId))
        {
            _oidcServices[tenantId] = new OidcService(tenant.IdentityProvider.DiscoveryUrl, tenant.IdentityProvider.ClientId, tenant.IdentityProvider.ClientSecret);
        }
        return tenant;
    }

    public async Task<AuthResult> HandleSsoLoginAsync(string tenantId, string protocol, string response)
    {
        var tenant = await GetTenantConfigAsync(tenantId);
        if (protocol == "saml")
        {
            var (nameId, attrs) = _samlServices[tenantId].ValidateResponse(response);
            var user = await FindOrProvisionUser(tenantId, nameId, attrs);
            return AuthResult.Success(user);
        }
        else if (protocol == "oidc")
        {
            var (idToken, userInfo) = await _oidcServices[tenantId].HandleCallbackAsync(response);
            var claims = await _oidcServices[tenantId].ValidateIdTokenAsync(idToken);
            var user = await FindOrProvisionUser(tenantId, claims.FindFirst("sub").Value, new Dictionary<string, string>());
            return AuthResult.Success(user);
        }
        throw new NotSupportedException($"Protocol {protocol} not configured for tenant {tenantId}");
    }
}

public record TenantConfig(string Id, string Name, IdentityProviderConfig IdentityProvider);
public record IdentityProviderConfig(string Type, string MetadataUrl, string Issuer,
    string Certificate, string DiscoveryUrl, string ClientId, string ClientSecret);
Multi-Tenant Considerations: (1) Each tenant has its own IdP configuration; never share across tenants. (2) Tenant-specific signing certificates must have different KIDs to avoid collision. (3) Rate limit per tenant. (4) Tenant data must be strictly isolated " never allow Tenant A users to authenticate as Tenant B. (5) Support tenant admins uploading their own IdP metadata and certificates. (6) Implement tenant-specific branding on login pages.

20. SCIM Provisioning " User Sync

System for Cross-domain Identity Management (SCIM, RFC 7643/7644) is a RESTful protocol for automating user and group provisioning between an IdP and a service provider. Enterprises use SCIM to automatically create, update, and deactivate user accounts when changes occur in their HR system or directory. When an employee leaves the company, SCIM deactivates their account in all SaaS applications within minutes.

C#
public class ScimController : ControllerBase
{
    private readonly IScimService _scim;
    private readonly IUserRepository _users;

    [HttpPost("/scim/v2/Users")]
    [ProducesResponseType(typeof(ScimUser), 201)]
    public async Task<IActionResult> CreateUser([FromBody] ScimCreateUserRequest request)
    {
        var user = new User
        {
            Id = Guid.NewGuid(), ExternalId = request.ExternalId ?? Guid.NewGuid().ToString(),
            Email = request.Emails?.FirstOrDefault()?.Value,
            FirstName = request.Name?.GivenName, LastName = request.Name?.FamilyName,
            IsActive = request.Active
        };
        user = await _users.CreateAsync(user);
        return CreatedAtAction(nameof(GetUser), new { id = user.Id }, ToScimUser(user));
    }

    [HttpPatch("/scim/v2/Users/{id}")]
    public async Task<IActionResult> PatchUser(Guid id, [FromBody] ScimPatchRequest request)
    {
        var user = await _users.GetByIdAsync(id);
        foreach (var op in request.Operations)
        {
            if (op.Op == "replace" && op.Path == "active")
                user.IsActive = (bool)op.Value;
            else if (op.Op == "replace" && op.Path == "emails[type eq \"work\"].value")
                user.Email = op.Value.ToString();
        }
        await _users.UpdateAsync(user);
        return Ok(ToScimUser(user));
    }

    [HttpDelete("/scim/v2/Users/{id}")]
    public async Task<IActionResult> DeactivateUser(Guid id)
    {
        var user = await _users.GetByIdAsync(id);
        user.IsActive = false;
        await _users.UpdateAsync(user);
        return NoContent();
    }

    private ScimUser ToScimUser(User u) => new()
    {
        Schemas = new[] { "urn:ietf:params:scim:schemas:core:2.0:User" },
        Id = u.Id.ToString(),
        ExternalId = u.ExternalId,
        Name = new ScimName { GivenName = u.FirstName, FamilyName = u.LastName },
        Emails = new[] { new ScimEmail { Value = u.Email, Primary = true } },
        Active = u.IsActive,
        Meta = new ScimMeta { ResourceType = "User", Created = u.CreatedAt, LastModified = u.UpdatedAt }
    };
}

public record ScimCreateUserRequest(string? ExternalId, ScimName? Name, ScimEmail[]? Emails, bool Active);
public record ScimPatchRequest(List<ScimPatchOperation> Operations);
public record ScimPatchOperation(string Op, string Path, object Value);
public record ScimUser { public string[] Schemas { get; init; } public string Id { get; init; } public string ExternalId { get; init; } public ScimName Name { get; init; } public ScimEmail[] Emails { get; init; } public bool Active { get; init; } public ScimMeta Meta { get; init; } }
public record ScimName(string GivenName, string FamilyName);
public record ScimEmail(string Value, bool Primary);
public record ScimMeta(string ResourceType, DateTime Created, DateTime LastModified);
SCIM Benefits: (1) Automatic user provisioning " no manual account creation. (2) Automatic deactivation when employees leave " critical for security audits. (3) Group and role provisioning via SCIM Groups endpoint. (4) Just-in-time (JIT) provisioning: create user on first SSO login if they don't exist. (5) SCIM sync status dashboard for IT admins showing successful/failed syncs, pending changes, and error details.

21. Security " CSRF, XSS, Token Theft

The auth system itself must be secure against web application threats. We implement defenses against Cross-Site Request Forgery (CSRF), Cross-Site Scripting (XSS), token theft, and other attack vectors. The auth server is a high-value target " a compromise would grant attackers access to all connected applications.

CSRF Protection

In OAuth flows, the state parameter prevents CSRF on the authorization callback. The client generates a random state value before redirecting to the auth server, and validates it in the callback. Without this check, an attacker could trick a user into authorizing the attacker's app. The state parameter must be: (1) cryptographically random (minimum 128 bits), (2) bound to the user's session, (3) single-use, and (4) time-limited (5 minutes).

XSS Protection

The auth server renders user-controlled content (names, email addresses) on admin panels and user profile pages. All rendered content must be HTML-encoded. The auth server sets Content-Security-Policy headers restricting script sources. Using the auth server as a Universal Login page means it must be especially hardened against XSS since any script injection could steal tokens.

Token Theft Mitigation

Attack VectorMitigation
Access token in URLUse authorization code flow (code, never token, in URL)
Access token in localStorageStore in memory only; if needed for persistence, use httpOnly cookies
Refresh token theftRotation: each use invalidates old token; reuse = alert
Authorization code interceptionPKCE: attacker can't exchange code without verifier
Man-in-the-middleHTTPS with HSTS; pin certificates for critical endpoints
ClickjackingX-Frame-Options: DENY on auth pages
Session fixationGenerate new session ID after login
Log injectionSanitize user input before writing to audit logs
Security Hardening Checklist: (1) HSTS preload with max-age=31536000. (2) CSP with strict script-src. (3) X-Content-Type-Options: nosniff. (4) X-Frame-Options: DENY. (5) Set-Cookie with SameSite=Lax, HttpOnly, Secure. (6) Rate limit all endpoints. (7) Input validation on all parameters. (8) Output encoding on all responses. (9) No sensitive data in URLs. (10) Audit all admin operations. (11) Run automated security scans (OWASP ZAP, SonarQube). (12) Penetration testing before major releases.

22. Compliance " SOC 2, GDPR, FedRAMP

Enterprise auth systems must comply with regulatory frameworks: SOC 2 (service organization controls), GDPR (EU data protection), FedRAMP (US government cloud), HIPAA (healthcare), and PCI DSS (payment card industry). Compliance is not optional for enterprise customers " it is a prerequisite for doing business.

Data Protection

RequirementSOC 2GDPRImplementation
Encryption at restAES-256Art. 32Encrypt PII columns in DB; encrypt entire DB with TDE
Encryption in transitTLS 1.2+Art. 32HTTPS only; HSTS; mTLS for inter-service calls
Access controlCC6.1Art. 25RBAC for admin UI; separate admin roles for audit, user management
Audit loggingCC3.1Art. 30Immutable audit log; 1 year hot, 7 year archive
Data retentionCC3.2Art. 5(1)(e)Automated data cleanup; configurable per tenant
Data deletionCC6.2Art. 17 (Right to erasure)Soft delete + 30 day grace + permanent wipe
Breach notificationCC6.4Art. 33 (72 hours)Automated alerting pipeline; incident response plan
Data portabilityNot requiredArt. 20Export API (JSON/CSV) for user data
Consent managementNot requiredArt. 7Consent records; opt-in/opt-out; revocation tracking
Compliance Strategy: (1) Design for compliance from day one " retrofitting is expensive. (2) Document all security controls and processes. (3) Implement continuous compliance monitoring (drift detection, automated evidence collection). (4) Get SOC 2 Type II before selling to enterprises. (5) For EU customers, ensure GDPR-compliant data processing agreements (DPAs). (6) Consider dedicated EU data region for GDPR compliance. (7) Maintain a SOC (Security Operations Center) for 24/7 monitoring.

23. Monitoring & Alerting

Monitoring the auth system requires tracking metrics for availability, performance, security, and business health. Every metric should have an alert threshold and a runbook. The monitoring stack uses Prometheus + Grafana for metrics, Loki for logs, and PagerDuty for alerting.

CategoryMetricWarningCritical
AvailabilityAuth endpoint uptime< 99.99% in 5 min< 99.9% in 1 min
PerformanceP99 login latency> 500ms> 2s
PerformanceP99 token validation> 10ms> 50ms
SecurityLogin failure rate> 10%> 30%
SecurityAccount lockouts> 100/hour> 1000/hour
SecurityToken reuse events> 1/hour> 10/hour
BusinessActive logins/minuteDrop > 50%Drop > 90%
BusinessNew user registrationsDrop > 50%Zero for 30 min
InfraCPU on auth servers> 70%> 90%
InfraRedis memory usage> 70%> 85%
InfraDB connection pool> 70%> 90%
Dashboard: Create a Grafana dashboard with four rows: Availability (uptime, error rates, latency), Security (failed logins, lockouts, token reuse, suspicious IPs), Business (daily active users, registrations, SSO usage by provider), and Infrastructure (CPU, memory, DB connections, Redis cache hit ratio). Export dashboard JSON to version control.

24. API Design for Auth

The auth system exposes a RESTful API for token management, user management, client management, and tenant management. All endpoints use JWT bearer authentication for admin operations and OAuth 2.0 flows for application integration. The API follows OpenAPI 3.0 specifications with auto-generated client SDKs.

HTTP
# Authentication Endpoints
POST   /api/v1/auth/login                  # Email/password login
POST   /api/v1/auth/logout                 # End session (revoke refresh token)
POST   /api/v1/auth/refresh                # Refresh access token
POST   /api/v1/auth/mfa/verify             # Verify MFA code
POST   /api/v1/auth/mfa/enroll             # Enroll in MFA (TOTP/WebAuthn)
POST   /api/v1/auth/password/reset          # Request password reset
POST   /api/v1/auth/password/change         # Change password (authenticated)

# OAuth 2.0 / OIDC Endpoints
GET    /api/v1/oauth/authorize              # Authorization endpoint
POST   /api/v1/oauth/token                  # Token endpoint
GET    /api/v1/oauth/userinfo               # UserInfo endpoint
POST   /api/v1/oauth/revoke                 # Token revocation
GET    /.well-known/openid-configuration    # OIDC discovery
GET    /.well-known/jwks.json               # JWKS public keys

# User Management (Admin)
GET    /api/v1/users                        # List users
POST   /api/v1/users                        # Create user
GET    /api/v1/users/{id}                   # Get user
PATCH  /api/v1/users/{id}                   # Update user
DELETE /api/v1/users/{id}                   # Deactivate user
POST   /api/v1/users/{id}/roles             # Assign roles
DELETE /api/v1/users/{id}/roles/{role}       # Remove role

# Client Management (OAuth apps)
GET    /api/v1/clients                      # List OAuth clients
POST   /api/v1/clients                      # Register OAuth client
GET    /api/v1/clients/{id}                 # Get client details
PUT    /api/v1/clients/{id}                 # Update client
DELETE /api/v1/clients/{id}                 # Delete client
POST   /api/v1/clients/{id}/rotate-secret   # Generate new client secret

# Tenant Management (SSO)
GET    /api/v1/tenants/{id}/saml/metadata   # SAML metadata URL
PUT    /api/v1/tenants/{id}/saml/config     # Update SAML config
PUT    /api/v1/tenants/{id}/oidc/config     # Update OIDC config
POST   /api/v1/tenants/{id}/scim/test       # Test SCIM connection

# API Keys
GET    /api/v1/api-keys                     # List API keys
POST   /api/v1/api-keys                     # Create API key
DELETE /api/v1/api-keys/{id}                # Delete API key

Error Response Format

JSON
{
    "error": "invalid_grant",
    "error_description": "The authorization code has expired or was already used",
    "error_uri": "https://docs.example.com/errors/invalid_grant",
    "request_id": "req_abc123"
}
API Design Principles: (1) Use standard OAuth 2.0/OIDC error codes. (2) Always include request_id for debugging. (3) Paginate list endpoints with cursor-based pagination. (4) Version the API (v1, v2) for backward compatibility. (5) Return rate limit headers on all responses. (6) Support idempotency keys for POST endpoints. (7) Document all endpoints with OpenAPI 3.0 and provide a Postman collection.

25. Testing Strategy

Testing an auth system is particularly challenging because security vulnerabilities have severe consequences and because the system interacts with external IdPs, SMS gateways, email services, and hardware security modules. The testing strategy uses a test pyramid with unit tests, integration tests, contract tests, security tests, and end-to-end tests.

Testing Pyramid

LayerTest TypeCoverageTools
Unit (60%)Token generation, password hashing, rate limiting, JWT validation, TOTP generation, SAML parsingIndividual functions and classesxUnit, NUnit, FakeItEasy, Moq
Integration (25%)Database operations, Redis cache, OAuth flows, token exchange, MFA verificationInteractions between services and data storesTestcontainers (PostgreSQL, Redis), WireMock
Contract (10%)API responses match OpenAPI spec, SCIM responses match RFC, SAML responses valid XMLSchema compliancePact (consumer-driven contracts), SchemaSpy
Security (3%)SQL injection, XSS, CSRF, JWT alg=none, token tampering, rate limit bypassVulnerability detectionOWASP ZAP, SonarQube, custom fuzzing
E2E (2%)Full OAuth login flow, SAML SSO, SCIM provisioning, password reset, MFA enrollmentUser-facing scenariosPlaywright, Cypress, Postman/Newman

Security Test Cases

C#
public class SecurityTests
{
    [Fact]
    public void Jwt_WithAlgNone_ShouldBeRejected()
    {
        var token = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.";
        var handler = new JwtSecurityTokenHandler();
        Assert.Throws<SecurityTokenException>(() =>
            handler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true, RequireSignedTokens = true,
                IssuerSigningKey = new SymmetricSecurityKey("test"u8)
            }, out _));
    }

    [Fact]
    public void RateLimiter_ExceedsLimit_ShouldBlock()
    {
        var limiter = new SlidingWindowRateLimiter(_redis);
        var key = "test-user";
        for (int i = 0; i < 10; i++)
        {
            var result = limiter.CheckAsync(key, 10, TimeSpan.FromMinutes(1)).Result;
            if (i < 10) Assert.True(result.IsAllowed);
            else Assert.False(result.IsAllowed);
        }
    }
}
Testing Best Practices: (1) Use Testcontainers for PostgreSQL and Redis in integration tests. (2) Mock external IdPs with WireMock (serve SAML responses, OIDC discovery). (3) Parameterize password hash cost (use low costs in tests, high in production). (4) Test token expiration by controlling the system clock. (5) Run security tests as part of CI/CD pipeline. (6) Fuzz all API parameters. (7) Include performance tests (k6) for login, token refresh, and token validation endpoints.

26. Cost Estimation

Estimating the infrastructure cost for an auth system depends on user count, request volume, and compliance requirements. The following estimates assume a mid-tier deployment on AWS with 10 million users and 100K tenants, with SOC 2 compliance. Costs are monthly unless noted.

ComponentSpecificationMonthly Cost
Auth Server (EC2 / ECS)6 instances (c6i.4xlarge), across 3 AZs$3,600
PostgreSQL (RDS)db.r6g.4xlarge (3 nodes: primary + 2 replicas)$5,200
Redis (ElastiCache)cache.r6g.4xlarge (3 nodes: cluster mode)$2,400
Load Balancer (ALB)3 ALBs (public + internal + admin)$600
CDN / DDoS (CloudFront + WAF + Shield)100 TB/month$3,000
KMS / HSMKey management, HSM backing$500
S3 / GlacierAudit logs (7-year retention)$800
Monitoring (CloudWatch, Prometheus, Grafana)Metrics, logs, alarms$1,200
SIEM (Splunk / Datadog)100 GB/day ingest$4,000
Penetration TestingAnnual, external vendor$3,300/mo ($40K/yr)
SMS / Email (MFA codes)1 million SMS + 10 million emails$1,500
Incident Response Retainer24/7 SOC$8,000
Total$34,100/month

For a startup with 100K users, costs drop to approximately $5,000-8,000/month. For a hyper-scale system with 100M+ users, costs scale to $100K-300K/month depending on compliance requirements. The largest cost drivers are SIEM/licensing, SMS/email delivery, and compliance audits.

Cost Optimization: (1) Use reserved instances (30-40% savings on compute). (2) Keep access tokens stateless (no DB cost per validation). (3) Use CDN for static JWKS files. (4) Cache IdP metadata aggressively. (5) Use S3 Intelligent Tiering for audit logs. (6) Negotiate SMS volume pricing. (7) Consider multi-tenant RDS instances for non-prod environments.

27. Interview Q&A Deep Dive

This section covers the most common and challenging system design interview questions about authentication and authorization systems. Each question includes the expected depth of answer, key numbers to remember, and common pitfalls to avoid.

Q1: How would you design a system that handles 100K OAuth 2.0 token requests per second?

Answer: The token endpoint is the most performance-critical path. First, optimize the signing operation: use ES256 (ECDSA) instead of RS256 (RSA) " ECDSA signing is ~10x faster and produces smaller tokens. Second, avoid database reads on the hot path: validate client credentials from Redis cache (client_id → hashed_secret), with PostgreSQL as the source of truth. Third, use connection pooling and keep-alive for all upstream connections. Fourth, horizontally scale the auth server behind a load balancer " the token endpoint is stateless (it only needs the signing key and cache), so adding instances provides linear scaling. For 100K TPS, use 20 instances each handling 5K TPS, with a Redis cluster for cache and a PostgreSQL read replica for credential lookup. Benchmark: ES256 signing takes ~0.1ms; with network overhead, total request time should be < 5ms P99.

Q2: How would you implement Single Sign-Out (SLO) across multiple applications?

Answer: SLO is the hardest part of SSO. The problem: when a user logs out of App A, they should also be logged out of App B and App C (which are SSO-connected). For OIDC, implement RP-initiated logout using the OIDC session management spec: the auth server maintains a session registry. When the user logs out at the auth server (the OP), it redirects the user to each known RP's logout endpoint (front-channel) and simultaneously notifies RPs via the back-channel (HTTP POST with a logout token). For SAML, use the SingleLogout profile with either HTTP-Redirect (front-channel) or SOAP (back-channel). The practical reality: SLO is unreliable because Apps B and C may be offline. Instead, use short-lived sessions (15 minute access tokens, 1 hour session max) so that sessions expire quickly. This is called "soft logoff" and is the most common enterprise approach.

Q3: How do you handle the case where an authorization code is used twice?

Answer: The auth server must mark codes as single-use immediately upon first redemption. If the same code is used again, return an error ("invalid_grant: authorization code already used") and optionally revoke all tokens issued by that code. This is a critical OAuth security control " if an attacker intercepts an authorization code and uses it before the legitimate client, the attacker gets the tokens and the client gets the "already used" error. The client should detect this error and restart the authorization flow. PKCE mitigates this by requiring the code_verifier that only the legitimate client knows.

Q4: How would you design a passwordless authentication system?

Answer: Passwordless auth uses magic links (send a one-time link to email) or one-time codes (send a 6-digit code to email/phone) or WebAuthn (biometric/public key). For magic links: (1) User enters email, (2) Server generates a one-time token (JWT, 15 min TTL), (3) Server sends email with link containing token, (4) User clicks link, (5) Server validates token, creates session. For WebAuthn: (1) User registers a credential (public key + credential ID stored server-side), (2) On login, server sends a challenge, (3) User's device signs the challenge with the private key, (4) Server verifies the signature with the stored public key. WebAuthn is the most secure passwordless method " it is phishing-resistant and provides strong authentication without any shared secrets.

Q5: How do you handle the tradeoff between security and user experience in MFA?

Answer: Adaptive authentication (risk-based MFA) is the answer. Low-risk actions (viewing public data, logging in from a known device on the corporate network) require only a password. High-risk actions (admin operations, login from a new device in a new country) require step-up authentication (MFA). The risk score is computed from: device reputation (known vs unknown), location (geographic distance from last login), time (business hours vs 3 AM), behavior (typing speed, mouse movements, typical pages). This model provides strong security without friction for legitimate users. Implementation: send the risk score as a JWT claim; the API gateway checks the score and either allows, requires MFA, or blocks the request.

Q6: How would you handle credential stuffing attacks?

Answer: Credential stuffing (using leaked passwords from other sites) is the most common attack on auth systems. Mitigations: (1) Rate limit login attempts per IP (100/hour) and per user (10/hour after 3 failures). (2) CAPTCHA on repeated failures. (3) Check passwords against HIBP (Have I Been Pwned) API during registration and force password change if compromised. (4) Implement device fingerprinting " if a login attempt comes from a device that has never been seen for this user, require email verification. (5) Use bot detection (reCAPTCHA v3, fingerprint.js) to identify automated login attempts. (6) Notify users via email when their account is accessed from a new device or location.

Q7: Explain the difference between authentication and authorization in the context of microservices.

Answer: In microservices, authentication is centralized (API gateway validates the JWT, caches the JWKS response, and forwards the validated claims to downstream services). Authorization is decentralized " each service has its own policy engine that evaluates the claims against resource-specific rules. The API gateway should NOT make authorization decisions because it doesn't understand the resource semantics. For example, the gateway can verify that User A has a valid token (authentication), but it cannot know if User A can delete Document X (authorization). The document service performs that check. This pattern is called "auth at the edge, authz in the service."

Key Numbers to Remember

MetricValue
Access token lifetime15 minutes
Refresh token lifetime90 days (rotate each use)
Authorization code lifetime10 minutes (single use)
PKCE code verifier length64 bytes (random)
Argon2id time cost≥ 3 iterations
Argon2id memory cost≥ 64 MB
Account lockout threshold5 attempts / 15 minutes
Account lockout duration30 minutes
Session idle timeout (web)30 minutes
Session idle timeout (mobile)7 days
JWT signing algorithmES256 (ECDSA P-256)
JWKS rotation frequencyEvery 90 days
P99 token validation latency< 10ms (cached JWKS)
P99 login latency (no IdP)< 100ms
P99 login latency (with SSO)< 500ms
SAML assertion max age5 minutes
Auth server availability target99.99%
Monthly cost (mid-tier)~$34,100

Pre-Interview Checklist

  • Understand OAuth 2.0 flows (auth code, PKCE, client credentials, device) and their security properties
  • Know JWT structure (header, payload, signature) and validation (signature, exp, iss, aud)
  • Distinguish authentication from authorization clearly
  • Explain PKCE and why it prevents code interception
  • Understand SAML vs OIDC tradeoffs
  • Know password hashing (Argon2id > bcrypt > PBKDF2) and why memory-hard matters
  • Design a multi-tenant SSO system with tenant isolation
  • Explain refresh token rotation and token theft detection
  • Design rate limiting for a login endpoint
  • Understand WebAuthn and public-key authentication
  • Know RBAC vs ABAC tradeoffs
  • Discuss compliance (SOC 2, GDPR) and data protection requirements
  • Understand cost drivers for auth infrastructure

Auth System with OAuth 2.0 & SSO Senior+ Guide | Ayodhyya

Sandip Mhaske

I’m a software developer exploring the depths of .NET, AWS, Angular, React, and digital entrepreneurship. Here, I decode complex problems, share insightful solutions, and navigate the evolving landscape of tech and finance.

Post a Comment

Previous Post Next Post