Lesson 0017 · Fullstack · ±15 menit

JWT, authentication vs authorization, CORS, dan error handling

Satu cerita login dari form Angular sampai API, termasuk saat token ditolak atau server error

Target lesson ini: Anda bisa menjawab pertanyaan interview "Jelaskan alur login aplikasi Anda, dari form Angular sampai API, termasuk apa yang terjadi saat token tidak valid atau terjadi error." dalam 90 detik, tanpa melompat-lompat antara frontend dan backend.

1. Peta alurnya

Semua bagian lesson ini adalah satu langkah dari peta berikut. Hafalkan petanya dulu, detailnya menyusul.

#LangkahDi manaPadanan Node/React
1Form login (Reactive Forms) → AuthService → POST /api/auth/loginAngular (lesson 0016)axios.post('/login')
2Browser cek CORS: beda origin, jadi kirim preflight OPTIONS duluBrowser ↔ UseCorspackage cors di Express
3API memeriksa kredensial, menerbitkan JWTController → Servicejsonwebtoken.sign()
4Angular menyimpan token; HTTP Interceptor menambah Authorization: Bearer …Angularinterceptor Axios
5UseAuthentication: validasi token → HttpContext.UserMiddlewarepassport-jwt → req.user
6UseAuthorization: cek [Authorize] / role → lolos, 401, atau 403Middleware + atributmiddleware cek role
7Exception di mana pun → IExceptionHandler → ProblemDetails; interceptor menangani 401API + Angularerror 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:

[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 BearerCookie 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 utamaXSS mencuri tokenCSRF: "browser requests automatically include all cookies" [14]
Mitigasicegah XSS, token berumur pendekSameSite + token anti-CSRF; Angular HttpClient mengirim X-XSRF-TOKEN dari cookie XSRF-TOKEN, tapi "supports only the client half" [15]
Dikirim lewatHTTP Interceptorbrowser 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.

  1. Form login Angular memanggil AuthService → POST /api/auth/login
  2. Browser mengirim preflight OPTIONS; UseCors mengizinkan origin Angular
  3. API memeriksa kredensial lalu menerbitkan JWT
  4. Angular menyimpan token; interceptor menambah Authorization: Bearer ke request berikutnya
  5. UseAuthentication memvalidasi token dan mengisi HttpContext.User
  6. UseAuthorization memeriksa [Authorize]: lolos, 401, atau 403
  7. 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 → HttpClient POST /api/auth/login?
  • API menerbitkan JWT (header.payload.signature, claims + exp), key di User Secrets?
  • HTTP Interceptor Angular: clone request + header Authorization: Bearer?
  • backend: UseAuthentication memvalidasi 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 OPTIONS untuk header Authorization / JSON; policy harus mengizinkan header itu?
  • konfigurasi .NET: WithOrigins spesifik, UseCors sesudah UseRouting sebelum auth, jangan AllowAnyOrigin + 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?
Bacaan utama (±15 menit) Microsoft Learn: Configure JWT bearer authentication in ASP.NET Core, terutama bagian 401 vs 403, validasi token, dan "Recommended approaches". Kalau masih ada waktu, baca bagian "Preflight requests" di Enable CORS in ASP.NET Core.

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