Skip to content

Authentication Interview Questions & Answers

15 questions Updated 2026-06-23 Share:

ASP.NET Core authentication interview questions — cookie auth, ClaimsPrincipal, authentication schemes, custom handlers, and challenge vs forbid.

Read the in-depth guideAuthentication in ASP.NET Core(opens in new tab)
15 of 15

Authentication answers "who are you?" — it establishes the caller's identity. Authorization answers "what are you allowed to do?" — it checks whether the identified caller has permission to perform an action.

// Authentication — establish identity (runs first, via middleware):
app.UseAuthentication(); // populates HttpContext.User from cookie/token

// Authorization — check permissions (runs second):
app.UseAuthorization();  // evaluates [Authorize] attributes / policies

// Execution order matters — swapping them breaks auth entirely:
// app.UseAuthorization();  // HttpContext.User is still anonymous here
// app.UseAuthentication();

// What each middleware does:
// UseAuthentication: calls IAuthenticationService.AuthenticateAsync on each request
// → reads a cookie / Bearer token → populates HttpContext.User (ClaimsPrincipal)
// UseAuthorization:  evaluates [Authorize] attributes and policies against that User
// → 401 (unauthenticated) or 403 (authenticated but forbidden) if checks fail

[Authorize]           // requires authentication (401 if anonymous)
[Authorize(Roles = "Admin")] // requires authentication + role (403 if wrong role)
public class AdminController : ControllerBase { }

In ASP.NET Core, these are separate middleware and separate concerns. A request can be authenticated (identity known) but not authorized (insufficient permissions), which produces HTTP 403, not 401.

Rule of thumb: UseAuthentication before UseAuthorization, always. Authentication establishes identity; authorization uses it.

Authentication is added in two steps: register services with AddAuthentication and wire the middleware with UseAuthentication.

// Program.cs — cookie authentication (simplest scheme):
builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath        = "/account/login";   // redirect here when 401
        options.AccessDeniedPath = "/account/denied";  // redirect here when 403
        options.ExpireTimeSpan   = TimeSpan.FromHours(8);
        options.SlidingExpiration = true; // reset expiry on each request
    });

var app = builder.Build();

// Middleware pipeline (order is critical):
app.UseRouting();
app.UseAuthentication(); // populate HttpContext.User
app.UseAuthorization();  // check permissions against that user

// Multiple schemes — default + JWT bearer:
builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme    = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddJwtBearer(options =>
    {
        options.Authority = "https://auth.example.com";
        options.Audience  = "api1";
    });

Rule of thumb: Always call AddAuthentication before AddAuthorization, and always place UseAuthentication before UseAuthorization in the pipeline. Missing either call silently leaves every request as anonymous.

Claims-based identity represents a user as a set of name-value assertions (claims) rather than a binary "logged in or not". A ClaimsPrincipal contains one or more ClaimsIdentity objects, each holding a list of Claim instances.

// The three-layer model:
// Claim         — one assertion: (type, value, issuer)
// ClaimsIdentity — a collection of claims + the authentication scheme that created them
// ClaimsPrincipal — one or more identities (e.g., Windows + cookie at the same time)

// Reading claims from HttpContext.User (a ClaimsPrincipal):
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); // "42"
var email  = User.FindFirstValue(ClaimTypes.Email);          // "alice@example.com"
var roles  = User.FindAll(ClaimTypes.Role)
                 .Select(c => c.Value)
                 .ToList();                                   // ["Admin", "Editor"]

// Boolean helpers:
bool isAuth    = User.Identity?.IsAuthenticated ?? false;
bool isAdmin   = User.IsInRole("Admin");    // checks ClaimTypes.Role claims
string? name   = User.Identity?.Name;       // value of ClaimTypes.Name claim

// Well-known claim types (from System.Security.Claims):
// ClaimTypes.NameIdentifier — user's unique ID
// ClaimTypes.Email          — email address
// ClaimTypes.Name           — display name
// ClaimTypes.Role           — role membership
// ClaimTypes.DateOfBirth    — etc.

// Custom claims — any string key is valid:
var tenantId = User.FindFirstValue("tenant_id"); // app-specific claim

Rule of thumb: Prefer ClaimTypes.* constants over raw strings for standard claims so your code stays compatible with token issuers that use the long URI forms. Add custom claims sparingly — keep sensitive data out of cookies/tokens.

An authentication scheme is a named configuration of an authentication handler — the combination of a handler type and its options. Multiple schemes can coexist; the default scheme is used when no scheme is explicitly specified.

builder.Services
    .AddAuthentication(options =>
    {
        // Default scheme for all operations unless overridden:
        options.DefaultScheme            = CookieAuthenticationDefaults.AuthenticationScheme;
        // Override per operation when you need different behavior:
        options.DefaultChallengeScheme   = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) // "Cookies"
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme)         // "Bearer"
    .AddGoogle("Google", options =>                               // custom name
    {
        options.ClientId     = config["Auth:Google:ClientId"];
        options.ClientSecret = config["Auth:Google:ClientSecret"];
    });

// Select a specific scheme on a controller or endpoint:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
public class ApiController : ControllerBase { }

// Or use a policy that specifies the scheme:
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ApiOnly", policy =>
        policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
              .RequireAuthenticatedUser());

Rule of thumb: Use the default scheme for the majority of your app. Only specify per-endpoint schemes for areas that genuinely use a different auth mechanism (e.g., API endpoints using JWT while the admin UI uses cookies).

Implement AuthenticationHandler<TOptions> and register it with AddScheme<>. The framework calls HandleAuthenticateAsync on every request matching the scheme.

// Options — scheme configuration:
public class ApiKeyAuthOptions : AuthenticationSchemeOptions
{
    public string HeaderName { get; set; } = "X-Api-Key";
}

// Handler — core logic:
public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOptions>
{
    private readonly IApiKeyStore _store;

    public ApiKeyAuthHandler(
        IOptionsMonitor<ApiKeyAuthOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        IApiKeyStore store)
        : base(options, logger, encoder)
    {
        _store = store;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue(Options.HeaderName, out var keyValues))
            return AuthenticateResult.NoResult(); // scheme not applicable

        var apiKey = keyValues.FirstOrDefault();
        var client = await _store.FindByKeyAsync(apiKey);

        if (client is null)
            return AuthenticateResult.Fail("Invalid API key");

        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, client.ClientId),
            new Claim(ClaimTypes.Name,           client.Name),
        };
        var identity  = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket    = new AuthenticationTicket(principal, Scheme.Name);

        return AuthenticateResult.Success(ticket);
    }

    // Challenge = 401 response when unauthenticated:
    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.StatusCode  = 401;
        Response.Headers.WWWAuthenticate = $"ApiKey realm=\"api\", header=\"{Options.HeaderName}\"";
        return Task.CompletedTask;
    }
}

// Register the scheme:
builder.Services
    .AddAuthentication()
    .AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>("ApiKey", options => { });

Rule of thumb: AuthenticateResult.NoResult() means "this scheme doesn't apply"; AuthenticateResult.Fail() means "this scheme applies but the credential is invalid". The distinction matters when multiple schemes are evaluated in order.

SignInAsync calls the authentication scheme's handler to persist the principal (writing a cookie, issuing a token, etc.). SignOutAsync calls the handler to remove that persistence (clearing the cookie, revoking the session).

// SignInAsync — persists the ClaimsPrincipal using the specified scheme:
await HttpContext.SignInAsync(
    scheme: CookieAuthenticationDefaults.AuthenticationScheme,
    principal: new ClaimsPrincipal(identity),
    properties: new AuthenticationProperties
    {
        IsPersistent  = true,                          // survive browser close
        ExpiresUtc    = DateTimeOffset.UtcNow.AddDays(30),
        RedirectUri   = "/dashboard",                  // post-login redirect
        IssuedUtc     = DateTimeOffset.UtcNow,
        AllowRefresh  = true,
    });
// For cookies: sets an encrypted Set-Cookie header.
// For JWT: you would generate and return a token yourself — SignInAsync isn't used.

// SignOutAsync — removes the session for the specified scheme:
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// For cookies: sets an expired Set-Cookie to clear the client cookie.

// AuthenticateAsync — reads/validates the current request's credential:
var result = await HttpContext.AuthenticateAsync();
if (result.Succeeded)
{
    var user = result.Principal; // ClaimsPrincipal
}
// This is what UseAuthentication middleware calls automatically each request.

// ChallengeAsync — trigger 401 + redirect to login:
await HttpContext.ChallengeAsync();

// ForbidAsync — trigger 403 (authenticated but not allowed):
await HttpContext.ForbidAsync();

Rule of thumb: SignInAsync and SignOutAsync are handler-specific. For cookie auth they write/clear cookies. For JWT-based APIs, you generate tokens manually — there's no SignInAsync step on the server.

Challenge (HTTP 401) means the request is unauthenticated — the caller's identity is unknown. Forbid (HTTP 403) means the request is authenticated but the caller lacks the required permission.

// The framework calls these automatically for [Authorize] failures:
// Anonymous user → 401 Challenge (redirect to login for cookies, WWW-Authenticate for API)
// Authenticated user without required role → 403 Forbid

// You can also trigger them manually in controllers:
public IActionResult GetAdminData()
{
    if (!User.Identity!.IsAuthenticated)
        return Challenge(); // 401 — please log in

    if (!User.IsInRole("Admin"))
        return Forbid();    // 403 — you're logged in but not an admin

    return Ok(_adminService.GetData());
}

// IActionResult vs HttpContext extension:
return Challenge(JwtBearerDefaults.AuthenticationScheme); // use specific scheme
await HttpContext.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme);

// Cookie auth behavior:
// Challenge → 302 redirect to options.LoginPath (e.g., /account/login)
// Forbid    → 302 redirect to options.AccessDeniedPath (e.g., /account/denied)

// JWT bearer auth behavior:
// Challenge → 401 with WWW-Authenticate: Bearer header
// Forbid    → 403 with no redirect

// Custom override — if default scheme behavior doesn't suit you:
builder.Services.AddAuthentication()
    .AddCookie(options =>
    {
        options.Events.OnRedirectToLogin        = ctx => { ctx.Response.StatusCode = 401; return Task.CompletedTask; };
        options.Events.OnRedirectToAccessDenied = ctx => { ctx.Response.StatusCode = 403; return Task.CompletedTask; };
    });
// Useful for cookie auth serving an API — return status codes instead of redirects.

Rule of thumb: 401 means "log in"; 403 means "you're logged in but you can't do that". The distinction matters for clients — a browser follows the 401 redirect; an API client should not.

HttpContext.User is populated by the UseAuthentication middleware early in the pipeline. It calls IAuthenticationService.AuthenticateAsync using the default scheme, which reads the credential (cookie/token) from the request and returns a ClaimsPrincipal.

// Execution flow for every request:
// 1. UseAuthentication middleware fires
// 2. Calls AuthenticationService.AuthenticateAsync(defaultScheme)
// 3. Handler reads the credential (e.g., decrypts cookie, validates JWT)
// 4. Returns AuthenticateResult.Success(ticket) if valid
// 5. Middleware sets: HttpContext.User = ticket.Principal
// 6. If no valid credential: HttpContext.User = new ClaimsPrincipal() (anonymous)

// If UseAuthentication is missing, User is always anonymous:
// HttpContext.User.Identity.IsAuthenticated → false
// HttpContext.User.Identity.Name           → null

// Accessing User in a controller or Razor Page:
public class OrdersController : ControllerBase
{
    public IActionResult GetMyOrders()
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        // User is always populated (may be anonymous) — never null
    }
}

// Accessing User in minimal APIs:
app.MapGet("/orders", (ClaimsPrincipal user) =>
{
    var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
    return userId is null ? Results.Unauthorized() : Results.Ok();
});

// Accessing User outside of request context (e.g., a service):
// Inject IHttpContextAccessor and access .HttpContext?.User
public class CurrentUserService
{
    private readonly IHttpContextAccessor _accessor;
    public CurrentUserService(IHttpContextAccessor accessor) => _accessor = accessor;

    public string? UserId => _accessor.HttpContext?.User
        .FindFirstValue(ClaimTypes.NameIdentifier);
}
// Register: builder.Services.AddHttpContextAccessor();

Rule of thumb: HttpContext.User is always a ClaimsPrincipal — never null. Check IsAuthenticated before reading claims. Avoid IHttpContextAccessor in domain services; pass the user ID as a parameter instead.

Windows Authentication uses the OS-level Kerberos/NTLM protocol to authenticate users against Active Directory. The browser negotiates credentials automatically on corporate networks — no login form needed.

// Program.cs — enable Windows auth (Kestrel or IIS):
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
    .AddNegotiate();

builder.Services.AddAuthorization();

// launchSettings.json / appsettings (for Kestrel):
// "windowsAuthentication": true, "anonymousAuthentication": false

// IIS: enable "Windows Authentication" in the IIS Authentication feature panel.
// Kestrel: install Microsoft.AspNetCore.Authentication.Negotiate NuGet package.

// Reading Windows identity claims:
[Authorize]
public class ReportsController : ControllerBase
{
    public IActionResult GetReport()
    {
        var windowsUser = User.Identity?.Name;         // "DOMAIN\\alice"
        var isAdmin     = User.IsInRole("DOMAIN\\AdminGroup"); // AD group check
        return Ok(windowsUser);
    }
}

// When to use it:
// Internal corporate line-of-business apps (intranet)
// Users are on the same AD domain as the server
// No-login-form requirement (seamless SSO experience)
// Public-facing apps (users aren't on the domain)
// Mobile/API clients (no browser to negotiate NTLM/Kerberos)
// Docker/Linux hosts (limited Kerberos support without workarounds)

Rule of thumb: Windows Authentication is the right default for internal enterprise apps where all users are on the corporate domain. For public apps or REST APIs, use cookie auth or JWT instead.

ASP.NET Core has built-in OAuth 2.0 and OpenID Connect handlers. Each provider is registered as a named scheme; the framework handles the redirect dance, token exchange, and claim mapping automatically.

// Add NuGet: Microsoft.AspNetCore.Authentication.Google
builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie()         // cookie to persist the identity after OAuth completes
    .AddGoogle(options =>
    {
        options.ClientId     = config["Auth:Google:ClientId"];
        options.ClientSecret = config["Auth:Google:ClientSecret"];
        // Map Google profile claims to standard ClaimTypes:
        options.ClaimActions.MapJsonKey(ClaimTypes.Email,      "email");
        options.ClaimActions.MapJsonKey(ClaimTypes.GivenName,  "given_name");
        options.ClaimActions.MapJsonKey("picture",             "picture");
        // Scopes to request:
        options.Scope.Add("profile");
        options.Scope.Add("email");
    })
    .AddMicrosoftAccount(options =>
    {
        options.ClientId     = config["Auth:Microsoft:ClientId"];
        options.ClientSecret = config["Auth:Microsoft:ClientSecret"];
    })
    .AddOpenIdConnect("AzureAD", options =>
    {
        options.Authority    = $"https://login.microsoftonline.com/{tenantId}";
        options.ClientId     = config["Auth:AzureAD:ClientId"];
        options.ClientSecret = config["Auth:AzureAD:ClientSecret"];
        options.ResponseType = OpenIdConnectResponseType.Code;
    });

// Trigger login redirect from an endpoint:
[HttpGet("login/google")]
public IActionResult LoginGoogle()
    => Challenge(new AuthenticationProperties { RedirectUri = "/" }, "Google");

Rule of thumb: Always pair external OAuth with a local cookie scheme. The OAuth scheme handles the redirect; the cookie scheme persists the resulting identity so subsequent requests don't re-trigger the OAuth flow.

Data Protection is ASP.NET Core's key management and cryptographic API. Authentication cookies, antiforgery tokens, and the IDataProtector API all use it under the hood.

// Data Protection encrypts the cookie payload — the default is automatic:
// No explicit configuration needed for a single machine.

// For multi-machine deployments or persistence across restarts,
// configure a shared key store:

// Option 1 — persist keys to the file system (e.g., a shared network path):
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo("/keys"))
    .SetApplicationName("MyApp");       // required when multiple apps share the path

// Option 2 — persist keys to Azure Blob + protect with Azure Key Vault:
builder.Services.AddDataProtection()
    .PersistKeysToAzureBlobStorage(blobClient)
    .ProtectKeysWithAzureKeyVault(keyVaultClient, keyIdentifier);

// Option 3 — persist to Redis (for containerized apps):
builder.Services.AddDataProtection()
    .PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys");

// Direct use — encrypt/decrypt arbitrary payloads:
var protector = dataProtectionProvider.CreateProtector("my-purpose");
string ciphertext = protector.Protect("sensitive-value");
string plaintext  = protector.Unprotect(ciphertext);

// Key rotation — old keys are kept for decryption but new keys are used for encryption:
// Existing cookies remain valid; new cookies use the new key automatically.

Rule of thumb: In production, always configure a shared, persistent key store for Data Protection. Without it, every app restart or scale-out node generates new keys — invalidating all existing sessions and antiforgery tokens.

Minimal API endpoints are protected with RequireAuthorization() or the [Authorize] attribute (when applied to the endpoint group). AllowAnonymous() overrides on specific routes.

var app = builder.Build();

// Protect a single endpoint:
app.MapGet("/profile", (ClaimsPrincipal user) =>
{
    return Results.Ok(new { Name = user.Identity!.Name });
}).RequireAuthorization();

// Protect a group of endpoints and open one:
var api = app.MapGroup("/api").RequireAuthorization();

api.MapGet("/orders",   (IOrderService svc) => svc.GetAll());    // protected
api.MapPost("/orders",  (CreateOrderDto dto, IOrderService svc) => svc.Create(dto)); // protected
api.MapGet("/health",   ()  => Results.Ok()).AllowAnonymous();   // open

// Apply a named policy:
app.MapDelete("/orders/{id:int}", async (int id, IOrderService svc) =>
{
    await svc.DeleteAsync(id);
    return Results.NoContent();
}).RequireAuthorization("AdminOnly");

// Access the current user — inject ClaimsPrincipal directly:
app.MapGet("/me", (ClaimsPrincipal user) =>
    Results.Ok(new
    {
        Id    = user.FindFirstValue(ClaimTypes.NameIdentifier),
        Email = user.FindFirstValue(ClaimTypes.Email),
    })
).RequireAuthorization();

Rule of thumb: Group related endpoints with MapGroup and call RequireAuthorization once on the group. Only use .AllowAnonymous() on individual endpoints that explicitly need to be public.

OAuth 2.0 is an authorization framework — it grants access tokens so an app can act on a user's behalf. OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that adds an ID token containing the user's identity claims, enabling true authentication.

// OAuth 2.0 alone — grants an access token; tells you "what" but not "who":
// Access token: opaque credential to call an API on the user's behalf.
// You have to call the /userinfo endpoint separately to get identity.

// OIDC — adds an ID token (a JWT) carrying the user's identity:
// ID token: { sub, name, email, iat, exp, iss, aud } — signed by the identity provider.
// Access token: still issued alongside for API calls.
// Refresh token: optional, for long-lived sessions.

// Configure OIDC in ASP.NET Core (works with any OIDC provider):
builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme          = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.Authority     = "https://login.microsoftonline.com/{tenant-id}/v2.0";
        options.ClientId      = config["AzureAd:ClientId"];
        options.ClientSecret  = config["AzureAd:ClientSecret"];
        options.ResponseType  = OpenIdConnectResponseType.Code; // authorization code flow
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("email");
        options.SaveTokens    = true; // persist access + refresh tokens in the cookie
        options.CallbackPath  = "/signin-oidc"; // redirect URI registered with the provider
        // Map claims from the ID token to ClaimTypes:
        options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
        options.ClaimActions.MapJsonKey(ClaimTypes.Name,  "name");
    });

// After the flow completes, HttpContext.User is populated from the ID token.
// Access the saved access token later:
var accessToken = await HttpContext.GetTokenAsync("access_token");

Rule of thumb: Use OAuth 2.0 when you need delegated API access. Use OIDC when you need to know who the user is — it eliminates the need for a separate /userinfo call and is the standard for federated login (Google, Microsoft, Okta, Auth0).

ASP.NET Core Identity ships with built-in TOTP (time-based one-time password) 2FA. Enable it in Identity options, and the user must provide a TOTP code from an authenticator app after their password.

// Register Identity with 2FA token provider:
builder.Services
    .AddIdentity<ApplicationUser, IdentityRole>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true; // require email confirmation
    })
    .AddEntityFrameworkStores<AppDbContext>()
    .AddDefaultTokenProviders(); // includes TOTP provider

// Step 1 — enable 2FA for a user (generate QR code URI):
[HttpPost("enable-2fa")]
[Authorize]
public async Task<IActionResult> Enable2Fa()
{
    var user = await _userManager.GetUserAsync(User);
    if (user is null) return NotFound();

    // Get the authenticator key (generate one if not set):
    var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
    if (string.IsNullOrEmpty(unformattedKey))
    {
        await _userManager.ResetAuthenticatorKeyAsync(user);
        unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
    }

    // URI format for QR code scannable by Google Authenticator / Authy:
    var email    = await _userManager.GetEmailAsync(user);
    var qrUri    = $"otpauth://totp/MyApp:{email}?secret={unformattedKey}&issuer=MyApp";
    return Ok(new { qrUri, manualKey = unformattedKey });
}

// Step 2 — verify and enable (user enters code from authenticator app):
[HttpPost("verify-2fa")]
[Authorize]
public async Task<IActionResult> Verify2Fa([FromBody] TwoFactorDto dto)
{
    var user    = await _userManager.GetUserAsync(User);
    var isValid = await _userManager.VerifyTwoFactorTokenAsync(
        user!, _userManager.Options.Tokens.AuthenticatorTokenProvider, dto.Code);

    if (!isValid) return BadRequest("Invalid code");

    await _userManager.SetTwoFactorEnabledAsync(user!, true);
    return Ok("2FA enabled");
}

// Step 3 — login flow with 2FA:
// After password check, if user.TwoFactorEnabled:
// await _signInManager.PasswordSignInAsync → TwoFactorRequired result
// Prompt user for TOTP code, then:
await _signInManager.TwoFactorAuthenticatorSignInAsync(code, isPersistent: false, rememberClient: false);

Rule of thumb: Always generate new authenticator keys with ResetAuthenticatorKeyAsync when the user explicitly re-enrolls (not on first setup). Offer recovery codes so users can regain access if they lose their authenticator device.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel