# 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
  <h2>You have been logged in.</h2>
  <a asp-action="Index" asp-controller="Home" class="btn btn-outline-info">Go home</a>
  ```
- 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<Claim>();

      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
  <h2>You have been logged out.</h2>
  <a asp-action="Index" asp-controller="Home" class="btn btn-outline-info">Go home</a>
  ```

- 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<Claim>() {
      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 `<ul>` u layout prikazu:
  ```HTML
  @{
      var userName = this.ViewContext.HttpContext.User?.Identity.Name ?? "(user not logged in)";
  }
  <div class="d-flex">
      <div class="badge bg-primary">@userName</div>
  </div>
  ```

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
<!--Navigation-->
@if (!loggedIn)
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="User" asp-action="Login">Log In</a>
    </li>
} 
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="User" asp-action="Logout">Log Out @username</a>
    </li>
}
```

Također, uklonite vezu za registraciju iz izgleda i dodajte je u `Login.cshtml`:
```HTML
<div class="form-group">
    <a asp-controller="User" asp-action="Register">Not a user? Register here</a>
</div>
```

### 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  
```
  <style>
      .btn-primary, 
      .btn-primary:hover, 
      .btn-primary:active, 
      .btn-primary:visited {
          background-color: #8064A2 !important;
      }
  </style>
```
> 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.
