Reference · Fullstack · .NET 8 + Angular 16
JWT, authn/authz, CORS, dan error handling
Ringkasan satu halaman. Dari Lesson 0017.
Alur login (ucapkan dalam urutan ini)
- Form login →
AuthService→POST /api/auth/login(HttpClient) - Beda origin → browser kirim preflight
OPTIONS→UseCors("angular") - API cek kredensial → terbitkan JWT (
header.payload.signature; claimssub, role,exp) - Angular simpan token;
AuthInterceptor:req.clone({ setHeaders: { Authorization: `Bearer ${t}` } }) UseAuthentication: validasi signature, issuer, audience, expiry →HttpContext.User(ClaimsPrincipal)UseAuthorization:[Authorize]/[Authorize(Roles = "Admin")]/[Authorize(Policy = "...")]- Exception →
IExceptionHandler→ ProblemDetails; interceptor: 401 →/login
Authentication vs authorization
| Authentication | Authorization | |
|---|---|---|
| Pertanyaan | Siapa kamu? (identitas) | Boleh akses resource ini? |
| Middleware | UseAuthentication (duluan) | UseAuthorization (sesudahnya) |
| Gagal (JWT bearer) | challenge → 401 + WWW-Authenticate: Bearer | forbid → 403 |
| Node | passport-jwt → req.user | middleware cek role |
Authentication tidak otomatis mengunci endpoint: butuh [Authorize] atau fallback policy.
JWT di .NET 8
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o => o.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true, ValidIssuer = jwt.Issuer,
ValidateAudience = true, ValidAudience = jwt.Audience,
ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
ValidateLifetime = true });
builder.Services.AddAuthorization();
- Payload = base64url, bukan enkripsi. Siapa saja bisa membaca claims; jangan taruh data sensitif.
- Key di User Secrets / vault, dibaca lewat Options Pattern (
JwtSettings). - Production: Microsoft menyarankan OIDC/OAuth, bukan menerbitkan token sendiri dari username/password.
CORS
builder.Services.AddCors(o => o.AddPolicy("angular", p =>
p.WithOrigins("http://localhost:4200").AllowAnyHeader().AllowAnyMethod()));
app.UseRouting(); app.UseCors("angular"); app.UseAuthentication(); app.UseAuthorization();
| Same-origin policy | ditegakkan browser; beda skema/host/port = beda origin; Postman/curl tidak terkena |
| CORS | server melonggarkan SOP lewat header Access-Control-*; bukan fitur keamanan |
| Preflight | OPTIONS sebelum request bila ada header di luar safelist (mis. Authorization) atau Content-Type: application/json |
| Jebakan | origin tanpa / di akhir · jangan AllowAnyOrigin + AllowCredentials |
| Express | app.use(cors({ origin: 'http://localhost:4200' })) |
Global error handling
public class GlobalExceptionHandler : IExceptionHandler // Singleton
{ ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
// ValidationException → 400, NotFoundException → 404, lainnya → 500 (log, tanpa detail)
// tulis ProblemDetails, return true = sudah ditangani
}
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler(); // PALING AWAL (Express: error middleware paling AKHIR)
ProblemDetails (RFC 9457, dulu 7807): type, title, status, detail, instance.
[ApiController] otomatis mengembalikan 400 ValidationProblemDetails saat model tidak valid.
Angular: body ada di HttpErrorResponse.error.
Menyimpan JWT
localStorage | Cookie HttpOnly; Secure; SameSite | |
|---|---|---|
| Dibaca JS | ya | tidak |
| Risiko | XSS mencuri token | CSRF (cookie terkirim otomatis) |
| Mitigasi | cegah XSS, token pendek | SameSite, anti-CSRF token (Angular: XSRF-TOKEN → X-XSRF-TOKEN, server wajib verifikasi) |
| Kirim lewat | HTTP Interceptor | browser (withCredentials lintas origin) |
Microsoft: untuk web app yang aman, simpan token di server (BFF) dan hanya bagikan cookie HttpOnly. Jawab interview: trade-off + pilihan Anda + alasan.