Authentication & Authorization in ASP.NET Core

Master authentication and authorization patterns, JWT, OAuth 2.0, and role-based access control in ASP.NET Core applications

Security
Authentication
Authorization
JWT
ASP.NET Core
20 min read

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

  1. What's the difference between authentication and authorization?

    • Authentication verifies identity (login). Authorization verifies permissions (can this user access this resource).
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. What's the correct middleware order for authentication/authorization?

    • UseAuthentication() before UseAuthorization() before endpoints. Routing should be before both.
  7. How do you implement resource-based authorization?

    • Use IAuthorizationService with custom requirements and handlers that check resource properties against user claims.