Saya menjawab pertanyaan ini: Bagaimana cara mengamankan API Web ASP.NET 4 tahun yang lalu menggunakan HMAC.
Sekarang, banyak hal berubah dalam keamanan, terutama JWT semakin populer. Di sini, saya akan mencoba menjelaskan cara menggunakan JWT dengan cara paling sederhana dan dasar yang saya bisa, jadi kita tidak akan tersesat dari hutan OWIN, Oauth2, ASP.NET Identity ... :).
Jika Anda tidak tahu token JWT, Anda perlu melihatnya sedikit di:
https://tools.ietf.org/html/rfc7519
Pada dasarnya, token JWT terlihat seperti:
<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>
Contoh:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ
Token JWT memiliki tiga bagian:
- Header: Format JSON yang disandikan dalam Base64
- Klaim: Format JSON yang disandikan di Base64.
- Tanda tangan: Dibuat dan ditandatangani berdasarkan Header dan Klaim yang disandikan di Base64.
Jika Anda menggunakan situs web jwt.io dengan token di atas, Anda dapat mendekode token dan melihatnya seperti di bawah ini:
Secara teknis, JWT menggunakan tanda tangan yang ditandatangani dari header dan klaim dengan algoritma keamanan yang ditentukan dalam header (contoh: HMACSHA256). Oleh karena itu, JWT harus ditransfer melalui HTTP jika Anda menyimpan informasi sensitif apa pun dalam klaim.
Sekarang, untuk menggunakan otentikasi JWT, Anda tidak benar-benar membutuhkan middleware OWIN jika Anda memiliki sistem Web Api yang lama. Konsep sederhananya adalah bagaimana menyediakan token JWT dan bagaimana memvalidasi token ketika permintaan datang. Itu dia.
Kembali ke demo, agar token JWT tetap ringan, saya hanya menyimpan username
dan expiration time
di JWT. Tetapi dengan cara ini, Anda harus membangun kembali identitas lokal baru (kepala sekolah) untuk menambahkan lebih banyak informasi seperti: peran .. jika Anda ingin melakukan otorisasi peran. Tetapi, jika Anda ingin menambahkan lebih banyak informasi ke JWT, terserah Anda: sangat fleksibel.
Alih-alih menggunakan middleware OWIN, Anda cukup memberikan titik akhir JWT token dengan menggunakan aksi dari controller:
public class TokenController : ApiController
{
// This is naive endpoint for demo, it should use Basic authentication
// to provide token or POST request
[AllowAnonymous]
public string Get(string username, string password)
{
if (CheckUser(username, password))
{
return JwtManager.GenerateToken(username);
}
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
public bool CheckUser(string username, string password)
{
// should check in the database
return true;
}
}
Ini adalah tindakan naif; dalam produksi, Anda harus menggunakan permintaan POST atau titik akhir Otentikasi Dasar untuk memberikan token JWT.
Bagaimana cara menghasilkan token username
?
Anda dapat menggunakan paket NuGet yang dipanggil System.IdentityModel.Tokens.Jwt
dari Microsoft untuk menghasilkan token, atau bahkan paket lain jika Anda mau. Dalam demo, saya gunakan HMACSHA256
dengan SymmetricKey
:
/// <summary>
/// Use the below code to generate symmetric Secret Key
/// var hmac = new HMACSHA256();
/// var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";
public static string GenerateToken(string username, int expireMinutes = 20)
{
var symmetricKey = Convert.FromBase64String(Secret);
var tokenHandler = new JwtSecurityTokenHandler();
var now = DateTime.UtcNow;
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
}),
Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(symmetricKey),
SecurityAlgorithms.HmacSha256Signature)
};
var stoken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(stoken);
return token;
}
Titik akhir untuk memberikan token JWT telah selesai. Sekarang, bagaimana memvalidasi JWT ketika permintaan datang? Dalam demo saya telah membangun
JwtAuthenticationAttribute
yang mewarisi dari IAuthenticationFilter
(lebih detail tentang filter otentikasi di sini ).
Dengan atribut ini, Anda dapat mengautentikasi tindakan apa pun: Anda hanya perlu menempatkan atribut ini pada tindakan itu.
public class ValueController : ApiController
{
[JwtAuthentication]
public string Get()
{
return "value";
}
}
Anda juga dapat menggunakan middleware OWIN atau DelegateHander jika Anda ingin memvalidasi semua permintaan yang masuk untuk WebAPI Anda (tidak khusus untuk Kontroler atau tindakan)
Di bawah ini adalah metode inti dari filter otentikasi:
private static bool ValidateToken(string token, out string username)
{
username = null;
var simplePrinciple = JwtManager.GetPrincipal(token);
var identity = simplePrinciple.Identity as ClaimsIdentity;
if (identity == null)
return false;
if (!identity.IsAuthenticated)
return false;
var usernameClaim = identity.FindFirst(ClaimTypes.Name);
username = usernameClaim?.Value;
if (string.IsNullOrEmpty(username))
return false;
// More validate to check whether username exists in system
return true;
}
protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
string username;
if (ValidateToken(token, out username))
{
// based on username to get more information from database
// in order to build local identity
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, username)
// Add more claims if needed: Roles, ...
};
var identity = new ClaimsIdentity(claims, "Jwt");
IPrincipal user = new ClaimsPrincipal(identity);
return Task.FromResult(user);
}
return Task.FromResult<IPrincipal>(null);
}
Alur kerjanya adalah, menggunakan perpustakaan JWT (paket NuGet di atas) untuk memvalidasi token JWT dan kemudian kembali ClaimsPrincipal
. Anda dapat melakukan lebih banyak validasi seperti memeriksa apakah pengguna ada di sistem Anda dan menambahkan validasi khusus lainnya jika Anda mau. Kode untuk memvalidasi token JWT dan mendapatkan kembali pokok:
public static ClaimsPrincipal GetPrincipal(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;
if (jwtToken == null)
return null;
var symmetricKey = Convert.FromBase64String(Secret);
var validationParameters = new TokenValidationParameters()
{
RequireExpirationTime = true,
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
};
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);
return principal;
}
catch (Exception)
{
//should write log
return null;
}
}
Jika token JWT divalidasi dan kepala sekolah kembali, Anda harus membangun identitas lokal baru dan memasukkan lebih banyak informasi ke dalamnya untuk memeriksa otorisasi peran.
Ingatlah untuk menambahkan config.Filters.Add(new AuthorizeAttribute());
(otorisasi default) pada lingkup global untuk mencegah permintaan anonim ke sumber daya Anda.
Anda dapat menggunakan tukang pos untuk menguji demo:
Permintaan token (naif seperti yang saya sebutkan di atas, hanya untuk demo):
GET http://localhost:{port}/api/token?username=cuong&password=1
Masukkan token JWT di header untuk permintaan resmi, contoh:
GET http://localhost:{port}/api/value
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s
Demo diletakkan di sini: https://github.com/cuongle/WebApi.Jwt