# RESTful arhitektura (Web API) Ovaj materijal je dio Ishoda 1 (željeno). ## 6 JSON Web Token (JWT) JWT predstavlja tvrdnje (engl. claims) koji se prenose između klijenta i poslužitelja. To je kodirani skup brojeva i slova koji dokazuje da je korisnik onaj za kojeg se predstavlja. JWT token možete smatrati oblikom prijave. JWT konfiguracija u obliku SecurityTokenDescriptor-a: - https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.securitytokendescriptor?view=msal-web-dotnet-latest JWT debuger: - https://jwt.io/ Korištenjem podrške za JWT u ASP.NET Core, možete: - Zaustaviti neželjeni pristup resursima (kontrolorima i akcijama) u vašem projektu - Dopustiti pristup sigurnim resursima samo registriranim korisnicima --- Da biste zaustavili neželjeni pristup resursima u vašem projektu, morate: - Instalirati NuGet pakete za JWT - Koristiti međuprogram (engl. middleware) u `Program.cs` za postavljanje i konfiguraciju JWT sigurnosti - Koristiti atribut `[Authorize]` na kontrolerima ili akcijama koje želite osigurati Da biste omogućili pristup sigurnim resursima u vašem projektu, morate: - Stvoriti i vratiti JWT token klijentu u nezaštićenoj krajnjoj točki - Koristiti taj token u zaglavlju autorizacije kada izdajete zahtjeve zaštićenoj krajnjoj točki - Dodatno: postaviti podršku za JWT u Swaggeru Da biste omogućili pristup sigurnim resursima u vašem projektu samo registriranim korisnicima, morate: - Podržati registraciju korisnika - Stvoriti i vratiti JWT token za pojedinog korisnika - To se može smatrati izvođenjem prijave korisnika ### 6.1 Osiguranje kontrolera i akcija 1. Trebate instalirati sljedeće pakete u projekt: ``` dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8 dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect --version 8 ``` 2. U Program.cs, postavite međuprogram (engl. middleware) za konfiguraciju JWT sigurnosti. Ne zaboravite dodati usluge prije izgradnje web aplikacije (´var app = builder.Build()´), i međuprogram nakon toga. ```C# // Configure JWT security services var secureKey = "12345678901234567890123456789012"; builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(o => { var Key = Encoding.UTF8.GetBytes(secureKey); o.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, ValidateAudience = false, IssuerSigningKey = new SymmetricSecurityKey(Key) }; }); ``` ``` // Use authentication / authorization middleware app.UseAuthentication(); app.UseAuthorization(); // -> this should already be present ``` > Ako pregledate kod, možete vidjeti da se za autentifikaciju JWT tokena koristi sigurni ključ. Ovaj ključ je nešto što je poznato samo vašem poslužitelju i ne smije se dijeliti ni s kim drugim. Također, ne bi trebao biti hardkodiran. Kasnije ćemo premjestiti ključ u konfiguraciju. 3. Stvorite Web API kontroler akciju za čitanje/pisanje pod nazivom `SecuredController`. Dodajte atribut `[Authorize]` prije prve radnje `Get()`. Isprobajte kontroler u Swaggeru. > Zaštićena radnja vratit će `Error: Unauthorized` jer je osigurana atributom `[Authorize]`. Neosigurana radnja vratit će podatke. 4. Sada označite cijeli kontroler atributom `[Authorize]`. Isprobajte kontroler u Swaggeru. > Sve radnje osiguranog kontrolera vratit će `Error: Unauthorized` zbog osiguranja atributom `[Authorize]`. ### 6.2 Dozvoljavanje pristupa sigurnim resursima 1. Napravite Web API kontroler (prazan) `UserController` koji će podržavati stvaranje JWT tokena. 2. Dodajte novu mapu pod nazivom `Security`, i stvorite klasu `JwtTokenProvider` u toj mapi. ```C# public class JwtTokenProvider { public static string CreateToken(string secureKey, int expiration, string subject = null) { // Get secret key bytes var tokenKey = Encoding.UTF8.GetBytes(secureKey); // Create a token descriptor (represents a token, kind of a "template" for token) var tokenDescriptor = new SecurityTokenDescriptor { Expires = DateTime.UtcNow.AddMinutes(expiration), SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(tokenKey), SecurityAlgorithms.HmacSha256Signature) }; if (!string.IsNullOrEmpty(subject)) { tokenDescriptor.Subject = new ClaimsIdentity(new System.Security.Claims.Claim[] { new System.Security.Claims.Claim(ClaimTypes.Name, subject), new System.Security.Claims.Claim(JwtRegisteredClaimNames.Sub, subject), }); } // Create token using that descriptor, serialize it and return it var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(tokenDescriptor); var serializedToken = tokenHandler.WriteToken(token); return serializedToken; } } ``` Koristit ćete ovu klasu za stvaranje JWT tokena pomoću sigurnog ključa. > Napomena: Možete koristiti tu klasu u svom projektu i mijenjati je na način koji želite. Imajte na umu da `SecurityTokenDescriptor` dopušta dodatne parametre. Slobodno istražite ove parametre. 2. Stvorite akciju `GetToken()` koja vraća JWT token ```C# [HttpGet("[action]")] public ActionResult GetToken() { try { // The same secure key must be used here to create JWT, // as the one that is used by middleware to verify JWT var secureKey = "12345678901234567890123456789012"; var serializedToken = JwtTokenProvider.CreateToken(secureKey, 10); return Ok(serializedToken); } catch (Exception ex) { return StatusCode(500, ex.Message); } } ``` Testirajte ovu akciju u Swaggeru. > Koristite https://jwt.io/ da biste vidjeli sadržaj tokena. Postoji poveznica `Debugger` i tekstualni okvir za unos tokena. Sadržaj tokena možete vidjeti na desnoj strani. 3. Premjestite sigurnosni ključ u konfiguraciju. U `appsettings.json` stvorite sekciju `JWT` i u tu sekciju dodajte `SecureKey` koji Vam je potreban i u međuprogramu i tamo gdje stvarate JWT. ```JSON "JWT": { "SecureKey": "12345678901234567890123456789012" } ``` > U stvarnosti, kada koristite JWT vjerojatno će vam trebati sigurniji ključ kao što je, na primjer `E(H+MbQeThWmZq4t6w9z$C&F)J@NcRfU` :) 4. Za čitanje konfiguracije u vašem `UserControlleru` (gdje vam je potrebna), morate dopustiti DI spremniku da je proslijedi konstruktoru vašeg kontrolera: ```C# private readonly IConfiguration _configuration; public UserController(IConfiguration configuration) { _configuration = configuration; } ``` 5. Sada pročitajte ključ pomoću konfiguracije umjesto pomoću hardkodirane vrijednosti ```C# var secureKey = _configuration["JWT:SecureKey"]; var serializedToken = JwtTokenProvider.CreateToken(secureKey, 10); ``` 6. Da biste pročitali konfiguraciju u `Program.cs` (gdje vam je također potrebna), jednostavno je pročitajte izravno iz "app buildera": ```C# var secureKey = builder.Configuration["JWT:SecureKey"]; ``` 7. Kako biste omogućili Swaggeru da prihvati JWT token, trebate konfigurirati njegovo korisničko sučelje pomoću konfiguracije usluge Swagger u `Program.cs` ```C# builder.Services.AddSwaggerGen(option => { option.SwaggerDoc("v1", new OpenApiInfo { Title = "RWA Web API", Version = "v1" }); option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter valid JWT", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement( new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new List() } }); }); ``` 8. Sada testirajte JWT u Swaggeru - Koristite radnju `GetToken()` da biste dobili JWT - Koristite gumb `Authorize` na vrhu stranice da biste spremili JWT u preglednik - Sada isprobajte zaštićenu krajnju točku > Napomena: JWT se šalje pomoću zaglavlja `Authorization` u HTTP zahtjevu. To možete primijetiti ispitivanjem zaglavlja zahtjeva u Development Tools alatu preglednika ### 6.3 Podrška za registraciju korisnika Ne želite izdati JWT nepoznatom korisniku jer svaki korisnik s JWT-om može imati pristup vašim osiguranim resursima. To znači da trebate imati popis svojih registriranih korisnika (čitaj: tablicu baze podataka) s lozinkama. Važno je imati lozinku u bazi podataka ne kao čisti tekst, već kao kriptografski hash te lozinke. Kada se korisnik pokuša prijaviti, lozinka koju unese uspoređivat će se s kriptografskim hashom i ako vrijednosti međusobno odgovaraju, korisniku će biti dopušteno dobivanje JWT-a. Dakle, prvo podržavamo EF bazu podataka u našem projektu. To je ista stvar koju smo radili u vježbama 4. 1. Kreirajmo bazu podataka `Vježba6` i tablicu za korisnike ```SQL CREATE TABLE [dbo].[User]( [Id] [int] IDENTITY(1,1) NOT NULL, [Username] [nvarchar](50) NOT NULL, -- Use this for login [PwdHash] [nvarchar](256) NOT NULL, -- Use to check password hash [PwdSalt] [nvarchar](256) NOT NULL, -- Additional level of security (random string) [FirstName] [nvarchar](256) NOT NULL, [LastName] [nvarchar](256) NOT NULL, [Email] [nvarchar](256) NOT NULL, [Phone] [nvarchar](256) NULL, CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ( [Id] ASC ) ) ``` 2. Upotrijebimo `Package Manager Console` da instaliramo podršku za bazu podataka i konfiguriramo je. **Obratite pozornost na pravilnu promjenu connection stringa.** ``` dotnet tool install --global dotnet-ef --version 8 dotnet add package Microsoft.EntityFrameworkCore --version 8 dotnet add package Microsoft.EntityFrameworkCore.Design --version 8 dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8 dotnet ef dbcontext scaffold "server=.;Database=Exercise6;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=true" Microsoft.EntityFrameworkCore.SqlServer -o Models --force ``` 3. Izrežite/zalijepite connection string iz generiranog konteksta baze podataka `Exercise6Context` u `appsettings.json`: ```JSON "ConnectionStrings": { "ex6cs": "{ovdje-ide-zalijepljeni-connection-string}" } ``` 4. U `Exercise6Context.cs` trebali biste imati ovo: ```C# protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer("name=ConnectionStrings:ex6cs"); ``` 5. U `Program.cs` trebate dodati ovo: ```C# builder.Services.AddDbContext(options => { options.UseSqlServer("name=ConnectionStrings:ex6cs"); }); ``` 6. Sada možemo podržati registraciju korisnika u `UserController.cs`. Prvo koristimo DI da dobijemo kontekst baze podataka. ```C# private readonly IConfiguration _configuration; private readonly Exercise6Context _context; public UserController(IConfiguration configuration, Exercise6Context context) { _configuration = configuration; _context = context; } ``` 7. Trebamo DTO klasu za korisnika, pa kreiramo mapu `Dtos` i unutra klasu `UserDto`: ```C# public class UserDto { public int Id { get; set; } [Required(ErrorMessage = "User name is required")] public string Username { get; set; } [Required(ErrorMessage = "Password is required")] [StringLength(256, MinimumLength = 8, ErrorMessage = "Password should be at least 8 characters long")] public string Password { get; set; } [Required(ErrorMessage = "First name is required")] [StringLength(50, MinimumLength = 2, ErrorMessage = "First name should be between 2 and 50 characters long")] public string FirstName { get; set; } [Required(ErrorMessage = "Last name is required")] [StringLength(50, MinimumLength = 2, ErrorMessage = "Last name should be between 2 and 50 characters long")] public string LastName { get; set; } [EmailAddress(ErrorMessage = "Provide a correct e-mail address")] public string Email { get; set; } [Phone(ErrorMessage = "Provide a correct phone number")] public string Phone { get; set; } } ``` 8. Kako bismo podržali kriptografsku operaciju stvaranja salt i hash vrijednosti, stvorit ćemo pomoćnu klasu `PasswordHashProvider` u mapi `Security`: ```C# public class PasswordHashProvider { public static string GetSalt() { byte[] salt = RandomNumberGenerator.GetBytes(128 / 8); // divide by 8 to convert bits to bytes string b64Salt = Convert.ToBase64String(salt); return b64Salt; } public static string GetHash(string password, string b64salt) { byte[] salt = Convert.FromBase64String(b64salt); byte[] hash = KeyDerivation.Pbkdf2( password: password, salt: salt, prf: KeyDerivationPrf.HMACSHA256, iterationCount: 100000, numBytesRequested: 256 / 8); string b64Hash = Convert.ToBase64String(hash); return b64Hash; } } ``` 9. Sada možemo registrirati korisnika koristeći kreirani DTO i pomoćnu kriptografsku klasu. Imajte na umu da pazimo da izbjegnemo dvostruku registraciju korisnika tako da provjerimo postojeće korisničko ime. ```C# [HttpPost("[action]")] public ActionResult Register(UserDto userDto) { try { // Check if there is such a username in the database already var trimmedUsername = userDto.Username.Trim(); if (_context.Users.Any(x => x.Username.Equals(trimmedUsername))) return BadRequest($"Username {trimmedUsername} already exists"); // Hash the password var b64salt = PasswordHashProvider.GetSalt(); var b64hash = PasswordHashProvider.GetHash(userDto.Password, b64salt); // Create user from DTO and hashed password var user = new User { Id = userDto.Id, Username = userDto.Username, PwdHash = b64hash, PwdSalt = b64salt, FirstName = userDto.FirstName, LastName = userDto.LastName, Email = userDto.Email, Phone = userDto.Phone, }; // Add user and save changes to database _context.Add(user); _context.SaveChanges(); // Update DTO Id to return it to the client userDto.Id = user.Id; return Ok(userDto); } catch (Exception ex) { return StatusCode(500, ex.Message); } } ``` > Napomena: krajnja točka `Registracija` namijenjena je za samoregistraciju. > Ako osigurate krajnju točku `Registracija`, registracija korisnicima neće biti dopuštena. 10. Sada možete testirati registraciju korisnika pomoću Swaggera i nove krajnje točke `Register()`. JSON DTO primjer: ```JSON { "username": "johnny1234", "password": "qwertzuiop", "firstName": "John", "lastName": "Smith", "email": "johnsmith1234@example.com", "phone": "0987654321" } ``` ### 6.4 Podrška za prijavu korisnika Kao što je već spomenuto, proces prijave uključuje provjeru postoji li korisnik i ako postoji, dobivanje tokena. Token je obično personaliziran, što znači da sadrži tvrdnje (engl. claims) o korisniku (korisničko ime, uloga, itd...) Nakon prijave, korisnik dobiva token kojeg će poslati natrag poslužitelju kada pristupa zaštićenoj krajnjoj točki. > Napomena: da bi ovo radilo, morate stvoriti klasu `UserLoginDto` ```C# [HttpPost("[action]")] public ActionResult Login(UserLoginDto userDto) { try { var genericLoginFail = "Incorrect username or password"; // Try to get a user from database var existingUser = _context.Users.FirstOrDefault(x => x.Username == userDto.Username); if (existingUser == null) return BadRequest(genericLoginFail); // Check is password hash matches var b64hash = PasswordHashProvider.GetHash(userDto.Password, existingUser.PwdSalt); if(b64hash != existingUser.PwdHash) return BadRequest(genericLoginFail); // Create and return JWT token var secureKey = _configuration["JWT:SecureKey"]; var serializedToken = JwtTokenProvider.CreateToken(secureKey, 120, userDto.Username); return Ok(serializedToken); } catch (Exception ex) { return StatusCode(500, ex.Message); } } ``` > Ako osigurate `Login` krajnju točku, korisnicima neće biti dopuštena prijava. > Koristite https://jwt.io/ da vidite sadržaj tokena. Sada možete vidjeti korisnikovo ime. Također, kada korisnik izvrši autentificirani zahtjev, međuprogram se brine o sadržaju JWT-a i može ga pružiti kada vam je potreban, zajedno s imenom i drugim podacima koji su u JWT-u. ### 6.5 Middleware podrška za JWT podatke Middleware brine o identitetu korisnika proslijeđenom putem JWT-a. Prosljeđuje korisničke podatke putem `HttpContext.User.Identity`, otkuda ih možete dohvatiti. ```C# // Just an example in SecuredController [HttpGet] public ActionResult Get() { var identity = HttpContext.User.Identity as ClaimsIdentity; return identity.FindFirst(ClaimTypes.Name).Value; } ``` ### 6.6 Podrška za uloge Možete podržati kontrolu pristupa temeljenu na ulogama putem JWT-a. Za resurse koje je potrebno zaštititi ulogama, upotrijebite isti atribut `Authorize` i dodajte mu uloge. ```C# [Authorize(Roles = "Admin")] ``` ```C# [Authorize(Roles = "Admin,User")] ``` Da bi to funkcioniralo, trebate dodati tvrdnju `Role` prilikom generiranja tokena. ```C# var role = "Admin" // for example... tokenDescriptor.Subject = new ClaimsIdentity(new System.Security.Claims.Claim[] { new System.Security.Claims.Claim(ClaimTypes.Name, subject), new System.Security.Claims.Claim(JwtRegisteredClaimNames.Sub, subject), new System.Security.Claims.Claim(ClaimTypes.Role, role), }); ``` ### 6.7 Vježba: Stvorite ASP.NET Web API projekt sa zaštićenim kontrolerom 1. Stvorite novi Web API projekt koji koristi istu bazu podataka `Exercise6`. 2. Stvorite podršku u bazi podataka za entitete `User`, `Product`, `Receipt` i `ReceiptItem` i povežite projekt sa svojom bazom podataka. Atributi entiteta `Product`: - Id (int) - Title (string) - Price (decimal) Atributi entiteta `Receipt`: - Id (int) - Code (string) - Total (decimal) - IssuedAt (DateTime) Atributi entiteta `ReceiptItem`: - Id (int) - ProductId (int) - ReceiptId (int) - Quantity (int) - Price (decimal) - Value (decimal) > Napomena: pazite na pravilno postavljanje stranih ključeva. 3. Napravite kontroler za registraciju i prijavu korisnika (dobivanje JWT-a). 4. Postavite JWT podršku u Swaggeru. 5. Napravite CRUD kontroler za entitet `Product` i CRUD kontroler za entitet `Receipt` 6. Koristite entitete `ReceiptItem` kada dohvaćate/pohranjujete entitet `Product` - dodajte zbirku `ReceiptItems` klasi entiteta `Product` 7. Osigurajte `Receipt` CRUD kontroler. ### 6.8 Vježba: Promjena lozinke Za prethodni zadatak dodajte krajnju točku `ChangePassword` koja korisniku omogućuje promjenu lozinke. ### 6.9 Vježba: Podrška za uloge Za prethodni zadatak, podržite entitet uloge (`Roles`) u svojoj bazi podataka i članstvo korisnika u pojedinoj ulozi. Uloge bi trebale biti `Admin` i `User`. Članstvo u `Admin` ulozi treba omogućiti čitanje i pisanje svih entiteta. Članstvo u `User` ulozi omogućuje čitanje proizvoda. Prilikom samoregistracije korisnika, postavite njegovu ulogu na `Korisnik`. Dodajte zaštićenu krajnju točku `PromoteUser` koja omogućuje promicanje registriranog korisnika u administratora.