Overview
Security is critical in modern applications. Understanding authentication and authorization deeply is essential for building secure APIs and is frequently tested in senior-level interviews.
Core Concepts
Authentication Schemes
// Multiple authentication schemes
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Combined";
options.DefaultChallengeScheme = "Combined";
})
.AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
})
.AddCookie("Cookie", options =>
{
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromDays(14);
})
.AddPolicyScheme("Combined", "Bearer or Cookie", options =>
{
options.ForwardDefaultSelector = context =>
{
var authHeader = context.Request.Headers["Authorization"].ToString();
if (authHeader?.StartsWith("Bearer ") == true)
return "Bearer";
return "Cookie";
};
});
Claims-Based Authorization
// Login - create claims
public async Task<IActionResult> Login(LoginRequest request)
{
var user = await _userService.ValidateAsync(request.Username, request.Password);
if (user == null)
return Unauthorized();
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.Username),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Role, user.Role),
new("Department", user.Department),
new("EmployeeId", user.EmployeeId)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: credentials
);
return Ok(new
{
Token = new JwtSecurityTokenHandler().WriteToken(token),
Expiration = token.ValidTo
});
}
// Access claims in controller
[Authorize]
public IActionResult GetUserInfo()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var username = User.Identity.Name;
var roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value);
var department = User.FindFirst("Department")?.Value;
return Ok(new { userId, username, roles, department });
}
Policy-Based Authorization
// Configure policies
builder.Services.AddAuthorization(options =>
{
// Simple policy
options.AddPolicy("RequireAdmin", policy =>
policy.RequireRole("Admin"));
// Claims-based policy
options.AddPolicy("RequireHR", policy =>
policy.RequireClaim("Department", "HR"));
// Multiple requirements
options.AddPolicy("CanDeleteUsers", policy =>
policy.RequireRole("Admin", "SuperAdmin")
.RequireClaim("Permission", "DeleteUsers"));
// Custom requirement
options.AddPolicy("CanApproveExpenses", policy =>
policy.Requirements.Add(new ExpenseApprovalRequirement(5000)));
// Age requirement
options.AddPolicy("Over18", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
// Custom authorization requirement
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
// Custom authorization handler
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
if (dateOfBirthClaim == null)
return Task.CompletedTask;
var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
var age = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth.Date > DateTime.Today.AddYears(-age))
age--;
if (age >= requirement.MinimumAge)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
// Register handler
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
// Use policy
[Authorize(Policy = "Over18")]
[HttpGet("adult-content")]
public IActionResult GetAdultContent()
{
return Ok("Adult content");
}
Resource-Based Authorization
// Authorization service for resource-based checks
public interface IAuthorizationService
{
Task<AuthorizationResult> AuthorizeAsync(
ClaimsPrincipal user,
object resource,
IEnumerable<IAuthorizationRequirement> requirements);
Task<AuthorizationResult> AuthorizeAsync(
ClaimsPrincipal user,
object resource,
string policyName);
}
// Requirement
public class DocumentEditRequirement : IAuthorizationRequirement { }
// Handler
public class DocumentAuthorizationHandler
: AuthorizationHandler<DocumentEditRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DocumentEditRequirement requirement,
Document resource)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// User can edit if they own it or are admin
if (resource.OwnerId == userId || context.User.IsInRole("Admin"))
context.Succeed(requirement);
return Task.CompletedTask;
}
}
// Usage in controller
[Authorize]
[HttpPut("documents/{id}")]
public async Task<IActionResult> UpdateDocument(
int id,
UpdateDocumentRequest request)
{
var document = await _repository.GetByIdAsync(id);
if (document == null)
return NotFound();
var authResult = await _authorizationService.AuthorizeAsync(
User,
document,
new[] { new DocumentEditRequirement() });
if (!authResult.Succeeded)
return Forbid();
// Update document
return NoContent();
}
Bad vs Good Examples
Bad: Hardcoded Secrets
// Bad - secret in code
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("my-super-secret-key-12345"));
Good: Configuration-Based Secrets
// Good - secret in configuration/secrets manager
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]));
// Even better - Azure Key Vault
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{keyVaultName}.vault.azure.net/"),
new DefaultAzureCredential());
Bad: Role Checks in Business Logic
// Bad - authorization logic in business layer
public class UserService
{
public async Task DeleteUserAsync(int id, ClaimsPrincipal currentUser)
{
if (!currentUser.IsInRole("Admin")) // Bad!
throw new UnauthorizedAccessException();
await _repository.DeleteAsync(id);
}
}
Good: Authorization at API Layer
// Good - authorization in controller/middleware
[Authorize(Roles = "Admin")]
[HttpDelete("users/{id}")]
public async Task<IActionResult> DeleteUser(int id)
{
await _userService.DeleteUserAsync(id); // Business logic is clean
return NoContent();
}
Interview Tips
Tip 1: Explain the difference between authentication (who you are) and authorization (what you can do) with concrete examples.
Tip 2: Know JWT structure (header.payload.signature) and why it's stateless. Be ready to discuss token expiration and refresh tokens.
Tip 3: Understand the middleware order: Authentication must come before Authorization.
Common Interview Questions
-
What's the difference between authentication and authorization?
- Authentication verifies identity (login). Authorization verifies permissions (can this user access this resource).
-
How does JWT authentication work?
- Server generates token with claims, signs it with secret. Client sends token in Authorization header. Server validates signature and claims.
-
What are claims and why use them?
- Key-value pairs about the user. Flexible, can include any data (role, department, permissions). Embedded in token, no database lookup needed.
-
What's the difference between roles and policies?
- Roles: simple string-based checks. Policies: flexible rules combining roles, claims, custom logic. Policies are more powerful and recommended.
-
How do you handle token expiration?
- Short-lived access tokens (15-60 min) + long-lived refresh tokens. When access token expires, use refresh token to get new access token.
-
What's the correct middleware order for authentication/authorization?
- UseAuthentication() before UseAuthorization() before endpoints. Routing should be before both.
-
How do you implement resource-based authorization?
- Use IAuthorizationService with custom requirements and handlers that check resource properties against user claims.