JWT, authentication vs authorization, CORS, dan error handling
Satu cerita login dari form Angular sampai API, termasuk saat token ditolak atau server error
1. Peta alurnya
Semua bagian lesson ini adalah satu langkah dari peta berikut. Hafalkan petanya dulu, detailnya menyusul.
| # | Langkah | Di mana | Padanan Node/React |
|---|---|---|---|
| 1 | Form login (Reactive Forms) → AuthService → POST /api/auth/login | Angular (lesson 0016) | axios.post('/login') |
| 2 | Browser cek CORS: beda origin, jadi kirim preflight OPTIONS dulu | Browser ↔ UseCors | package cors di Express |
| 3 | API memeriksa kredensial, menerbitkan JWT | Controller → Service | jsonwebtoken.sign() |
| 4 | Angular menyimpan token; HTTP Interceptor menambah Authorization: Bearer … | Angular | interceptor Axios |
| 5 | UseAuthentication: validasi token → HttpContext.User | Middleware | passport-jwt → req.user |
| 6 | UseAuthorization: cek [Authorize] / role → lolos, 401, atau 403 | Middleware + atribut | middleware cek role |
| 7 | Exception di mana pun → IExceptionHandler → ProblemDetails; interceptor menangani 401 | API + Angular | error middleware (err, req, res, next) |
2. Authentication vs authorization (401 vs 403)
"Authentication is the process of determining a user's identity. Authorization is the process of determining whether a user has access to a resource."
[1]
Keduanya terpisah, "However, authorization relies on an authentication mechanism"
[2].
Itulah alasan urutan UseAuthentication → UseAuthorization di lesson 0005.
Saat gagal, handler JWT bearer menjawab dengan dua kode yang berbeda:
- Challenge → 401. User belum terautentikasi (token tidak ada, signature salah, kedaluwarsa): "A JWT bearer scheme returning a 401 result with a
www-authenticate: bearerheader." [1] - Forbid → 403. User sudah dikenal tapi tidak berhak: "A JWT bearer scheme returning a 403 result." [1]
[ApiController, Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet] // publik
public Task<IActionResult> GetAll() => ...
[HttpPost, Authorize] // harus login (selain itu 401)
public Task<IActionResult> Create(CreateProductDto dto) => ...
[HttpDelete("{id}"), Authorize(Roles = "Admin")] // login + role Admin (selain itu 403)
public Task<IActionResult> Delete(int id) => ...
}
Role adalah bentuk sederhana; aturan yang sama bisa ditulis sebagai policy: AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin")), lalu [Authorize(Policy = "RequireAdminRole")]
[3].
Ingat juga: "Configuring authentication doesn't automatically restrict access to endpoints." Tanpa [Authorize] (atau fallback policy), endpoint tetap terbuka [1].
3. JWT: apa isinya dan apa yang divalidasi
"JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties."
Bentuknya "a sequence of URL-safe parts separated by period ('.') characters. Each part contains a base64url-encoded value"
[4], yaitu header.payload.signature.
Base64url bukan enkripsi: "The payload is only base64url encoded, not encrypted, so anyone who obtains the token can read every claim"
[5].
Signature hanya menjamin token tidak diubah.
Di API, "The JwtBearerHandler validates the token and extracts the user's identity from its claims."
Yang harus divalidasi: "Signature, for trust and integrity", "Issuer claim", "Audience claim", dan "Token expiration";
kalau ada yang salah, "the API should return a 401 response"
[6].
// Program.cs — JwtSettings dibaca lewat Options Pattern (lesson 0005), Key dari User Secrets
var jwt = builder.Configuration.GetSection("JwtSettings").Get<JwtSettings>()!;
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(Encoding.UTF8.GetBytes(jwt.Key)),
ValidateLifetime = true, // exp
});
builder.Services.AddAuthorization();
Jujur saat interview: endpoint login sederhana yang menerbitkan JWT sendiri cukup untuk mini project, tapi Microsoft menulis "Don't generate your own access tokens or ID tokens, except for testing purposes" dan menyarankan OpenID Connect / OAuth untuk production [6]. Menyebut ini menunjukkan Anda tahu batasnya.
4. CORS: kenapa Angular di port 4200 "diblokir"
"Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction is called the same-origin policy." CORS adalah cara server melonggarkannya, dan CORS "is not a security feature, CORS relaxes security" [7]. Beda port saja sudah beda origin. Yang memblokir adalah browser, jadi Postman dan curl tidak pernah terkena CORS.
Preflight. "For some CORS requests, the browser sends an additional OPTIONS request before making the actual request."
Browser hanya boleh melewatinya kalau header yang di-set terbatas pada Accept, Accept-Language, Content-Language, Content-Type, atau Last-Event-ID,
dan Content-Type hanya form/text [7].
Request Angular dengan Authorization: Bearer dan body JSON tidak memenuhi syarat itu, jadi selalu ada preflight.
builder.Services.AddCors(o => o.AddPolicy("angular", p => p
.WithOrigins("http://localhost:4200") // tanpa "/" di akhir
.AllowAnyHeader() // termasuk Authorization
.AllowAnyMethod()));
app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("angular"); // sesudah routing, sebelum auth (lesson 0005)
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Dua jebakan dari dokumentasi: URL origin "must not contain a trailing slash", dan
"Specifying AllowAnyOrigin and AllowCredentials is an insecure configuration and can result in cross-site request forgery"
[7].
Padanan Express: app.use(cors({ origin: 'http://localhost:4200' })), middleware yang "sets CORS response headers"
[8].
5. Global error handling: exception → ProblemDetails
IExceptionHandler "gives the developer a callback for handling known exceptions in a central location".
Implementasinya didaftarkan dengan AddExceptionHandler<T>, lifetime-nya Singleton, dan return true dari TryHandleAsync berarti exception sudah ditangani
[9].
Format response-nya ProblemDetails, standar untuk membawa "machine-readable details of errors in HTTP response content"
[10] (RFC 9457, pengganti RFC 7807).
Dengan AddProblemDetails(), UseExceptionHandler juga menghasilkan ProblemDetails untuk exception yang tidak ditangani handler Anda
[11].
public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
{
var (status, title) = ex switch
{
ValidationException => (400, "Validation failed"),
NotFoundException => (404, "Not found"),
_ => (500, "Server error"),
};
if (status == 500) logger.LogError(ex, "Unhandled exception");
ctx.Response.StatusCode = status;
await ctx.Response.WriteAsJsonAsync(new ProblemDetails
{ Status = status, Title = title, Detail = status == 500 ? null : ex.Message }, ct);
return true;
}
}
// Program.cs
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler(); // paling awal di pipeline
// Response yang diterima Angular (contoh)
{ "status": 400, "title": "Validation failed", "detail": "Price must be greater than 0." }
Di Angular, body ProblemDetails ada di HttpErrorResponse.error, jadi interceptor dari lesson 0016 bisa
mengarahkan 401 ke /login dan menampilkan error.title untuk status lain.
Jangan kirim stack trace ke client di production: "Don't share detailed exception information publicly when the app runs in production"
[11].
Padanan Express: error middleware empat argumen (err, req, res, next) yang dipasang paling akhir
[12]; di .NET justru paling awal, karena membungkus pipeline.
6. Menyimpan JWT: localStorage vs httpOnly cookie
localStorage + header Bearer | Cookie HttpOnly + Secure + SameSite | |
|---|---|---|
| Dibaca JavaScript? | Ya. "Do not store session identifiers in local storage as the data is always accessible by JavaScript." [13] | Tidak. "Cookies can mitigate this risk using the httpOnly flag." [13] |
| Risiko utama | XSS mencuri token | CSRF: "browser requests automatically include all cookies" [14] |
| Mitigasi | cegah XSS, token berumur pendek | SameSite + token anti-CSRF; Angular HttpClient mengirim X-XSRF-TOKEN dari cookie XSRF-TOKEN, tapi "supports only the client half" [15] |
| Dikirim lewat | HTTP Interceptor | browser otomatis (perlu withCredentials lintas origin) |
Tidak ada pilihan yang bebas risiko; "Cross-Site Scripting (XSS) can defeat all CSRF mitigation techniques!" [14]. Microsoft cenderung ke cookie: "storing access tokens directly in the browser by using local storage, session storage, or web workers" adalah contoh penyimpanan yang rentan, dan untuk web app yang aman "Only a secure HTTP only cookie is shared on the client browser" (pola backend-for-frontend) [6]. Jawaban interview yang baik: sebut trade-off keduanya, lalu sebut pilihan Anda beserta alasannya.
Latihan 1 — Susun alur login
Susun langkahnya dari klik "Login" sampai request berikutnya diproses. Ucapkan setiap langkah dengan suara keras saat memindahkannya.
- Form login Angular memanggil
AuthService→POST /api/auth/login - Browser mengirim preflight
OPTIONS;UseCorsmengizinkan origin Angular - API memeriksa kredensial lalu menerbitkan JWT
- Angular menyimpan token; interceptor menambah
Authorization: Bearerke request berikutnya UseAuthenticationmemvalidasi token dan mengisiHttpContext.UserUseAuthorizationmemeriksa[Authorize]: lolos, 401, atau 403- Exception di Service →
IExceptionHandler→ ProblemDetails ke Angular
CORS terjadi sebelum request apa pun sampai ke Controller, termasuk request login. Token baru ada setelah login, jadi interceptor baru berguna untuk request berikutnya. Authentication selalu sebelum authorization, dan exception handler membungkus semuanya, jadi error dari langkah mana pun berakhir sebagai ProblemDetails.
Latihan 2 — JWT, 401, dan 403
Jawab tanpa scroll ke atas; kalau ragu, tebak dulu, lalu baca penjelasannya.
Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil di mock interview 0018.
Latihan 3 — CORS, ProblemDetails, dan penyimpanan token
Sebelum mengklik, ucapkan alasannya dalam satu kalimat.
Latihan 4 — "Jelaskan alur login aplikasi Anda"
Pasang timer 90 detik. Jawab dengan suara keras, seolah sedang interview. Mulai dari frontend, selesaikan di backend, lalu ceritakan jalur error. Jangan buka jawaban contoh sebelum selesai.
"Jelaskan alur login aplikasi Anda, dari form Angular sampai API, termasuk apa yang terjadi saat token tidak valid atau terjadi error."
Bandingkan dengan jawaban contoh dan checklist-nya
"User mengisi form login Reactive Forms; AuthService mengirim POST /api/auth/login lewat HttpClient.
Karena Angular di port 4200 dan API di port lain, browser menerapkan same-origin policy, jadi API mengaktifkan CORS untuk origin Angular saja.
API memeriksa kredensial dan menerbitkan JWT: header.payload.signature, berisi claims seperti sub, role, dan exp, ditandatangani key yang disimpan di User Secrets.
Di Angular, AuthInterceptor meng-clone setiap request dan menambahkan Authorization: Bearer.
Di API, UseAuthentication memvalidasi signature, issuer, audience, dan expiry lalu mengisi HttpContext.User;
UseAuthorization memeriksa [Authorize] atau [Authorize(Roles = "Admin")].
Token tidak valid atau kedaluwarsa menghasilkan 401, dan interceptor mengarahkan ke /login; sudah login tapi tidak berhak menghasilkan 403.
Exception lain ditangkap IExceptionHandler dan dikembalikan sebagai ProblemDetails, misalnya ValidationException jadi 400 dan NotFoundException jadi 404, tanpa stack trace di production."
Checklist: apakah Anda menyebut…
- frontend: form login →
AuthService→HttpClientPOST /api/auth/login? - API menerbitkan JWT (header.payload.signature, claims +
exp), key di User Secrets? - HTTP Interceptor Angular:
clonerequest + headerAuthorization: Bearer? - backend:
UseAuthenticationmemvalidasi token (signature, issuer, audience, expiry) →HttpContext.User;UseAuthorization+[Authorize]/role? - 401 (token tidak valid, interceptor → /login) vs 403 (login tapi tidak berhak)?
- error global:
IExceptionHandler+AddProblemDetails→ ProblemDetails (400/404/500), dan CORS hanya untuk origin Angular?
Kalau kurang dari 4 poin, ulangi setelah membaca ulang bagian 1. Soal ini akan muncul lagi di mock interview 0018.
Latihan 5 — CORS dan tempat menyimpan JWT
Timer 60 detik. Dua pertanyaan ini sering ditanyakan berurutan.
"Apa itu CORS, kenapa API Anda perlu mengonfigurasinya, dan di mana Anda menyimpan JWT di frontend?"
Bandingkan dengan jawaban contoh dan checklist-nya
"Browser menerapkan same-origin policy: halaman dari localhost:4200 tidak boleh membaca response dari origin lain.
CORS adalah cara server melonggarkannya dengan header seperti Access-Control-Allow-Origin; untuk request dengan header Authorization atau JSON, browser mengirim preflight OPTIONS dulu.
Jadi CORS bukan fitur keamanan, justru melonggarkan keamanan. Di .NET saya pakai WithOrigins untuk origin Angular saja, UseCors sesudah UseRouting dan sebelum auth,
dan tidak pernah AllowAnyOrigin + AllowCredentials. Padanan Express-nya package cors.
Untuk JWT: localStorage mudah dan cocok dengan interceptor, tapi bisa dibaca script hasil XSS.
Cookie HttpOnly + Secure + SameSite tidak bisa dibaca JavaScript, tapi otomatis terkirim sehingga perlu proteksi CSRF; Angular membantu lewat XSRF-TOKEN.
Microsoft menyarankan pola backend-for-frontend dengan cookie HttpOnly untuk production; di mini project saya pakai localStorage dengan token berumur pendek dan sadar trade-off-nya."
Checklist: apakah Anda menyebut…
- same-origin policy ditegakkan browser; beda port = beda origin; Postman tidak terkena?
- CORS = server melonggarkan lewat header
Access-Control-*; bukan fitur keamanan? - preflight
OPTIONSuntuk headerAuthorization/ JSON; policy harus mengizinkan header itu? - konfigurasi .NET:
WithOriginsspesifik,UseCorssesudahUseRoutingsebelum auth, janganAllowAnyOrigin+AllowCredentials? localStorage: mudah + cocok interceptor, tapi terbaca oleh XSS?- httpOnly cookie: aman dari pembacaan JS, tapi perlu proteksi CSRF (
SameSite,XSRF-TOKEN); lalu pilihan Anda + alasannya?
Ringkasan untuk dicetak: Reference: JWT, authn/authz, CORS, dan error handling · Pipeline: Reference: Middleware dan Options · Istilah: GLOSSARY.md
Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "bagaimana refresh token bekerja bersama access token yang berumur pendek?" atau "bagaimana menulis integration test untuk endpoint [Authorize] dengan WebApplicationFactory?"
Sumber: [1] Overview of ASP.NET Core Authentication · [2] Introduction to authorization · [3] Role-based authorization · [4] RFC 7519 JWT · [5] OWASP JWT Cheat Sheet · [6] Configure JWT bearer authentication · [7] Enable CORS in ASP.NET Core · [8] Express cors middleware · [9] Handle errors in ASP.NET Core · [10] RFC 9457 Problem Details · [11] Handle errors in ASP.NET Core APIs · [12] Express error handling · [13] OWASP HTML5 Security Cheat Sheet · [14] OWASP CSRF Prevention Cheat Sheet · [15] Angular v16: XSRF protection