ASP.NET Core API #7: Menambahkan Fitur Login dan Registrasi dengan JWT

ASP.NET Core API #7: Menambahkan Fitur Login dan Registrasi dengan JWT

Halo lagi, calon penjaga gerbang API! 

Sampai saat ini, API kita bisa diakses oleh siapa saja. Semua orang bisa menambah, mengubah, atau menghapus data wisata. Tentu ini berbahaya, seperti membiarkan pintu rumah terbuka lebar. Sekarang kita akan memasang gembok digital yang bernama JWT (JSON Web Token). Dengan JWT, hanya pengguna yang sudah login yang bisa mengakses endpoint tertentu.

🤣 Kenapa JWT cocok buat ngamankan API? Karena dia Jago Web Token! (maaf, bapak-bapak mode on)

Apa itu JWT?

Bayangkan JWT seperti stempel yang diberikan ke pengguna setelah login. Setiap kali pengguna mau masuk ke area terlarang (endpoint tertentu), dia harus menunjukkan stempel itu. Stempel ini sudah ditandatangani oleh server, jadi tidak bisa dipalsukan. Di dalam stempel juga tersimpan data seperti username dan waktu berlaku.

Alur kerjanya:

  1. Pengguna daftar (registrasi) ke server.
  2. Pengguna login (mengirim username & password).
  3. Server memeriksa, kalau cocok, server buatkan JWT dan mengirimkannya ke pengguna.
  4. Selanjutnya, setiap kali pengguna mau akses endpoint yang diamankan, dia harus menyertakan JWT di header (biasanya Authorization: Bearer <token>).
  5. Server memverifikasi token, kalau valid, akses diizinkan.

Mari kita implementasikan!

Langkah 1: Install Package yang Diperlukan

Kita perlu beberapa package tambahan untuk JWT dan hashing password. Buka Package Manager Console dan install:

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Install-Package BCrypt.Net-Next
Install-Package System.IdentityModel.Tokens.Jwt

Atau lewat NuGet Package Manager. Package BCrypt.Net-Next digunakan untuk meng-hash password (jangan pernah simpan password asli di database!).

Langkah 2: Membuat Model User

Kita perlu tabel untuk menyimpan data pengguna. Buat model baru di folder Models dengan nama User.cs:

using System.ComponentModel.DataAnnotations;

namespace WisataAPI.Models
{
    public class User
    {
        [Key]
        public int Id { get; set; }
        
        [Required]
        [MaxLength(50)]
        public string Username { get; set; }
        
        [Required]
        [EmailAddress]
        [MaxLength(100)]
        public string Email { get; set; }
        
        [Required]
        public string PasswordHash { get; set; } // Yang disimpan adalah hash, bukan password asli
    }
}

Perhatikan: kita hanya menyimpan PasswordHash, bukan password asli. Ini penting untuk keamanan.

Tambahkan juga DbSet<User> di WisataDbContext.cs:

public DbSet<User> Users { get; set; }

Sekarang kita perlu membuat migration untuk menambah tabel Users. Buka Package Manager Console, jalankan:

Add-Migration AddUsersTable
Update-Database

Kalau berhasil, tabel Users akan muncul di database.

Langkah 3: Membuat DTO untuk Registrasi dan Login

DTO (Data Transfer Object) digunakan untuk menerima data dari client tanpa mengekspos model asli. Buat folder DTOs, lalu buat dua class:

RegisterDto.cs

using System.ComponentModel.DataAnnotations;

namespace WisataAPI.DTOs
{
    public class RegisterDto
    {
        [Required]
        [MaxLength(50)]
        public string Username { get; set; }
        
        [Required]
        [EmailAddress]
        public string Email { get; set; }
        
        [Required]
        [MinLength(6)]
        public string Password { get; set; }
    }
}

LoginDto.cs

using System.ComponentModel.DataAnnotations;

namespace WisataAPI.DTOs
{
    public class LoginDto
    {
        [Required]
        public string Username { get; set; }
        
        [Required]
        public string Password { get; set; }
    }
}

🔧 Langkah 4: Menyiapkan JWT Settings di appsettings.json

Buka appsettings.json, tambahkan bagian Jwt di dalamnya:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost\\SQLEXPRESS;Database=WisataDB;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "Jwt": {
    "Key": "IniAdalahKunciRahasiaYangSangatPanjangDanSulitDitebak12345!",
    "Issuer": "WisataAPI",
    "Audience": "WisataAPIClient",
    "DurationInMinutes": 60
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Key adalah kunci rahasia untuk menandatangani token. Panjang minimal 32 karakter. Jangan pernah bagikan kunci ini! Di production nanti disimpan di tempat aman (misal environment variable). Issuer dan Audience bisa diisi sesuai nama aplikasi. DurationInMinutes menentukan berapa lama token berlaku.

Langkah 5: Konfigurasi Authentication di Program.cs

Sekarang kita konfigurasi service authentication dengan JWT. Tambahkan kode berikut di Program.cs (setelah AddDbContext atau sebelum AddControllers):

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

// ... setelah builder.Services.AddDbContext...

// Konfigurasi JWT
var jwtSettings = builder.Configuration.GetSection("Jwt");
var key = Encoding.ASCII.GetBytes(jwtSettings["Key"]);

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false; // di production set true
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = true,
        ValidIssuer = jwtSettings["Issuer"],
        ValidateAudience = true,
        ValidAudience = jwtSettings["Audience"],
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero
    };
});

Jangan lupa tambahkan juga app.UseAuthentication(); sebelum app.UseAuthorization(); di bagian bawah file (sebelum app.MapControllers();).

Bagian konfigurasi ini memberi tahu ASP.NET untuk menggunakan JWT sebagai mekanisme autentikasi.

Langkah 6: Membuat AuthController

Buat controller baru: AuthController.cs di folder Controllers. Ini akan menangani registrasi dan login.

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using WisataAPI.Data;
using WisataAPI.DTOs;
using WisataAPI.Models;

namespace WisataAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        private readonly WisataDbContext _context;
        private readonly IConfiguration _configuration;

        public AuthController(WisataDbContext context, IConfiguration configuration)
        {
            _context = context;
            _configuration = configuration;
        }

        // POST: api/auth/register
        [HttpPost("register")]
        public async Task<IActionResult> Register(RegisterDto registerDto)
        {
            // Cek apakah username sudah dipakai
            if (await _context.Users.AnyAsync(u => u.Username == registerDto.Username))
            {
                return BadRequest("Username sudah digunakan.");
            }

            // Cek email sudah dipakai
            if (await _context.Users.AnyAsync(u => u.Email == registerDto.Email))
            {
                return BadRequest("Email sudah digunakan.");
            }

            // Hash password
            string passwordHash = BCrypt.Net.BCrypt.HashPassword(registerDto.Password);

            // Buat user baru
            var user = new User
            {
                Username = registerDto.Username,
                Email = registerDto.Email,
                PasswordHash = passwordHash
            };

            _context.Users.Add(user);
            await _context.SaveChangesAsync();

            return Ok("Registrasi berhasil.");
        }

        // POST: api/auth/login
        [HttpPost("login")]
        public async Task<IActionResult> Login(LoginDto loginDto)
        {
            // Cari user berdasarkan username
            var user = await _context.Users.FirstOrDefaultAsync(u => u.Username == loginDto.Username);
            if (user == null)
            {
                return Unauthorized("Username atau password salah.");
            }

            // Verifikasi password
            bool isPasswordValid = BCrypt.Net.BCrypt.Verify(loginDto.Password, user.PasswordHash);
            if (!isPasswordValid)
            {
                return Unauthorized("Username atau password salah.");
            }

            // Buat token JWT
            var token = GenerateJwtToken(user);

            return Ok(new { token });
        }

        private string GenerateJwtToken(User user)
        {
            var jwtSettings = _configuration.GetSection("Jwt");
            var key = Encoding.ASCII.GetBytes(jwtSettings["Key"]);

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(new[]
                {
                    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                    new Claim(ClaimTypes.Name, user.Username),
                    new Claim(ClaimTypes.Email, user.Email)
                }),
                Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(jwtSettings["DurationInMinutes"])),
                Issuer = jwtSettings["Issuer"],
                Audience = jwtSettings["Audience"],
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
            };

            var tokenHandler = new JwtSecurityTokenHandler();
            var token = tokenHandler.CreateToken(tokenDescriptor);
            return tokenHandler.WriteToken(token);
        }
    }
}

Penjelasan:

  • Register: menerima username, email, password. Mengecek duplikat, menghash password dengan BCrypt, simpan user.
  • Login: mencari user berdasarkan username, verifikasi password dengan BCrypt. Jika cocok, buat token JWT dan kembalikan.
  • GenerateJwtToken: membuat token dengan claims (data pengguna) dan kunci rahasia.
💡 BCrypt secara otomatis menghasilkan salt dan menyimpannya bersama hash. Jadi kita tidak perlu repot-repot mengelola salt.

Langkah 7: Mengamankan Endpoint dengan [Authorize]

Sekarang kita bisa melindungi endpoint tertentu. Misalnya, kita ingin hanya pengguna yang sudah login yang bisa menambah, mengubah, atau menghapus kategori dan wisata. Caranya, tambahkan atribut [Authorize] di atas controller atau method.

Di KategoriController.cs, tambahkan using Microsoft.AspNetCore.Authorization; lalu tempelkan [Authorize] di atas class (atau method tertentu). Misalnya:

[Authorize]
[Route("api/[controller]")]
[ApiController]
public class KategoriController : ControllerBase
{
    // ... semua method di dalamnya hanya bisa diakses dengan token valid
}

Kalau mau method GET tetap terbuka untuk umum, tapi POST, PUT, DELETE hanya untuk yang login, kita bisa letakkan [Authorize] hanya pada method-method tersebut.

Contoh di WisataController.cs:

// GET tetap publik
[HttpGet]
public async Task<...> GetWisata() { ... }

[Authorize]
[HttpPost]
public async Task<...> PostWisata(...) { ... }

Sesuaikan dengan kebutuhan. Untuk latihan, kita akan amankan semua method kecuali GET yang bersifat publik.

Langkah 8: Uji Coba dengan Postman

Jalankan API (F5). Buka Postman.

1. Registrasi User Baru

  • Method: POST
  • URL: https://localhost:7000/api/auth/register
  • Body (raw JSON):
    {
        "username": "joko",
        "email": "joko@example.com",
        "password": "rahasia123"
    }
  • Klik Send. Response: "Registrasi berhasil."

2. Login

  • Method: POST
  • URL: https://localhost:7000/api/auth/login
  • Body:
    {
        "username": "joko",
        "password": "rahasia123"
    }
  • Klik Send. Response akan berisi token, misalnya:
    {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
  • Salin token tersebut (tanpa tanda kutip).

3. Akses Endpoint yang Dilindungi Tanpa Token

  • Coba POST ke /api/kategori tanpa mengirim token.
  • Hasilnya: 401 Unauthorized. Bagus, berarti endpoint sudah diamankan.

4. Akses Endpoint dengan Token

  • Di Postman, pilih tab Authorization.
  • Pilih type Bearer Token.
  • Tempel token yang tadi didapat di field Token.
  • Kirim request POST ke /api/kategori dengan data kategori baru (misal "Taman Safari").
  • Seharusnya berhasil (201 Created).

Cara lain: tambahkan header manual: Authorization: Bearer <token> di tab Headers.

5. Cek Endpoint Publik (GET)

  • GET ke /api/kategori tanpa token harus tetap bisa.
  • Ini karena kita tidak mengamankan method GET.
💡 Di Postman, kamu bisa menyimpan token ke variable environment agar otomatis terkirim. Tapi untuk sekarang, manual dulu gapapa.

Penjelasan Tambahan

Mengapa pakai BCrypt?

BCrypt adalah algoritma hashing yang dirancang khusus untuk password. Dia lambat (secara sengaja) sehingga sulit ditebak dengan brute force. Jangan pernah pakai hash cepat seperti MD5 atau SHA untuk password.

Apa itu Claims?

Claims adalah potongan data yang disimpan di dalam token. Biasanya berisi informasi pengguna seperti id, username, email. Nanti di controller kita bisa mengambil data ini melalui User.FindFirstValue(ClaimTypes.NameIdentifier).

Bagaimana cara mengambil data user dari token di controller?

Contoh di method POST kategori, kita bisa tahu siapa yang menambah data dengan membaca claim. Tapi untuk sekarang kita tidak pakai dulu.

Membersihkan dan Menambahkan Validasi

Kita mungkin perlu menambahkan role nantinya (admin, user biasa). Tapi untuk sekarang, fitur login dasar sudah cukup.

Jangan lupa, di production, set RequireHttpsMetadata = true dan pastikan kunci JWT disimpan di tempat aman (misal environment variable, Azure Key Vault).

Selamat! API Kamu Sekarang Punya Sistem Keamanan

Sekarang API-mu sudah memiliki sistem autentikasi. Pengguna harus login dulu untuk bisa mengubah data. Ini langkah besar menuju aplikasi yang profesional.

Di artikel berikutnya (ASP.NET Core API #8), kita akan membahas lebih dalam tentang authorization dan mungkin menambahkan role admin. Sampai jumpa!

😎 Kalau ada yang error, coba cek lagi secret key-nya. Jangan sampai beda panjang atau lupa di-set. Ingat, JWT itu ibarat kunci, kalau salah kunci, pintu nggak bakal kebuka.

Ditulis oleh Kakak programmer yang dulu juga suka lupa password. Kalau ada pertanyaan, tulis di komentar ya!

Lebih baru Lebih lama

نموذج الاتصال