Parte 4 — JWT Authentication
Pasul 7 — Configurare JWT
appsettings.json — adăugați secțiunea Jwt
Nu rescrieți fișierul. Păstrați
ConnectionStrings,Logging,AllowedHostsașa cum sunt. Adăugați doar bloculJwt, la nivelul rădăcinii JSON-ului (frați cuConnectionStrings):
"Jwt": {
"Key": "SuperSecretKeyThatIsAtLeast32BytesLong!!!!",
"Issuer": "NewsPortal",
"Audience": "NewsPortalUsers",
"ExpiresInMinutes": 60
}
Rezultatul final (exemplu pe Lab08_start):
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Lab08_start;..."
},
"Jwt": {
"Key": "SuperSecretKeyThatIsAtLeast32BytesLong!!!!",
"Issuer": "NewsPortal",
"Audience": "NewsPortalUsers",
"ExpiresInMinutes": 60
}
}
Cheia trebuie să aibă minim 32 bytes (256 bits) pentru HMAC-SHA256. Dacă e mai scurtă,
SymmetricSecurityKeyaruncă o excepție la startup.
Atenție în producție: cheia nu se ține în
appsettings.jsonurcat pe git. În dezvoltare e ok. În producție — user secrets, variable de mediu sau Azure Key Vault / Vault / etc.
Controllers/Api/AuthApiController.cs — fișier nou
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Lab08.DTOs;
using Lab08.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace Lab08.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class AuthApiController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IConfiguration _configuration;
public AuthApiController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IConfiguration configuration)
{
_userManager = userManager;
_signInManager = signInManager;
_configuration = configuration;
}
// POST: /api/authapi/login
[HttpPost("login")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Login(LoginDto dto)
{
var user = await _userManager.FindByEmailAsync(dto.Email);
if (user == null)
return Unauthorized(new { message = "Email sau parolă incorectă" });
// IMPORTANT: CheckPasswordSignInAsync — NU setează cookie, doar verifică parola
var result = await _signInManager.CheckPasswordSignInAsync(user, dto.Password, lockoutOnFailure: false);
if (!result.Succeeded)
return Unauthorized(new { message = "Email sau parolă incorectă" });
var token = await GenerateJwtAsync(user);
return Ok(new { token, expiresIn = _configuration.GetValue<int>("Jwt:ExpiresInMinutes") * 60 });
}
private async Task<string> GenerateJwtAsync(ApplicationUser user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Name, user.UserName!),
new(ClaimTypes.Email, user.Email!)
};
// Adaugă rolurile ca și claims — pentru [Authorize(Roles = "Admin")]
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
claims.Add(new Claim(ClaimTypes.Role, role));
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(
_configuration.GetValue<int>("Jwt:ExpiresInMinutes")),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
Ce face CheckPasswordSignInAsync vs PasswordSignInAsync:
| Metodă | Efect |
|---|---|
PasswordSignInAsync |
Verifică parola și setează cookie-ul de sesiune |
CheckPasswordSignInAsync |
Doar verifică parola — nu setează nimic |
Pentru API vrem varianta a doua — nu ne interesează cookies, generăm manual JWT-ul.
Configurare JWT în Program.cs
Adăugați using-urile în capul fișierului:
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
Și după blocul AddIdentity (care rămâne neschimbat), adăugați schema JWT:
// JWT Bearer — pentru API
builder.Services.AddAuthentication()
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
Atenție —
AddAuthentication()fără argument! Identity a setat deja schema default (Cookies). Dacă dațiAddAuthentication(JwtBearerDefaults.AuthenticationScheme), anulați Identity și MVC-ul încetează să mai meargă.
[Authorize] pe API cu schema JWT
Atributul [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] l-am pus deja pe POST, PUT, DELETE la Pasul 5. Acum, că schema JWT e înregistrată în Program.cs, endpoint-urile protejate vor accepta token în header-ul Authorization: Bearer <token> și vor întoarce 401 când lipsește.
GET-urile (GetAll, GetById) rămân fără [Authorize] — lectură publică.
Alternativă stilistică — atribut pe clasă: dacă toate endpoint-urile protejate folosesc aceeași schemă, puteți scoate [Authorize(...)] de pe fiecare acțiune și să-l puneți o singură dată pe clasă, cu [AllowAnonymous] pe acțiunile publice:
[ApiController]
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class ArticlesApiController : ControllerBase
{
[HttpGet]
[AllowAnonymous]
public async Task<ActionResult<List<ArticleDto>>> GetAll(...) { ... }
// POST, PUT, DELETE — moștenesc [Authorize] de pe clasă
}
Pentru lab rămânem pe varianta per-acțiune din Pasul 5 — mai explicit, mai ușor de urmărit.
Pasul 8 — JWT Deep Dive + jwt.io
Structura unui JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiSm9obiJ9.SflKxwRJSMeKKF2Q...
└─────────── Header ───────────┘.└──────── Payload ────────┘.└──── Signature ────┘
Trei părți, separate prin ., fiecare Base64Url-encoded:
- Header —
{ "alg": "HS256", "typ": "JWT" }— algoritmul de semnare - Payload — claim-urile —
sub,name,email,role,exp,iss,aud - Signature —
HMAC-SHA256(base64(header) + "." + base64(payload), secretKey)
Întrebări cheie
E JWT-ul criptat? Nu. E semnat. Base64 nu e criptare — oricine poate decoda payload-ul. Deschideți jwt.io, lipiți token-ul, vedeți payload-ul în clar. Semnătura garantează că nu a fost modificat — dacă cineva schimbă un claim, signature-ul nu mai corespunde și serverul respinge token-ul.
Ce nu punem niciodată în JWT? Parole, date sensibile, numere de card, CNP-uri. Payload-ul e public.
HMAC-SHA256 e simetric sau asimetric? Simetric — aceeași cheie la semnare și la verificare. Funcționează aici pentru că un singur server generează și verifică. Dacă am avea mai multe servicii care verifică (dar doar unul semnează), am folosi RS256 (asimetric — cheie privată pentru semnare, publică pentru verificare).
Unde e stocat token-ul pe client?
localStorage,sessionStoragesau cookieHttpOnly. Fiecare cu trade-off-urile lui legate de XSS și CSRF.localStoragee cel mai comun în SPA-uri (Angular, React).
Ce face clientul când expiră token-ul? Primește
401, redirecționează la login sau folosește un refresh token (pattern avansat — nu la acest lab).
Testare end-to-end în Postman
- Creez collection „News Portal API"
- POST
/api/authapi/login— body JSON:Primesc:{ "email": "admin@newsportal.com", "password": "Admin@123" }{ "token": "eyJhbGciOi...", "expiresIn": 3600 } - Copiez token-ul și îl lipesc pe jwt.io:
- Header:
{ "alg": "HS256", "typ": "JWT" } - Payload: claims (sub, name, email, role, exp, iss, aud)
- Signature: marcată roșie până lipesc și cheia secretă
- Header:
- GET
/api/articlesapi— funcționează fără token (read public). - POST
/api/articlesapifără token → 401 Unauthorized. - POST
/api/articlesapicu headerAuthorization: Bearer <token>→ 201 Created. - PUT
/api/articlesapi/1ca alt user → 403 Forbidden (content ownership).
Tip Postman: la Collection level, în tab-ul „Variables", definiți {{token}}. Apoi fiecare request autentificat folosește Authorization: Bearer {{token}}. După login, copiați token-ul în variabilă o singură dată.