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
csharpbuilder.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;
}
JWT vs Cookie - Khi Nào Chọn Gì?
| Tiêu chí | JWT (Bearer Token) | Cookie |
|---|---|---|
| Storage | Client (localStorage/memory) | Browser auto-manage |
| CSRF | Không bị CSRF | Cần anti-forgery token |
| XSS | Bị XSS nếu localStorage | HttpOnly 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
csharpbuilder.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+)
csharpbuilder.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
| Concept | Key Takeaway |
|---|---|
| Identity | Built-in user/role management, extend IdentityUser cho custom fields |
| JWT | Short-lived access + long-lived refresh, rotation pattern |
| OAuth/OIDC | Authorization Code + PKCE cho mọi client type |
| Authorization | Policy-based > role-based, resource-based cho fine-grained |
| Data Protection | Encrypt tokens/cookies, auto key rotation |
| Rate Limiting | Built-in .NET 7+, nhiều algorithms (Fixed, Sliding, Token Bucket) |
| Security Headers | CSP, 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/loginendpoint (5 attempts/5 min). Verify bằng Postman.