Wprowadzenie

Visual Studio dostarcza trzy odmienne sposoby uwierzytelniania użytkowników:

  • Konto indywidualne - aplikacja używa bazy danych;
  • Konto organizacji - użytkownik loguje się do swojego konta Azure Active Directory, Office 365 lub na podstawie uprawnień do usługi Active Directory;
  • Uwierzytelnienie systemu Windows - opcja przeznaczona dla aplikacji intranetowych, które używają modułu uwierzytelniania IIS.

Konta indywidualne pozwalają na dwa sposoby logowania:

  • Lokalne logowanie - użytkownik rejestruje się na stronie internetowej podając login i hasło. Aplikacja przechowuje hash hasła w bazie danych. Kiedy użytkownik próbuje się zalogować, ASP.NET Identity weryfikuje hasło;
  • Lokalne do serwisów społecznościowych - użytkownik loguje się za pomocą zewnętrznej usługi takiej jak: Facebook, Microsoft czy Google. Aplikacja tworzy odpowiedni wpis w bazie danych ale nie przechowuje żadnych danych. Użytkownik jest uwierzytelniony przez zewnętrzny serwis.

W poniższym artykule omówię sposób lokalnego logowania. Zarówno dla lokalnego jak społecznościowego logowania WebAPI używa OAuth2 do uwierzytelniania żądań. Sam przebieg procesu weryfikacji jest inny dla powyższych przypadków.

Posłużymy się przykładową aplikacją LocalAccountApp by MikeWasson, która pozwala użytkownikowi na zalogowanie się oraz przesłanie uwierzytelnionego zapytania do WebAPI.

OAuth2 - wprowadzenie

Przykładowa aplikacja używa Knockout.js do wiązania danych oraz jQuery do wysyłania żądań AJAX. W artykule skupimy się na żądaniach AJAX więc wiedza o Knockout.js nie jest wymagana.

Zostaną opisane kolejne punkty:

  • Co aplikacja robi po stronie klienta;
  • Co dzieje się na serwerze;
  • Ruch HTTP pomiędzy.

W pierwszej kolejności musimy zdefiniować terminologię używaną w OAuth2:

  • Resource (zasób) – dane, które mogą być chronione;
  • Resource server (serwer zasobu) – serwer na którym znaduje sie zasób;
  • Resource owner (właściciel zasobu) – podmiot, który może pozwolić na uzyskanie dostępu do zasobu;
  • Client (klient) – w naszym artykule jest to przeglądarka internetowa;
  • Access token (token dostępu) – token, który pozwala na dostęp do zasobu;
  • Bearer token – szczególowy rodzaj tokena dostępu posiadający właściowość mówiącą o tym, że każdy może użyć tokena. Innymi słowy, klient nie potrzebuje klucza kryptograficznego, aby korzystać z tokena. Z tego powodu, taki token powinien być tylko używany przy protokole HTTPS oraz powinien mieć stosunkowy krótki czas przydatności.
  • Authorization server (serwer autoryzacji) – serwer, który dostarcza tokeny dostępu.

Aplikacja może pełnić zarówno rolę serwera autoryzacji jak i serwera zasobów. Szablon projektu WebAPI podąża za tym wzorem.


Logowanie lokalne

Do przeprowadzania logowania lokalnego WebAPI używa hasła właściciela zasobu (resource owner) zdefionwanego w OAuth2.

  1. Użytkownik wprowadza nazwę i hasło po stronie klienta.
  2. Klient wysyła dane logowania do serwera autoryzacji.
  3. Serwer autoryzacji uwierzytelnia dane i zwraca token dostępu.
  4. Aby uzyskać dostęp do chronionych zasobów, klient wysyła token w nagłówku autoryzacji żądania HTTP.


OAUth2 - logowanie lokalne

Gdy wybierzesz konta indywidualne w szablonie WebAPI, utworzony projekt będzie zawierał serwer autoryzacji, który będzie sprawdzał poprawność danych oraz pozwoli na wystawienie tokena. Poniższy schemat przedstawia schemat takiego logowania:

OAuth2 - architektura logowania użytkownika

W tym scenariuszu kontrolery WebAPI zachowują się jako serwery zasobów. Filtr uwierzytelniania sprawdza tokeny dostępu a atrybut [Authorize] służy do zabezpieczania zasobów. Jeżeli kontroler lub akcja posiadają taki atrybut, wówczas wszystkie żądania do takiego kontrolera lub akcji muszą być uwierzytelnione. W przeciwnym wypadku autoryzacja się nie powiedzie a WebAPI zwróci błąd 401 – brak autoryzacji.

Serwer autoryzacji oraz filtr uwierzytelnienia korzystają z oprogramowania pośredniczącego OWIN (OWIN middleware) w celu obsługi OAuth2. Zostanie to opisane bardziej szczegółowo w dalszej części artykułu.


Wysyłanie nieautoryzowanego żądania

Aby rozpocząć należy uruchomić aplikację i kliknąć przycisk: Call API. Kiedy żądanie zostanie wykonane powinien pojawić się komunikat o błędzie w oknie wynikowym. Powodem tego jest fakt, że żądanie nie zawiera tokena dostępu – nie ma więc autoryzacji.

OAuth2 - wysyłanie nieautoryzowanego żądania

Przycisk Call API wysyła ajaxowe żądanie do ~/api/values, które wywołuje akcję z kontrolera. Poniżej kod javascript, który wysyła powyższe żądanie:

var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
    headers.Authorization = 'Bearer ' + token;
}
$.ajax({
    type: 'GET',
    url: '/api/values',
    headers: headers
}).done(function (data) {
    self.result(data);
}).fail(showError);
W aplikacji cały kod javascript znajduje się w pliku: Scripts\app.js

Dopóki użytkownik nie zaloguje się, żaden token nie jest dostępy, tj. brak tej informacji w nagłówku autoryzacji w wysyłanym żądaniu. Powoduje to zwrócenie błędu 401.

Poniżej żądanie oraz odpowiedź HTTP. Do przechwycenia ruchu HTTP użyłem narzędzi developerskich przeglądarki chrome.

Narzędzia programisty pozwalające na przychwycenie ruchu sieciowego

Zauważ, że odpowiedź zawiera nagłówek WWW-Authenticate, który wskazuje na oczekiwanie odpowiedniego tokena.


Rejestracja użytkownika

W sekcji rejestrowania użytkownika podaj adres e-mail oraz hasło a następnie kliknij przycisk: Register.

W przypadku tej aplikacji nie musisz podawać prawidłowego adresu e-mail – pamiętaj jednak, że prawdziwa aplikacja ten adres będzie sprawdzać. Jako hasło możesz podać coś w rodzaju: Password1!, tj. z dużymi oraz małymi literami, liczbą oraz znakiem specjalnym. Aby zachować czytelność oraz prostotę nie ma żadnej dodatkowej walidacji po stronie klienta.

OAuth2 - rejestracja nowego użytkownika

Przycisk Register wysyła żądanie POST do ~/api/Account/Register. Ciało żądania jest obiektem typu JSON, które przechowuje adres e-mail oraz hasło. Poniżej kod odpowiedzialny za to działanie:

var data = {
    Email: self.registerEmail(),
    Password: self.registerPassword(),
    ConfirmPassword: self.registerPassword2()
};
$.ajax({
    type: 'POST',
    url: '/api/Account/Register',
    contentType: 'application/json; charset=utf-8',
    data: JSON.stringify(data)
}).done(function (data) {
    self.result("Done!");
}).fail(showError);
Poniżej żądanie oraz odpowiedź przy wykorzystaniu domyślnych narzędzi przeglądarki Chrome:

Ruch sieciowy - rejestracja użytkownika

Żądanie jest obsługiwane przez klasę AccountController. Wewnątrz klasy używane jest ASP.NET Identity do zarządzania danymi użytkowników w bazie danych.


Pobranie tokena dostępu

Kolejnym krokiem jest pobranie tokena dostępu, który pozwoli nam na pełna autoryzację. Wprowadzamy dane logowania i klikamy przycisk Log In.

OAuth2 - logowanie

Przycisk Log In wysyła żądanie. Ciało żądania zawiera następujące dane:

  • grant_type: "hasło"
  • username: <adres e-mail>
  • password: <hasło>

Kod javascript odpowiedzialny za ajaxowe żądanie:

var loginData = {
    grant_type: 'password',
    username: self.loginEmail(),
    password: self.loginPassword()
};
$.ajax({
    type: 'POST',
    url: '/Token',
    data: loginData
}).done(function (data) {
    self.user(data.userName);
    // Token dostępu będzie przechowywany w sesji
    sessionStorage.setItem(tokenKey, data.access_token);
}).fail(showError);

Jeżeli żadanie się powiedzie zostanie zwrócony token dostępu w ciele odpowiedzi. Warto zauważyć, że tokeny są przechowywane w magazynie danych (session storage), aby mogłby być użyte w kolejnych żądaniach wysyłanych do API. W przeciwieństwie do innych form uwierzytelniania (uwierzytelnienie w oparciu o plik cookie), przeglądarka nie doda tokena automatycznie do kolejnych żądań. Aplikacja musi to zrobić sama. Jest do bardzo dobra rzecz, ponieważ ogranicza ataki CSRF

Poniżej żądanie i odpowiedź po logowaniu się do aplikacji:

Ruch sieciowy - logowanie do aplikacji


Wysłanie autoryzowanego żądania

Posiadając token dostępu jesteśmy w stanie dokonać uwierzytelnionego żądania do API. Jest to robione przez ustawienie nagłówka uwierzytelniającego. Kliknij przycisk Call API, aby zobaczyć rezultat:

OAuth2 - wysyłanie autoryzowanego żądania

Oraz żądanie i odpowiedź:

Ruch sieciowy - wysyłanie autoryzowanego żądania


Wylogowanie

Ponieważ przeglądarka nie cache’uje uprawnień oraz tokena dostępu, wylogowanie jest prostym procesem "zapomnienia" tokena poprzez usunięcie go z session storage:

self.logout = function () {
    sessionStorage.removeItem(tokenKey)
}


Zrozumieć szablon konta indywidualnego

Kiedy wybierasz szablon Konta indywidualnego w projekcie webowym ASP.NET otrzymujesz:

  • serwer autoryzacji OAuth2;
  • punkt końcowy dla WebAPI do zarządzania kontami użytkowników;
  • model EF do przechowywania kont użytkowników.

Poniżej główne klasy aplikacji, które implementują powyższe funkcje:

  • AccountController - dostarcza punkt końcowy do zarządzania kontami użytkowników. Akcja Register jest jedyną opisaną w tym poradniku. Inne metody tej klasy służą do resetowania hasła czy logowania przez serwisy społecznościowe;
  • ApplicationUser - zdefiniowana w /Models/IdentityModels.cs. Jest to model EF dla kont użytkowników w bazie danych;
  • ApplicationUserManager - zdefiniowana w /App_Start/IdentifyConfig.cs. Klasa ta dziedziczy po UserManager i wykonuje operacja na kontach użytkowników takie jak: tworzenie nowego użytkownika, weryfikacja hasła a także automatycznie zachowuje zmiany w bazie danych;
  • ApplicationOAuthProvider - obiekt podłącza się do oprogramowania pośredniczącego OWIN i przetwarza zdarzenia zgłaszane przez to oprogramowanie. Dziedziczy z OAuthAuthorizationServerProvider.


OAuth2 - klasy odpowiedzialne za autoryzację użytkownika


Konfiguracja serwera autoryzacji

W klasie StartupAuth, poniższy kod jest odpowiedzialny za konfigurację serwera:

OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/Token"),
    Provider = new ApplicationOAuthProvider(PublicClientId),
    AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(14)
    //AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);

Właściwość TokenEndpointPath jest ścieżką do punktu końcowego serwera autoryzacji. Jest to URL, którego aplikacja używa do pobrania tokena.

Provider określa dostawcę, który może podłączyć się do oprogramowania pośredniczącego OWIN i przetwarzać zdarzenia zgłoszone przez to oprogramowanie.

Poniżej podstawowy przepływ informacji, gdy aplikacja chce uzyskać dostęp do tokena:

  1. Aby usyskać tokena aplikacja wysyła żądanie do ~/Token;
  2. Oprogramowanie pośredniczące OWIN wywołuje GrantResourceOwnerCredentials;
  3. Dostawca wywołuje ApplicationUserManager, aby dokonać walidacji uprawnień oraz utworzyć tożsamość użytkownika;
  4. Jeżeli walidacja się powiedzie zostanie utworzony bilet uwierzytelnienia, który jest wykorzystywany do wygenerowania tokena.


OAuth2 - konto indywidualne

Oprogramowanie pośredniczące OAuth nic nie wie o kontach użytkowników. Dostawca komunikuje się pomiędzy OWIN oraz ASP.NET Identity.


Bearer Token – konfiguracja WebAPI

W metodzie WebApiConfig.Register poniższy kod jest odpowiedzialny za ustawienia uwierzytelnienia dla WebAPI:

config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

Klasa HostAuthenticationFilter umożliwia uwierzytelnianie za pomocą tokenów.

Metoda SuppressDefaultHostAuthentication przekazuje do WebAPI informację o ignorowaniu każdego uwierzytelniania, które ma miejsce zanim żądanie osiągnie przepływ zgodny z WebAPI - dotyczy to również IIS oraz OWIN. Dzięki temu możemy wprowadzić ograniczenia pozwalające na walidację jedynie za pomocą tokenów.

Poniżej przepływ informacji w momencie, gdy użytkownik chce uzyskać dostęp do chronionych danych:

  1. Filtr HostAuthentication wywołuje oprogramowanie pośredniczące OWIN w celu sprawdzenia poprawności tokena.
  2. OWIN konwertuje token na tożsamość użytkownika.
  3. W tym momencie żądanie jest uwierzytelnione ale nie zautoryzowane.
  4. Filtr autoryzacyjny sprawdza tożsamość użytkownika. Jeżeli filtr dokona autoryzacji, użytkownik uzyska dostęp do zasobu. Domyślnie, atrybut [Authorize] upoważni każde żądanie, które jest uwierzytelnione. Można również dokonać autoryzacji przez role lub inne prawa do czego, tj. claims.
  5. Jeżeli wszystkie powyższe kroki są poprawne, kontroler zwróci zabezpieczone zasoby. W przeciwnym razie, klient otrzyma błąd 401 – brak autoryzacji.


OAuth2 - token autoryzacji