Autentikasi Fullstack Aman dengan Next.js dan ASP.NET Core melalui Cookie Lintas Domain
Dalam dunia pengembangan web modern, membangun aplikasi full-stack yang aman adalah hal yang terpenting. Autentikasi, proses memverifikasi identitas pengguna, adalah komponen penting dari keamanan. Dalam postingan blog ini, kita akan menjelajahi cara menerapkan sistem autentikasi full-stack yang kuat menggunakan Next.js sebagai frontend dan ASP.NET Core sebagai backend, memanfaatkan cookie lintas domain yang aman.
Daftar Isi
- Pendahuluan
- Memahami Autentikasi Lintas Domain
- Apa itu Autentikasi Lintas Domain?
- Tantangan dan Pertimbangan
- Mengapa Cookie Lintas Domain?
- Penyiapan Proyek
- Membuat Aplikasi Next.js
- Membuat Proyek ASP.NET Core Web API
- Konfigurasi CORS
- Implementasi Backend ASP.NET Core
- Membuat Model Pengguna dan Konteks Database
- Implementasi Endpoint Pendaftaran dan Login
- Implementasi Refresh Token
- Konfigurasi Cookie Aman
- Implementasi Frontend Next.js
- Membuat Formulir Pendaftaran dan Login
- Menangani Permintaan API dengan `fetch` atau `axios`
- Menyimpan dan Mengelola Token Autentikasi dalam Cookie
- Mengimplementasikan Perlindungan Rute
- Keamanan dan Praktik Terbaik
- Menggunakan HTTPS
- Cookie Aman dan Atribut HTTPOnly
- Mencegah Serangan CSRF
- Validasi dan Sanitasi Input
- Pembaruan dan Patch Keamanan Reguler
- Penyebaran
- Konfigurasi untuk Lingkungan Produksi
- Pertimbangan untuk Skalabilitas
- Kesimpulan
1. Pendahuluan
Arsitektur full-stack di mana frontend dan backend dihosting di domain yang berbeda menjadi semakin umum. Skenario ini menimbulkan tantangan dalam mengelola autentikasi pengguna dengan aman. Artikel ini akan memberikan panduan langkah demi langkah tentang cara menerapkan autentikasi yang aman dan efektif menggunakan Next.js (frontend) dan ASP.NET Core (backend) dengan memanfaatkan cookie lintas domain. Kita akan membahas tantangan, solusi, dan praktik terbaik untuk memastikan aplikasi kita aman dan mudah digunakan.
2. Memahami Autentikasi Lintas Domain
2.1 Apa itu Autentikasi Lintas Domain?
Autentikasi lintas domain terjadi ketika frontend dan backend aplikasi web dihosting di domain yang berbeda (mis., `app.example.com` dan `api.example.com`). Dalam skenario ini, mekanisme autentikasi tradisional seperti cookie sesi mungkin tidak berfungsi langsung karena kebijakan same-origin browser.
2.2 Tantangan dan Pertimbangan
- Kebijakan Same-Origin (SOP): SOP mencegah JavaScript dari satu origin membuat permintaan ke origin lain. Kebijakan ini diterapkan untuk mencegah serangan lintas situs.
- Berbagi Sumber Daya Lintas Asal (CORS): CORS adalah mekanisme yang menggunakan header HTTP untuk memberi tahu browser web untuk mengizinkan permintaan lintas asal. Konfigurasi CORS yang tepat sangat penting untuk autentikasi lintas domain.
- Keamanan Cookie: Mengelola cookie lintas domain memerlukan pertimbangan yang cermat untuk memastikan bahwa cookie aman dan tidak rentan terhadap serangan seperti pemalsuan permintaan lintas situs (CSRF) dan skrip lintas situs (XSS).
2.3 Mengapa Cookie Lintas Domain?
Meskipun ada metode autentikasi lain seperti token berbasis localStorage atau sessionStorage, cookie menawarkan beberapa keuntungan:
- Keamanan: Cookie dapat dikonfigurasi dengan bendera `HttpOnly` dan `Secure`, sehingga membuatnya lebih tahan terhadap serangan XSS dan memastikan mereka hanya dikirim melalui HTTPS.
- Otomatisasi: Browser secara otomatis menangani pengiriman cookie dengan setiap permintaan, mengurangi jumlah kode yang perlu kita tulis di frontend.
- Kompatibilitas: Cookie didukung secara luas dan dipahami oleh semua browser web.
3. Penyiapan Proyek
3.1 Membuat Aplikasi Next.js
Pertama, kita perlu membuat aplikasi Next.js baru. Anda dapat melakukan ini menggunakan `create-next-app`:
- Buka terminal Anda.
- Jalankan perintah berikut:
npx create-next-app frontend
cd frontend
Ini akan membuat aplikasi Next.js baru di direktori bernama `frontend`.
3.2 Membuat Proyek ASP.NET Core Web API
Selanjutnya, kita perlu membuat proyek ASP.NET Core Web API untuk backend kita:
- Buka terminal baru.
- Jalankan perintah berikut:
dotnet new webapi -n Backend
cd Backend
Ini akan membuat proyek Web API ASP.NET Core baru di direktori bernama `Backend`.
3.3 Konfigurasi CORS
Untuk memungkinkan permintaan lintas domain, kita perlu mengonfigurasi CORS di aplikasi ASP.NET Core. Lakukan langkah-langkah berikut:
- Buka file `Startup.cs`.
- Di metode `ConfigureServices`, tambahkan kode berikut:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
builder =>
{
builder.WithOrigins("http://localhost:3000") // Ganti dengan origin aplikasi Next.js Anda
.AllowCredentials()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
services.AddControllers();
}
- Di metode `Configure`, tambahkan `app.UseCors(“AllowSpecificOrigin”);` sebelum `app.UseEndpoints`.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("AllowSpecificOrigin");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Pastikan untuk mengganti `”http://localhost:3000″` dengan origin aplikasi Next.js Anda. `AllowCredentials()` penting untuk memungkinkan cookie dikirim dalam permintaan lintas domain.
4. Implementasi Backend ASP.NET Core
4.1 Membuat Model Pengguna dan Konteks Database
Pertama, mari kita buat model Pengguna dan konteks database. Buat direktori baru bernama `Models` di dalam proyek ASP.NET Core Anda. Tambahkan kelas `User.cs` ke direktori ini dengan konten berikut:
using System;
using System.ComponentModel.DataAnnotations;
namespace Backend.Models
{
public class User
{
[Key]
public int Id { get; set; }
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; } // Hashing diimplementasikan dalam kode lain.
public string RefreshToken { get; set; }
public DateTime RefreshTokenExpiryTime { get; set; }
}
}
Sekarang, buat kelas `ApplicationDbContext.cs` di direktori yang sama:
using Microsoft.EntityFrameworkCore;
using Backend.Models;
namespace Backend.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<User> Users { get; set; }
}
}
Untuk menggunakan Entity Framework Core, kita perlu menginstal paket yang diperlukan. Buka terminal dan navigasikan ke direktori proyek backend Anda dan jalankan:
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.Design
Selanjutnya, konfigurasikan konteks database di `Startup.cs`:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("AuthDb")); // Ganti dengan database yang sebenarnya nanti
// Kode CORS dari sebelumnya
services.AddControllers();
}
4.2 Implementasi Endpoint Pendaftaran dan Login
Sekarang, mari kita buat controller untuk menangani pendaftaran dan login pengguna. Buat controller baru bernama `AuthController.cs`:
using Microsoft.AspNetCore.Mvc;
using Backend.Models;
using Backend.Data;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System;
using Microsoft.AspNetCore.Http;
namespace Backend.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly IConfiguration _configuration;
public AuthController(ApplicationDbContext context, IConfiguration configuration)
{
_context = context;
_configuration = configuration;
}
[HttpPost("register")]
public async Task<IActionResult> Register(User user)
{
// Validasi bahwa pengguna tidak ada
if (_context.Users.Any(u => u.Username == user.Username))
{
return BadRequest("User already exists");
}
// Hash password pengguna
string salt = GenerateSalt();
string hashedPassword = HashPassword(user.Password, salt);
// Buat entitas Pengguna baru
User newUser = new User
{
Username = user.Username,
Password = hashedPassword
};
_context.Users.Add(newUser);
await _context.SaveChangesAsync();
return Ok("User registered successfully");
}
[HttpPost("login")]
public async Task<IActionResult> Login(User user)
{
var existingUser = _context.Users.FirstOrDefault(u => u.Username == user.Username);
if (existingUser == null)
{
return BadRequest("Invalid username or password");
}
// Verifikasi password
var salt = Convert.FromBase64String(existingUser.Password.Substring(0, 24));
var hashedPassword = HashPassword(user.Password, salt);
if (existingUser.Password != hashedPassword)
{
return BadRequest("Invalid username or password");
}
// Generate JWT token
var token = GenerateJwtToken(existingUser);
var refreshToken = GenerateRefreshToken();
existingUser.RefreshToken = refreshToken;
existingUser.RefreshTokenExpiryTime = DateTime.Now.AddDays(7);
await _context.SaveChangesAsync();
SetRefreshTokenInCookie(refreshToken, 7); // 7 days expiry
return Ok(new { Token = token });
}
private string GenerateJwtToken(User user)
{
var jwtTokenHandler = new JwtSecurityTokenHandler();
var secretKey = _configuration.GetSection("Jwt:Secret").ToString();
var key = Encoding.UTF8.GetBytes(secretKey);
var authSigningKey = new SymmetricSecurityKey(key);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("Id", user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Sub, user.Username),
new Claim(JwtRegisteredClaimNames.Email, user.Username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
}),
Expires = DateTime.UtcNow.AddMinutes(10),
SigningCredentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature)
};
var token = jwtTokenHandler.CreateToken(tokenDescriptor);
return jwtTokenHandler.WriteToken(token);
}
private string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
}
return Convert.ToBase64String(randomNumber);
}
private void SetRefreshTokenInCookie(string refreshToken, int expiryInDays)
{
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = true, // Hanya kirim melalui HTTPS
SameSite = SameSiteMode.None, // Penting untuk lintas domain
Expires = DateTime.UtcNow.AddDays(expiryInDays)
};
Response.Cookies.Append("refreshToken", refreshToken, cookieOptions);
}
private string GenerateSalt()
{
byte[] salt = new byte[12];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
return Convert.ToBase64String(salt);
}
private string HashPassword(string password, string salt)
{
byte[] saltBytes = Convert.FromBase64String(salt);
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: saltBytes,
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 100000,
numBytesRequested: 256 / 8));
return salt + hashed;
}
}
}
Pastikan untuk menginstal paket yang diperlukan:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package Microsoft.AspNetCore.Cryptography.KeyDerivation
Juga tambahkan bagian Jwt ke `appsettings.json`:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Jwt": {
"Secret": "supersecretkeythatneedstobeverylong"
}
}
4.3 Implementasi Refresh Token
Endpoint `login` sudah menghasilkan refresh token dan menyimpannya dalam cookie. Sekarang kita perlu membuat endpoint untuk menggunakan refresh token dan mendapatkan token JWT baru.
Tambahkan metode berikut ke `AuthController`:
[HttpPost("refresh-token")]
public async Task<IActionResult> RefreshToken()
{
var refreshToken = Request.Cookies["refreshToken"];
if (refreshToken == null)
{
return BadRequest("Refresh Token is required");
}
var user = _context.Users.FirstOrDefault(u => u.RefreshToken == refreshToken);
if (user == null)
{
return NotFound("Invalid Refresh Token");
}
if (user.RefreshTokenExpiryTime < DateTime.Now)
{
return Unauthorized("Refresh Token has expired");
}
var newJwtToken = GenerateJwtToken(user);
var newRefreshToken = GenerateRefreshToken();
user.RefreshToken = newRefreshToken;
user.RefreshTokenExpiryTime = DateTime.Now.AddDays(7);
await _context.SaveChangesAsync();
SetRefreshTokenInCookie(newRefreshToken, 7);
return Ok(new { Token = newJwtToken });
}
4.4 Konfigurasi Cookie Aman
Dalam metode `SetRefreshTokenInCookie`, kita mengatur bendera `HttpOnly` ke `true` untuk mencegah JavaScript sisi klien mengakses cookie. Kita juga mengatur `Secure` ke `true` untuk memastikan bahwa cookie hanya dikirim melalui HTTPS. `SameSite = SameSiteMode.None` sangat penting untuk memungkinkan cookie dikirim dalam permintaan lintas domain. Ini harus dipasangkan dengan konfigurasi CORS yang tepat.
5. Implementasi Frontend Next.js
5.1 Membuat Formulir Pendaftaran dan Login
Buat komponen untuk formulir pendaftaran dan login di aplikasi Next.js Anda. Anda dapat membuat direktori baru bernama `components` dan menambahkan dua file: `RegisterForm.js` dan `LoginForm.js`.
Contoh `RegisterForm.js`:
import { useState } from 'react';
const RegisterForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('http://localhost:5000/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
mode: 'cors',
});
if (response.ok) {
alert('Registration successful!');
} else {
const errorData = await response.text();
alert(`Registration failed: ${errorData}`);
}
} catch (error) {
console.error('Error:', error);
alert('Registration failed. Check console for errors.');
}
};
return (
<form onSubmit={handleSubmit}>
<label>
Username:
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</label>
<br />
<label>
Password:
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<br />
<button type="submit">Register</button>
</form>
);
};
export default RegisterForm;
Contoh `LoginForm.js`:
import { useState } from 'react';
const LoginForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('http://localhost:5000/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
mode: 'cors',
});
if (response.ok) {
const data = await response.json();
// Token ditempatkan ke penyimpanan lokal dalam kasus ini, tapi lebih aman menggunakan cookie.
// localStorage.setItem('token', data.token);
alert('Login successful!');
} else {
const errorData = await response.text();
alert(`Login failed: ${errorData}`);
}
} catch (error) {
console.error('Error:', error);
alert('Login failed. Check console for errors.');
}
};
return (
<form onSubmit={handleSubmit}>
<label>
Username:
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</label>
<br />
<label>
Password:
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<br />
<button type="submit">Login</button>
</form>
);
};
export default LoginForm;
Pastikan untuk mengganti `’http://localhost:5000/api/auth/login’` dan `’http://localhost:5000/api/auth/register’` dengan URL backend Anda yang sebenarnya.
5.2 Menangani Permintaan API dengan `fetch` atau `axios`
Dalam contoh di atas, kita menggunakan `fetch` untuk membuat permintaan API. Anda juga dapat menggunakan `axios`, pustaka klien HTTP populer. Jika Anda memilih untuk menggunakan `axios`, Anda harus menginstalnya:
npm install axios
5.3 Menyimpan dan Mengelola Token Autentikasi dalam Cookie
Karena backend mengatur cookie `refreshToken`, kita hanya perlu mengambil token JWT setelah login yang berhasil. Dalam contoh sebelumnya, token JWT disimpan di penyimpanan lokal, ini sangat rentan terhadap serangan XSS. Kita akan mengubah ini untuk hanya bergantung pada Cookie yang disetel oleh server.
Berikut adalah fungsi yang dapat Anda gunakan untuk mengambil token JWT baru menggunakan refreshToken.
import axios from 'axios';
const refreshToken = async () => {
try {
const response = await axios.post('http://localhost:5000/api/auth/refresh-token', {}, { withCredentials: true });
if (response.status === 200) {
return response.data.token;
} else {
// Handle error
console.error('Failed to refresh token:', response.statusText);
return null;
}
} catch (error) {
console.error('Error refreshing token:', error);
return null;
}
};
export default refreshToken;
withCredentials: true – Ini penting untuk memastikan bahwa cookie dikirim dengan permintaan lintas domain.
5.4 Mengimplementasikan Perlindungan Rute
Untuk melindungi rute di aplikasi Next.js Anda, Anda dapat menggunakan wrapper komponen atau middleware. Berikut adalah contoh menggunakan wrapper komponen:
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import refreshToken from '../utils/refreshToken';
const withAuth = (WrappedComponent) => {
const Wrapper = (props) => {
const router = useRouter();
const [loading, setLoading] = useState(true);
const [jwtToken, setJwtToken] = useState(null);
useEffect(() => {
const checkAuthentication = async () => {
const token = await refreshToken();
if (!token) {
router.push('/login'); // Redirect ke halaman login jika tidak ada token
} else {
setJwtToken(token);
}
setLoading(false);
};
checkAuthentication();
}, [router]);
if (loading) {
return <p>Loading...</p>; // Tampilkan indikator loading
}
if (!jwtToken) {
return null; // Komponen tidak dirender jika tidak ada token
}
return <WrappedComponent {...props} token={jwtToken} />;
};
return Wrapper;
};
export default withAuth;
Untuk menggunakan wrapper ini, cukup bungkus komponen yang perlu dilindungi:
import withAuth from '../utils/withAuth';
const Profile = ({ token }) => {
return (
<div>
<h1>Profile Page</h1>
<p>Welcome to your profile!</p>
<p>Your token: {token}</p>
</div>
);
};
export default withAuth(Profile);
6. Keamanan dan Praktik Terbaik
6.1 Menggunakan HTTPS
Selalu gunakan HTTPS untuk aplikasi Anda untuk mengenkripsi data yang dikirim antara frontend dan backend. Ini sangat penting untuk melindungi cookie autentikasi Anda dari dicegat.
6.2 Cookie Aman dan Atribut HTTPOnly
Seperti yang disebutkan sebelumnya, atur bendera `Secure` dan `HttpOnly` pada cookie Anda. `Secure` memastikan bahwa cookie hanya dikirim melalui HTTPS, dan `HttpOnly` mencegah JavaScript sisi klien mengakses cookie, mengurangi risiko serangan XSS.
6.3 Mencegah Serangan CSRF
Untuk mencegah serangan CSRF, Anda dapat menggunakan pola token Sinkronizer (STP). Ini melibatkan pembuatan token unik sisi server yang terkait dengan sesi pengguna saat ini dan memasukkannya ke dalam formulir atau permintaan AJAX apa pun. Pada penerimaan permintaan, server memverifikasi token untuk memastikan bahwa permintaan tersebut berasal dari pengguna yang sah.
6.4 Validasi dan Sanitasi Input
Selalu validasi dan sanitasi input pengguna di kedua sisi frontend dan backend untuk mencegah serangan injeksi dan memastikan bahwa data yang disimpan di database Anda bersih dan valid.
6.5 Pembaruan dan Patch Keamanan Reguler
Selalu perbarui framework, pustaka, dan dependensi Anda ke versi terbaru untuk mendapatkan manfaat dari perbaikan bug dan patch keamanan. Pantau buletin keamanan dan pengumuman untuk setiap kerentanan yang mungkin memengaruhi aplikasi Anda.
7. Penyebaran
7.1 Konfigurasi untuk Lingkungan Produksi
Saat menyebarkan aplikasi Anda ke produksi, pastikan Anda telah mengonfigurasi CORS dengan benar untuk domain produksi Anda. Atur variabel lingkungan yang sesuai, seperti string koneksi database dan kunci API. Pertimbangkan untuk menggunakan variabel lingkungan untuk mengelola konfigurasi yang berbeda untuk lingkungan pengembangan dan produksi Anda.
7.2 Pertimbangan untuk Skalabilitas
Jika Anda berencana untuk menskalakan aplikasi Anda, pertimbangkan untuk menggunakan database terdistribusi dan solusi caching. Pastikan bahwa sistem autentikasi Anda dapat menangani banyak permintaan secara efisien. Pertimbangkan untuk menggunakan penyedia identitas pihak ketiga seperti Auth0 atau Firebase Authentication untuk mendelegasikan autentikasi dan pengelolaan pengguna.
8. Kesimpulan
Menerapkan autentikasi full-stack yang aman dengan Next.js dan ASP.NET Core melalui cookie lintas domain memerlukan pertimbangan yang cermat untuk berbagai tantangan keamanan. Dengan mengikuti praktik terbaik yang diuraikan dalam postingan blog ini, Anda dapat membangun sistem autentikasi yang kuat dan aman yang melindungi aplikasi dan data pengguna Anda. Ingatlah untuk selalu memprioritaskan keamanan dan terus perbarui aplikasi Anda dengan patch dan pembaruan keamanan terbaru.
“`