EngineeringSenior DeveloperTechnical Sessions

Session 06: Security & Identity in .NET

Mục tiêu: Hiểu sâu authentication/authorization flows, implement secure APIs đúng chuẩn.


1. ASP.NET Core Identity Internals

Architecture

┌─────────────────────────────────────────────┐
│              ASP.NET Core Identity           │
├──────────────┬──────────────┬───────────────┤
│  UserManager │ SignInManager │ RoleManager   │
├──────────────┴──────────────┴───────────────┤
│              Identity Stores                 │
│  ┌──────────┐ ┌────────────┐ ┌────────────┐│
│  │UserStore │ │ RoleStore  │ │ClaimStore  ││
│  └──────────┘ └────────────┘ └────────────┘│
├─────────────────────────────────────────────┤
│         EF Core / Custom Store               │
└─────────────────────────────────────────────┘

Custom Identity Configuration

csharp
builder.Services.AddIdentity<AppUser, AppRole>(options =>
{
    // Password rules
    options.Password.RequiredLength = 12;
    options.Password.RequireDigit = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredUniqueChars = 4;

    // Lockout
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    options.Lockout.AllowedForNewUsers = true;

    // User
    options.User.RequireUniqueEmail = true;

    // SignIn
    options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();

// Extend IdentityUser
public class AppUser : IdentityUser<Guid>
{
    public string FullName { get; set; } = null!;
    public string? AvatarUrl { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? LastLoginAt { get; set; }
    public bool IsActive { get; set; } = true;
}

2. JWT Authentication Deep Dive

JWT Structure

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
│                      │                              │
└── Header             └── Payload                    └── Signature
    (algorithm)            (claims)                       (HMAC/RSA)

JWT Implementation

csharp
// 1. Configuration
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidateAudience = true,
        ValidAudience = builder.Configuration["Jwt:Audience"],
        ValidateLifetime = true,
        ClockSkew = TimeSpan.FromSeconds(30),    // Giảm mặc định 5 phút
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!))
    };

    // SignalR: truyền token qua query string
    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];
            var path = context.HttpContext.Request.Path;
            if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
            {
                context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };
});

// 2. Token Generation Service
public class TokenService(IOptions<JwtOptions> options, TimeProvider timeProvider)
{
    public TokenPair GenerateTokens(AppUser user, IList<string> roles)
    {
        var config = options.Value;
        var now = timeProvider.GetUtcNow();

        // Access Token (short-lived: 15 min)
        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new(JwtRegisteredClaimNames.Email, user.Email!),
            new("fullName", user.FullName),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(),
                ClaimValueTypes.Integer64)
        };
        claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.Secret));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var accessToken = new JwtSecurityToken(
            issuer: config.Issuer,
            audience: config.Audience,
            claims: claims,
            notBefore: now.UtcDateTime,
            expires: now.AddMinutes(15).UtcDateTime,
            signingCredentials: credentials);

        // Refresh Token (long-lived: 7 days)
        var refreshToken = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));

        return new TokenPair(
            AccessToken: new JwtSecurityTokenHandler().WriteToken(accessToken),
            RefreshToken: refreshToken,
            ExpiresAt: now.AddMinutes(15));
    }
}

// 3. Refresh Token Flow
public async Task<TokenPair> RefreshAsync(string refreshToken)
{
    var storedToken = await _db.RefreshTokens
        .Include(t => t.User)
        .FirstOrDefaultAsync(t => t.Token == refreshToken);

    if (storedToken is null || storedToken.ExpiresAt < _timeProvider.GetUtcNow())
        throw new UnauthorizedException("Invalid refresh token");

    // Rotation: invalidate old token, issue new pair
    storedToken.IsRevoked = true;
    storedToken.RevokedAt = _timeProvider.GetUtcNow();

    var user = storedToken.User;
    var roles = await _userManager.GetRolesAsync(user);
    var newTokens = _tokenService.GenerateTokens(user, roles);

    // Save new refresh token
    _db.RefreshTokens.Add(new RefreshToken
    {
        Token = newTokens.RefreshToken,
        UserId = user.Id,
        ExpiresAt = _timeProvider.GetUtcNow().AddDays(7),
        CreatedByIp = _httpContext.Connection.RemoteIpAddress?.ToString()
    });

    await _db.SaveChangesAsync();
    return newTokens;
}
Tiêu chíJWT (Bearer Token)Cookie
StorageClient (localStorage/memory)Browser auto-manage
CSRFKhông bị CSRFCần anti-forgery token
XSSBị XSS nếu localStorageHttpOnly cookie an toàn
Mobile✅ Native support❌ Phức tạp
Microservices✅ Stateless, dễ share❌ Sticky sessions
SSR (Next.js)⚠️ Cần truyền manual✅ Tự động gửi
Revocation❌ Khó (cần blacklist)✅ Server-side session
🎯 Best Practice:
- SPA + API → JWT (store trong memory, KHÔNG localStorage)
- SSR (Next.js) → Cookie (HttpOnly, Secure, SameSite=Strict)
- Mobile → JWT
- Monolith MVC → Cookie

3. OAuth 2.0 / OpenID Connect

Flows

┌──────────────────────────────────────────────────────────┐
│         Authorization Code + PKCE Flow (Recommended)      │
│                                                           │
│  User → App → Authorization Server (login page)          │
│                    │                                      │
│                    ▼                                      │
│  Authorization Server → App (authorization code)         │
│                    │                                      │
│                    ▼ (Backend exchange: code → tokens)    │
│  App Backend → Authorization Server                      │
│     POST /token                                          │
│     grant_type=authorization_code                        │
│     code=xxx                                             │
│     code_verifier=yyy   ← PKCE: proves same client       │
│                    │                                      │
│                    ▼                                      │
│  Authorization Server → App (access_token + id_token)    │
└──────────────────────────────────────────────────────────┘

Implementation với External Providers

csharp
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = "Google";
})
.AddCookie()
.AddGoogle("Google", options =>
{
    options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
    options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
    options.Scope.Add("profile");
    options.Scope.Add("email");

    // Map external claims → application claims
    options.ClaimActions.MapJsonKey("avatar", "picture");
    options.ClaimActions.MapJsonKey("locale", "locale");

    options.Events.OnCreatingTicket = async context =>
    {
        // Tạo/cập nhật user trong DB khi login lần đầu
        var email = context.Identity?.FindFirst(ClaimTypes.Email)?.Value;
        var userService = context.HttpContext.RequestServices
            .GetRequiredService<IUserService>();
        await userService.EnsureUserExistsAsync(email!);
    };
})
.AddMicrosoftAccount("Microsoft", options =>
{
    options.ClientId = builder.Configuration["Auth:Microsoft:ClientId"]!;
    options.ClientSecret = builder.Configuration["Auth:Microsoft:ClientSecret"]!;
});

4. Authorization Deep Dive

Policy-Based Authorization

csharp
// Policy definitions
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"))
    .AddPolicy("CanManageOrders", policy => policy
        .RequireAuthenticatedUser()
        .RequireRole("Admin", "OrderManager")
        .RequireClaim("department", "sales", "operations"))
    .AddPolicy("MinAge18", policy => policy
        .AddRequirements(new MinimumAgeRequirement(18)))
    .AddPolicy("SameCompany", policy => policy
        .AddRequirements(new SameCompanyRequirement()));

// Custom Authorization Requirement + Handler
public class MinimumAgeRequirement(int age) : IAuthorizationRequirement
{
    public int MinimumAge { get; } = age;
}

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var birthDateClaim = context.User.FindFirst("birthdate")?.Value;
        if (birthDateClaim is null) return Task.CompletedTask;

        var birthDate = DateOnly.Parse(birthDateClaim);
        var age = DateOnly.FromDateTime(DateTime.Today).Year - birthDate.Year;

        if (age >= requirement.MinimumAge)
            context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

// Resource-based authorization
public class OrderAuthorizationHandler :
    AuthorizationHandler<OperationAuthorizationRequirement, Order>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement,
        Order resource)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        if (requirement.Name == "Edit" &&
            resource.CustomerId.ToString() == userId)
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == "Delete" &&
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

// Sử dụng trong Controller/Endpoint
public class OrdersController(IAuthorizationService authService) : ControllerBase
{
    [HttpPut("{id}")]
    public async Task<IActionResult> Update(Guid id, UpdateOrderRequest request)
    {
        var order = await _db.Orders.FindAsync(id);
        if (order is null) return NotFound();

        var authResult = await authService.AuthorizeAsync(
            User, order, Operations.Update);

        if (!authResult.Succeeded) return Forbid();

        // Update order...
        return Ok();
    }
}

5. Data Protection API

csharp
// Mã hóa/giải mã data (tokens, cookies, form fields)
// Tự động rotate keys, manage key lifetime

builder.Services.AddDataProtection()
    .SetApplicationName("MyApp")
    .PersistKeysToAzureBlobStorage(blobClient)          // Shared across instances
    .ProtectKeysWithAzureKeyVault(keyIdentifier, credential)
    .SetDefaultKeyLifetime(TimeSpan.FromDays(90));

// Sử dụng:
public class SecureTokenService(IDataProtectionProvider provider)
{
    private readonly ITimeLimitedDataProtector _protector =
        provider.CreateProtector("SecureTokens")
                .ToTimeLimitedDataProtector();

    public string CreateToken(string userId)
    {
        return _protector.Protect(userId, TimeSpan.FromHours(24));
    }

    public string? ValidateToken(string token)
    {
        try
        {
            return _protector.Unprotect(token);
        }
        catch (CryptographicException)
        {
            return null; // Invalid or expired
        }
    }
}

6. Rate Limiting (.NET 7+)

csharp
builder.Services.AddRateLimiter(options =>
{
    // Global: Fixed Window
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                Window = TimeSpan.FromMinutes(1),
                PermitLimit = 100
            }));

    // Named policies
    options.AddTokenBucketLimiter("api", opt =>
    {
        opt.TokenLimit = 50;
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
        opt.TokensPerPeriod = 10;
    });

    options.AddSlidingWindowLimiter("auth", opt =>
    {
        opt.Window = TimeSpan.FromMinutes(5);
        opt.SegmentsPerWindow = 5;
        opt.PermitLimit = 10;  // 10 login attempts per 5 min
    });

    // Custom response
    options.OnRejected = async (context, ct) =>
    {
        context.HttpContext.Response.StatusCode = 429;
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            await context.HttpContext.Response.WriteAsJsonAsync(new
            {
                error = "Too many requests",
                retryAfterSeconds = retryAfter.TotalSeconds
            }, ct);
        }
    };
});

app.UseRateLimiter();

// Apply per endpoint
app.MapPost("/api/auth/login", Login).RequireRateLimiting("auth");
app.MapGet("/api/products", GetProducts).RequireRateLimiting("api");

7. Security Headers & CORS

csharp
// Security Headers middleware
app.Use(async (context, next) =>
{
    var headers = context.Response.Headers;
    headers["X-Content-Type-Options"] = "nosniff";
    headers["X-Frame-Options"] = "DENY";
    headers["X-XSS-Protection"] = "0"; // Disable (modern browsers)
    headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
    headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()";
    headers["Content-Security-Policy"] =
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";
    await next();
});

// CORS
builder.Services.AddCors(options =>
{
    options.AddPolicy("Production", policy =>
    {
        policy.WithOrigins("https://myapp.com", "https://admin.myapp.com")
              .WithMethods("GET", "POST", "PUT", "DELETE")
              .WithHeaders("Content-Type", "Authorization")
              .SetPreflightMaxAge(TimeSpan.FromHours(1))
              .AllowCredentials();
    });
});

Tổng Kết Session 06

ConceptKey Takeaway
IdentityBuilt-in user/role management, extend IdentityUser cho custom fields
JWTShort-lived access + long-lived refresh, rotation pattern
OAuth/OIDCAuthorization Code + PKCE cho mọi client type
AuthorizationPolicy-based > role-based, resource-based cho fine-grained
Data ProtectionEncrypt tokens/cookies, auto key rotation
Rate LimitingBuilt-in .NET 7+, nhiều algorithms (Fixed, Sliding, Token Bucket)
Security HeadersCSP, X-Frame-Options, CORS - defense in depth

Bài tập: Implement JWT authentication với refresh token rotation. Viết rate limiter cho /api/auth/login endpoint (5 attempts/5 min). Verify bằng Postman.