# MVC arhitektura
Autentifikacija i autorizacija dio su ishoda učenja 3 (željeno).
## 12 Pregled
- Autentifikacija i autorizacija:
- https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-7.0#reacting-to-back-end-changes
- https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-7.0
### 12.1 Postavljanje vježbe
**Postavljanje SQL poslužitelja**
U SQL Server Management Studiju učinite sljedeće:
- preuzmite skriptu: https://pastebin.com/jtJfak9E
- u skripti **promijenite naziv baze podataka u Exercise12 i koristite tu bazu**
- izvršite je da biste stvorili bazu podataka, njezinu strukturu i neke testne podatke
**Starter projekt**
> Sljedeće je već dovršeno kao starter projekt:
>
> - Postavljeni modeli i repozitorij
> - Podešen "Launch settings"
> - Stvoreni osnovni CRUD prikazi i funkcionalnost (Genre, Artist, Song)
> - Implementirana validacija i označavanje korištenjem viewmodela
>
> Za detalje pogledajte prethodne vježbe.
Raspakirajte starter arhivu i otvorite rješenje u Visual Studiju.
Postavite connection string i pokrenite aplikaciju.
Provjerite radi li aplikacija (npr. navigacija, popis pjesama, dodavanje nove pjesme).
> U slučaju da ne radi, provjerite jeste li ispravno slijedili upute.
### 12.2 Dodajte usluge za autentifikaciju pomoću kolačića i odgovarajući međuprogram
Preduvjet za implementaciju MVC autentifikacije je dodavanje usluga i međuprograma za autentifikaciju putem kolačića. Autentifikacija putem kolačića način je provjere autentičnosti korisnika u Vašoj MVC aplikaciji.
Izvršite korake iz poglavlja "Add cookie authentication" koje se nalazi na poveznici:
- https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-7.0#add-cookie-authentication
U detalje:
- Dodajte uslugu autentifikacije
- Dodajte autentifikacijski i autorizacijski međuprogram(autentifikacijski već postoji u predlošku projekta)
- Označite `ArtistController` i `GenreController` atributom `[Authorize]` i pogledajte što se događa kada pokušate otvoriti te stranice u navigaciji.
Pogledajte URL.
> Možete promatrati URL preusmjeravanje koje se događa jer korisnik nije autentificiran.
- Promijenite postavke međuprograma:
```C#
builder.Services.AddAuthentication()
.AddCookie(options =>
{
options.LoginPath = "/User/Login";
options.LogoutPath = "/User/Logout";
options.AccessDeniedPath = "/User/Forbidden";
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
})
```
- pogledajte što se sada događa kada kliknete na `Artists` u navigaciji - pogledajte URL
> Sada se mijenja URL za preusmjeravanje jer smo dali prilagođene veze umjesto zadanih.
Bilješke:
- Ne trebate `app.MapRazorPages();` međuprogram jer ne koristimo "Razor stranice"
- Kada međuprogram otkrije da treba preusmjeriti na prijavu, zadani LoginPath je `/Account/Login`
- Kada međuprogram otkrije da treba preusmjeriti na odjavu, zadana putanja za odjavu je `/Account/Logout`
- Kada međuprogram otkrije da treba preusmjeriti pristup zabranjenoj stranici, zadani AccessDeniedPath je `/Account/AccessDenied`
- Ovisno o vašim krajnjim točkama autentifikacije, možete promijeniti ove zadane postavke, kao što ste i učinili
- Pogledajte dokumentaciju za ostale postavke (`ExpireTimeSpan`, `SlidingExpiration`)
### 12.3 Stvorite autentifikacijski kolačić
Ovaj se odjeljak temelji na sljedećem sadržaju:
- https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-7.0#create-an-authentication-cookie
Izvedite korake:
- Stvorite prazan `UserController`
- Stvorite akciju `Login` u kontroleru `UserController` i proslijedite joj parametar `string returnUrl`
- Stvorite prazan `Login` prikaz i dodajte ovaj sadržaj:
```HTML
You have been logged in.
Go home
```
- Ažurirajte `_Layout.cshtml` kako biste uključili `Login` u navigaciju
> Napomena: ova će akcija sada prihvatiti parametar `returnUrl` i preusmjeravanje međuprograma na stranicu koju ste upravo htjeli otvoriti, a to je npr. `/Artist/Index`. Ideja je sada upotrijebiti najjednostavniji mogući način za stvaranje kolačića za autentifikaciju kako biste mogli pristupiti zaštićenoj stranici (ovdje: `/Artist/Index`). Kasnije ćete to poboljšati koristeći stvarne vjerodajnice.
Klikom na gumb `Login` u navigaciji, korisnik još nije prijavljen, samo se prikazuje poruka.
Stvorite "prazan" autentifikacijski kolačić u akciji prijave:
```C#
public IActionResult Login(string returnUrl)
{
var claims = new List();
var claimsIdentity = new ClaimsIdentity(
claims,
CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties();
// We need to wrap async code here into synchronous since we don't use async methods
Task.Run(async () =>
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties)
).GetAwaiter().GetResult();
if (returnUrl != null)
return LocalRedirect(returnUrl);
else
return return RedirectToAction("Index", "Home");
}
```
Kada kliknete `Login`, akcija će stvoriti novi prazan kolačić koji će vam omogućiti pristup zaštićenim stranicama.
Kada kliknete `Genres` ili `Artists`, međuprogram će proslijediti povratni URL akciji `Login`. Na kraju akcije, lokalno preusmjerite stranicu na taj URL.
Otvorite "Development Tools" u pregledniku, odaberite karticu "Application" i pronađite kolačić.
### 12.4 Uklonite autentifikacijski kolačić
Korisnik također mora biti u mogućnosti izvršiti odjavu.
Implementirajte akciju `Logout` i predložak `Logout.cshtml`.
- Implementirajte akciju:
```C#
public IActionResult Logout()
{
Task.Run(async () =>
await HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme)
).GetAwaiter().GetResult();
return View();
}
```
- Dodajte predložak `Logout.cshtml`:
```HTML
You have been logged out.
Go home
```
- Ažurirajte `_Layout.cshtml` kako biste uključili gumb `Log Out` u navigaciji
Sada kliknite gumb `Log Out`.
Otvorite Development Tools u pregledniku, odaberite karticu "Application" i vidite da kolačića više nema.
### 12.5 Implementirajte obrazac za registraciju
Implementacija obrasca za registraciju i prijavu radi se na isti način kao što ste to već učinili u Web API-ju, osim što su sada uključeni korisničko sučelje i kolačić. U Web API-ju ste umjesto toga imali Swagger i JWT.
**Otvorite vježbu 6 za detalje.**
Da budemo precizni, potrebno vam je sljedeće:
- Viewmodeli za registraciju i prijavu
- Koristite iste klase koje ste prije koristili kao DTO-ove, ali ih preimenujte, na primjer, iz `UserDTO` u `UserVM`
- Isti "helper" `PasswordHashProvider`
- Ne zaboravite mapu `Security`
Sada podržite funkcionalnost `Register`:
- Implementirajte prazne metode `GET Register()` i `POST Register()` kao dijelove kontrolera `UserController`
- POST metoda prihvaća parametar `UserVM userVm`
- Automatski generirajte prikaz `Register` (predložak `Create`, model `UserVM`) i fino podesite prikaz (uklonite ID, postavite ispravnu vrstu polja za lozinku...)
- Dodajte poveznicu `Register` na layout prikaz
- U POST metodi implementirajte isti algoritam za registraciju korisnika kao i za Web API (pronađite `POST Register` u **vježbi 6**)
- Obratite pozornost na podršku za pristup bazi podataka u kontroleru
- Na kraju algoritma nemojte vratiti `Ok()`, već preusmjerite na akciju `Index` kontrolera `Home`
- Pazite da u slučaju greške iskoristite `ModelState.AddModelError()` i vratite `View()`
Testirajte registraciju - provjerite postoje li podaci u bazi za registriranog korisnika.
### 12.6 Implementirajte obrazac za prijavu
Sada podržite funkcionalnost `Login`:
- Već imate metodu `GET Login()`, a potrebna vam je i metoda `POST Login()`
- POST metoda prihvaća `LoginVM loginVm` parametar (kreirajte `LoginVM`, tamo vam trebaju samo korisničko ime i lozinka)
- Premjestite logiku stvaranja kolačića na POST metodu - nema smisla stvarati kolačić prije nego što se korisnik stvarno prijavi
- Morate podržati `returnUrl` u viewmodelu
- Koristite skriveno polje u obrascu za održavanje podataka `returnUrl` dok se korisnik ne prijavi
```
public IActionResult Login(string returnUrl)
{
var loginVm = new LoginVM
{
ReturnUrl = returnUrl
};
return View();
}
```
- Automatski generirajte prikaz `Login` (`Create` predložak, overwrite) preko postojećeg prikaza i fino podesite prikaz (postavite ispravnu vrstu za skrivena polja i polja za lozinku...)
- Na početku POST metode sada implementirajte isti algoritam za pronalaženje i provjeru korisnika u bazi podataka kao i za Web API. S jednom razlikom - vrati View u slučaju greške i prije napuni grešku modela.
```C#
// Try to get a user from database
var existingUser = _context.Users.FirstOrDefault(x => x.Username == loginVm.Username);
if (existingUser == null)
{
ModelState.AddModelError("", "Invalid username or password");
return View();
}
// Check is password hash matches
var b64hash = PasswordHashProvider.GetHash(loginVm.Password, existingUser.PwdSalt);
if (b64hash != existingUser.PwdHash)
{
ModelState.AddModelError("", "Invalid username or password");
return View();
}
```
- Umjesto stvaranja i vraćanja JWT tokena, stvorite odgovarajući kolačić
```C#
// Create proper cookie with claims
var claims = new List() {
new Claim(ClaimTypes.Name, loginVm.Username),
new Claim(ClaimTypes.Role, "User")
};
var claimsIdentity = new ClaimsIdentity(
claims,
CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties();
Task.Run(async () =>
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties)
).GetAwaiter().GetResult();
```
Testirajte i provjerite može li se registrirani korisnik sada prijaviti.
### 12.7 Prikažite korisniku podatke s kojima je prijavljen
Za to morate zaviriti u `HttpContext`, isto kao što ste učinili u slučaju Web API-ja. Međutim, malo je drugačije ako to želite učiniti u layout prikazu (jer te informacije trebate kroz cijeli web site). U bilo kojem prikazu (također i `_Layout.cshtml`), `HttpContext` se može pronaći u `ViewContext`.
Dodajte sljedeći kod nakon navigacije `` u layout prikazu:
```HTML
@{
var userName = this.ViewContext.HttpContext.User?.Identity.Name ?? "(user not logged in)";
}
```
Sada bi korisnik trebao moći vidjeti je li prijavljen ili nije - prikazat će se korisničko ime.
### 12.8 Prilagodite stanje navigacije prema stanju autentifikacije korisnika
Obično se stanje navigacije prilagođava s obzirom je li korisnik prijavljen ili ne.
- Ako korisnik nije prijavljen, ona/on mora imati prikazan gumb `Log In`
- U slučaju da je korisnik prijavljen, samo `Log Out` bi trebao biti vidljiv
Da biste podržali to stanje, morate ga kontrolirati u `_Layout.cshtml`.
Na primjer:
```C#
@* Start of _Layout.html *@
@{
var user = this.ViewContext.HttpContext.User;
bool loggedIn = false;
string username = "";
if (user != null && !string.IsNullOrEmpty(user.Identity.Name))
{
loggedIn = true;
username = user.Identity.Name;
}
}
```
```HTML
@if (!loggedIn)
{
-
Log In
}
else
{
-
Log Out @username
}
```
Također, uklonite vezu za registraciju iz izgleda i dodajte je u `Login.cshtml`:
```HTML
```
### 12.9 Autorizacija: podrška za uloge u aplikaciji
Prvo podržimo uloge podrške u bazi podataka.
```SQL
CREATE TABLE UserRole (
Id int NOT NULL IDENTITY (1, 1),
[Name] nvarchar(50) NOT NULL,
CONSTRAINT PK_UserRole PRIMARY KEY (Id)
)
GO
SET IDENTITY_INSERT UserRole ON
GO
INSERT INTO UserRole (Id, [Name])
VALUES
(1, 'Admin'),
(2, 'User')
GO
SET IDENTITY_INSERT UserRole OFF
GO
ALTER TABLE [USER]
ADD RoleId int NULL
GO
UPDATE [USER]
SET RoleId = 2
GO
ALTER TABLE [USER]
ALTER COLUMN RoleId int NOT NULL
GO
ALTER TABLE dbo.[USER]
ADD CONSTRAINT FK_USER_UserRole FOREIGN KEY (RoleId)
REFERENCES dbo.UserRole (Id)
GO
```
Iako može postojati mnogo uloga, skripta podržava samo 2 uloge:
- Admin
- User
Ponovno izgradite kontekst baze podataka i modele za podršku za novu tablicu.
```PowerShell
dotnet ef dbcontext scaffold "Name=ConnectionStrings:ex12cs" Microsoft.EntityFrameworkCore.SqlServer -o Models --force
```
Ako pogledate `POST Login`, možete vidjeti da su tvrdnje korisnika (engl. claims) postavljeni tamo, prilikom kreiranja kolačića.
Tvrdnja (engl. claim) za ulogu tamo je hardkodirana i trebala bi se postaviti iz korisničkih podataka u bazi podataka.
- Zamijenite hardkodirana vrijednost s `existingUser.Role.Name`
- **Ne zaboravite uključiti ulogu u skup rezultata prilikom dohvaćanja podataka iz baze podataka (context)**
> Uvijek možete provjeriti je li tvrdnja (claim) za ulogu postavljena tako pogledate u `HttpContext.User`
> - `HttpContext.User.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value`
> Napomena: prilikom registracije korisnika potrebno je npr. postaviti korisnikovo svojstvo `RoleId = 2` kako biste izbjegli iznimku pri registraciji novog korisnika. Zapravo, zbog nepostojanja informacija o ulozi u instanci entiteta `User`, zadana bi vrijednost bila `0`, a ne postoji takav ID uloge u bazi podataka. Kod bi izbacio iznimku kada se korisnik pokuša registrirati.
### 12.10 Autorizacija: prilagođavanje izgleda prema ulozi
Možete razlikovati koji layout prikaz treba koristiti prema ulozi trenutnog korisnika. Samo promijenite izgled u predlošku `_ViewStart.cshtml`.
Prema zadanim postavkama, vaš je izgled hardkodiran kao...
```
@{
Layout = "_Layout";
}
```
Promijenimo ga da tako promijenimo layout prikaz:
```
@{
Layout = "_Layout";
var user = ViewContext.HttpContext.User;
if (user == null)
{
Layout = "_Layout";
}
else if (user.IsInRole("Admin"))
{
Layout = "_LayoutAdmin";
}
else if (user.IsInRole("User"))
{
Layout = "_LayoutUser";
}
}
```
Sada izradite predloške `_LayoutAdmin.cshtml` i `_LayoutUser.cshtml` i promijenite neka Bootstrap svojstva u svakom.
Prvo *vizualno* promijenite vezu u `Index.cshtml` u gumb s `class="btn btn-primary"`.
Zatim registrirajte `admin` korisnika u bazi podataka.
U bazi podataka ručno promijenite ID uloge `admin` iz 2 u 1.
**_LayoutUser.cshtml**
Uklonite `Genre`, `Artist`, `Song` i `Search` iz navigacije.
**_LayoutUser.cshtml**
Prisilno promijenite izgled Bootstrap gumba
```
```
> Postoje bolji načini za to. _Primjer: https://stackoverflow.com/questions/28261287/how-to-change-btn-color-in-bootstrap._
Promijenite klasu navigacijske trake iz `bg-white` u `bg-info`.
Remove `Genre`, `Artist` and `Song` from navigation.
Log in as `user1` and observe the change.
**_LayoutAdmin.cshtml**
Promijenite klasu navigacijske trake iz `bg-white` u `bg-dark`.
Promijenite klasu navigacijske trake iz `navbar-light` u `navbar-dark`.
Uklonite `text-dark` klase iz stavki navigacijske veze.
Prijavite se kao `admin` i promatrajte promjenu.
> Koji bi bio bolji način implementacije dodavanja administratora u aplikaciju?
### 12.11 Autorizacija: preusmjeravanje korisnika na odgovarajuću rutu prema ulozi
Pogledajte akciju Login().
Na kraju akcije jednostavno preusmjerite na odgovarajući kontrolera prema ulozi.
```C#
if (loginVm.ReturnUrl != null)
return LocalRedirect(loginVm.ReturnUrl);
else if (existingUser.Role.Name == "Admin")
return RedirectToAction("Index", "AdminHome");
else if (existingUser.Role.Name == "User")
return RedirectToAction("Index", "Home");
else
return View();
```
### 12.12 Autorizacija: ograničavanje kontrolora i akcija na određenu ulogu
Vidi **vježbu 6** i atribut `[Authorize(Role="...")]`.
Isti sustav radi za MVC kao i za Web API.