Jak zrobić własny mechanizm uwierzytelniania – na przykładzie API key i BasicAuth

Jak zrobić własny mechanizm uwierzytelniania – na przykładzie API key i BasicAuth

Wstęp

Hej, mimo że .NET daje Ci kilka gotowych mechanizmów (schematów) uwierzytelniania, to jednak czasem trzeba napisać coś swojego. Takimi przykładami mogą być Basic Authentication albo chociażby Api Key Authentication. Api Key będziesz używał wtedy, kiedy masz swoje API dostępne dla innych programistów, jednak chcesz uwierzytelnić w jakiś sposób każdego klienta, który z Twojego API korzysta.

W tym artykule pokażę Ci jak skonstruować swój własny mechanizm uwierzytelniania. Co więcej – pokażę jak wybrać dynamicznie odpowiedni schemat w zależności od przekazanego żądania. No to jedziemy.

Do artykułu przygotowałem przykładowe kody, które możesz pobrać z GitHub.

Czym jest uwierzytelnienie

Co nieco pisałem już na ten temat w artykule o uwierzytelnianiu i o tym, czym jest ClaimsPrincipal.

Generalnie proces uwierzytelnienia polega na tym, żeby sprawdzić dane identyfikacyjne, które przychodzą od użytkownika (np. w żądaniu HTTP) i wystawić na ich podstawie ClaimsPrincipal.

Najprostszym przykładem będzie właśnie klucz API. Załóżmy, że gdy klient korzysta z Twojego API, powinien w żądaniu wysłać nagłówek X-API-KEY. Jeśli go nie ma, taka osoba jest anonimowa (nie jest uwierzytelniona). Jeśli jest, to sprawdzasz, czy ten klucz jest gdzieś u Ciebie zarejestrowany. Jeśli tak, to na tej podstawie możesz stworzyć odpowiedni obiekt ClaimsPrincipal. Na tym właśnie polega cały proces – uwierzytelnij klienta, czyli zwróć informację na temat KIM ON JEST.

Później ten ClaimsPrincipal jest używany przez mechanizm autoryzacji, który sprawdza, co dany użytkownik może zrobić. No i ten ClaimsPrincipal jest dostępny w kontrolerach w HttpContext.User.

Czym tak naprawdę jest API Key?

Jeśli wystawiasz dla świata jakieś API, to to API może być publiczne (dostęp dla każdego), niepubliczne (dostęp tylko dla zarejestrowanych klientów) lub mieszane, przy czym zarejestrowani klienci mogą więcej.

Jeśli ktoś rejestruje u Ciebie klienta do Twojego API, powinieneś wydać mu tzw. API Key – klucz jednoznacznie identyfikujący takiego klienta. To może być w najprostszej postaci GUID. Po prawdzie klient też powinien dostać od Ciebie API Secret – czyli coś w rodzaju hasła.

Gdy klient chce wykonać jakąś operację na API, powinien się uwierzytelnić, wysyłając w żądaniu co najmniej Api Key. W taki sposób możesz logować operacje tego klienta lub w ogóle nie dopuścić go do używania API. Klient może się też uwierzytelnić za pomocą różnych mechanizmów jak OpenId Connect, ale ten artykuł nie jest o tym.

Dzisiaj pokazuję jak stworzyć taki mechanizm uwierzytelniania w .NET.

Jak działa mechanizm uwierzytelniania w .NET?

Tworząc swój własny mechanizm uwierzytelniania, tak naprawdę tworzysz własny „schemat”. Schemat to nic innego jak nazwa (np. „ApiKey”) połączona z Twoją klasą do uwierzytelniania (handler).

Wszystko sprowadza się ostatecznie do trzech kroków:

  • stwórz swój handler do uwierzytelniania (klasa dziedzicząca po AuthenticationHandler)
  • stwórz w nim obiekt ClaimsPrincipal
  • zarejestruj swój schemat

AuthenticationHandler

Całą obsługę uwierzytelniania robimy w klasie, która dziedziczy po AuthenticationHandler (bądź implementuje interfejs IAuthenticationHandler, co jest nieco trudniejsze). To na początek może wyglądać nieco skomplikowanie, ale jest proste.

Opcje

Klasa abstrakcyjna AuthenticationHandler jest klasą generyczną. Przyjmuje w parametrze typ, w którym trzymamy opcje naszego schematu uwierzytelnienia. Przy czym te opcje muszą dziedziczyć po klasie AuthenticationSchemeOptions i mogą być zupełnie puste, np.:

public class ApiKeyAuthenticationOptions: AuthenticationSchemeOptions
{

}

W tych opcjach możemy mieć wszystko, co nam się podoba. Przykładem może być uwierzytelnianie za pomocą Bearer Token, gdzie w opcjach masz czas życia takiego tokena, wystawcę itd. Żeby zademonstrować całość, zrobimy sobie ograniczenie do długości klucza API. Nie ma to w prawdzie żadnego zastosowania praktycznego. Po prostu pokazuję, jak wykorzystać te opcje:

public class ApiKeyAuthenticationOptions: AuthenticationSchemeOptions
{
    public int ApiKeyLength { get; set; }
    public bool CheckApiKeyLength { get; set; }
}

Handler

Teraz musimy napisać klasę, która będzie całym sercem uwierzytelniania – ta, która dziedziczy po AuthenticationHandler:

public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        throw new NotImplementedException();
    }
}

Jak widzisz, wystarczy przesłonić metodę HandleAuthenticateAsync lub jej synchroniczną odpowiedniczkę.

Metoda musi zwrócić AuthenticationResult. Ten AuthenticationResult może przyjąć 3 stany:

  • sukces,
  • niepowodzenie,
  • brak wyniku.

Sukces

Jeśli rezultat kończy się sukcesem, musimy do niego przekazać „bilet” – ticket. Jest to taki mały obiekt, który trzyma informacje o schemacie uwierzytelnienia, ClaimsPrincipal i może zawierać jakieś dodatkowe dane (AuthenticationProperties). W swojej minimalnej postaci wystarczy mu nazwa schematu i ClaimsPrincipal.

Oczywiście „sukces” oznacza, że nasz mechanizm poprawnie uwierzytelnił danego klienta / użytkownika.

Niepowodzenie

Jeśli rezultat zakończy się niepowodzeniem (Fail) oznacza to, że nie dość, że użytkownik nie został uwierzytelniony przez nasz mechanizm, to jeszcze wszystkie inne ewentualne handlery już go nie mogą próbować uwierzytelnić.

Brak wyniku

Jeśli jednak rezultat zakończy się brakiem wyniku (NoResult) oznacza to, że użytkownik nie jest uwierzytelniony TYM SCHEMATEM, jednak inne ewentualne handlery mogą próbować go dalej uwierzytelniać.

Kiedy to stosujemy? Załóżmy, że mamy dwa schematy – ApiKey i Login + Hasło. Każdy handler jest uruchamiany po kolei przez Framework (chyba, że któryś handler zwróci sukces lub niepowodzenie – wtedy kolejne nie są już uruchamiane).

I teraz jeśli handler do ApiKey nie znajdzie klucza tam, gdzie powinien on być (np. w nagłówku żądania), może chcieć przekazać proces uwierzytelnienia kolejnym handlerom. Gdzieś tam wystartuje taki, który spodziewa się loginu i hasła.

Cały proces można by przedstawić w postaci prostego algorytmu:

UWAGA! W rzeczywistym świecie ta odpowiedź ma sens tylko, gdy w mechanizmie AUTORYZACJI wybrano kilka schematów uwierzytelnienia dla jakiejś końcówki. A, że to jak najbardziej jest możliwe, trzeba stosować tę wartość.

Podczas zwykłej operacji uwierzytelnienia (bez autoryzacji) zawsze w grę wchodzi tylko jeden schemat.

Konstruktor

Klasa AuthenticationHandler wymaga pewnych obiektów przekazanych w konstruktorze. Dlatego też minimalny konstruktor musi je przyjąć. Na szczęście wszystko ogarnia Dependency Injection. Teraz całość wygląda tak:

public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options, 
        ILoggerFactory logger, 
        UrlEncoder encoder, 
        ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        throw new NotImplementedException();
    }
}

Jak widzisz, jedną z tych wymaganych rzeczy jest IOptionsMonitor. Jeśli nie wiesz, czym to jest, pisałem o tym w artykule o opcjach.

Piszemy handlera

Napiszmy sobie teraz jakąś oszukaną klasę, która zwróci dane użytkownika, dla którego jest zarejestrowany dany ApiKey. Ta klasa pełni rolę „bazy danych”. Równie dobrze możesz tutaj użyć EfCore, czy czegokolwiek sobie życzysz:

public class ApiKeyClientProvider
{
    private Dictionary<string, ApiKeyClient> _clients = new Dictionary<string, ApiKeyClient>();
    public ApiKeyClientProvider()
    {
        AddClients();
    }

    public ApiKeyClient GetClient(string key)
    {
        ApiKeyClient result; ;

        if (_clients.TryGetValue(key, out result))
            return result;
        else
            return null;
    }

    private void AddClients()
    {
        var client = new ApiKeyClient()
        {
            ApiKey = "klucz-1",
            Email = "client1@example.com",
            Id = 1,
            Name = "Klient 1"
        };

        _clients[client.ApiKey] = client;

        var client2 = new ApiKeyClient()
        {
            ApiKey = "klucz-2",
            Email = "client2@example.com",
            Id = 2,
            Name = "Klient 2"
        };

        _clients[client2.ApiKey] = client2;
    }
}

W kolejnym kroku możemy zaimplementować ostatecznie nasz schemat uwierzytelniania:

public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    private readonly ApiKeyClientProvider _clientProvider;
    public ApiKeyAuthenticationHandler(
        ApiKeyClientProvider clientProvider, //wstrzykujemy naszą oszukaną bazę danych
        IOptionsMonitor<ApiKeyAuthenticationOptions> options, 
        ILoggerFactory logger, 
        UrlEncoder encoder, 
        ISystemClock clock) : base(options, logger, encoder, clock)
    {
        _clientProvider = clientProvider;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var apiKey = GetApiKey();
        if (string.IsNullOrWhiteSpace(apiKey))
            return AuthenticateResult.Fail("No API key provided");

        var client = _clientProvider.GetClient(apiKey);
        if (client == null)
            return AuthenticateResult.Fail("Invalid API key");

        var principal = CreatePrincipal(client);

        AuthenticationTicket ticket = new AuthenticationTicket(principal, "ApiKey");
        return AuthenticateResult.Success(ticket);
    }

    private string GetApiKey()
    {
        StringValues keyValue;
        if (!Context.Request.Headers.TryGetValue("X-API-KEY", out keyValue))
            return null;

        if (!keyValue.Any())
            return null;

        return keyValue.ElementAt(0);
    }

    private ClaimsPrincipal CreatePrincipal(ApiKeyClient client)
    {
        ClaimsIdentity identity = new ClaimsIdentity("ApiKey");
        identity.AddClaim(new Claim(ClaimTypes.Email, client.Email));
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, client.Id.ToString()));
        identity.AddClaim(new Claim(ClaimTypes.Name, client.Name));

        return new ClaimsPrincipal(identity);
    }
}

Przejdźmy ją fragmentami.

Na samym dole jest metoda CreatePrincipal. Ona tworzy obiekt ClaimsPrincipal na podstawie przekazanego rekordu klienta z naszej bazy.

Tworzenie ClaimsPrincipal polega w sumie na utworzeniu odpowiednich ClaimsIdentity wraz z Claimsami. ApiKey, które widzisz podczas tworzenia ClaimsIdentity to po prostu nazwa naszego schematu. Dzięki temu wiesz – aha, ten ClaimsIdentity powstał ze schematu ApiKey.

Jeśli nie wiesz, czym jest ten ClaimsPrincipal i Claimsy, przeczytaj ten artykuł.

Ok, dalej mamy metodę GetApiKey. Ona po prostu pobiera wartość odpowiedniego nagłówka żądania. Jak widzisz, klasa AuthenticationHandler daje nam bezpośredni dostęp do kontekstu HTTP przez właściwość Context.

No i najważniejsza metoda – HandleAuthenticateAsync. Przyjrzyjmy się jej jeszcze raz:

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
    var apiKey = GetApiKey();
    if (string.IsNullOrWhiteSpace(apiKey))
        return AuthenticateResult.NoResult;

    var client = _clientProvider.GetClient(apiKey);
    if (client == null)
        return AuthenticateResult.Fail("Invalid API key");

    var principal = CreatePrincipal(client);

    AuthenticationTicket ticket = new AuthenticationTicket(principal, "ApiKey");
    return AuthenticateResult.Success(ticket);
}

Na początku pobieramy klucz API z nagłówka żądania. Jeśli jest pusty, to znaczy że nie można uwierzytelnić takiego klienta TYM SCHEMATEM. Klient po prostu nie dodał klucza do żądania. Zwracamy błąd uwierzytelnienia. Być może inny schemat będzie w stanie go zidentyfikować.

Jeśli jednak ten klucz jest, pobieramy użytkownika przypisanego do niego z naszej bazy. I znowu – jeśli taki użytkownik nie istnieje, to znaczy że klucz API nie jest prawidłowy.

Na koniec jeśli użytkownik istnieje, tworzymy na jego podstawie ClaimsPrincipal. Na koniec wydajemy mu „bilecik” z jego danymi i zwracamy sukces uwierzytelnienia.

Używamy opcji

Jak widzisz, nie dorobiliśmy jeszcze sprawdzenia, czy nasz klucz API ma odpowiednią długość. Ale wszystko mamy wstrzyknięte w konstruktorze. IOptionsMonitor daje nam te opcje. Wykorzystajmy więc go. Jeśli nie wiesz, czym jest IOptionsMonitor i jak z niego korzystać, przeczytaj ten artykuł.

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
    var apiKey = GetApiKey();
    if (string.IsNullOrWhiteSpace(apiKey))
        return AuthenticateResult.Fail("No API key provided");

    if (Options.CheckApiKeyLength)
    {
        if (apiKey.Length != Options.ApiKeyLength)
            return AuthenticateResult.Fail("Invalid API key");
    }

    var client = _clientProvider.GetClient(apiKey);
    if (client == null)
        return AuthenticateResult.Fail("Invalid API key");

    var principal = CreatePrincipal(client);

    AuthenticationTicket ticket = new AuthenticationTicket(principal, "ApiKey");
    return AuthenticateResult.Success(ticket);
}

Jak widzisz, dostęp do opcji uwierzytelniania masz przez właściwość Options z klasy bazowej. Teraz tylko musimy zarejestrować nasz schemat.

Rejestracja

Pamiętaj o rejestracji naszej „bazy danych”:

builder.Services.AddScoped<ApiKeyClientProvider>();

No i sam schemat:

builder.Services.AddAuthentication("ApiKey")
    .AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", o =>
    {
        o.ApiKeyLength = 7;
        o.CheckApiKeyLength = true;
    });

Wszystko rozbija się o rejestrację AddAuthentication. W parametrze podajemy domyślny schemat uwierzytelniania. Następnie dodajemy nasz schemat przez metodę AddScheme. Jeśli nie używasz opcji, to w drugim parametrze możesz dać po prostu null. Drugi parametr to delegat, który ustawia nasze opcje. Oczywiście w prawdziwym programie te wartości byłyby pobierane z konfiguracji.

Pamiętaj też o middleware. Musisz dodać przed UseAuthorization():

app.UseAuthentication();
app.UseAuthorization();

Challenge

Challenge (authentication challenge) to mechanizm, który jest uruchamiany przez .NET, gdy użytkownika nie można uwierzytelnić. Efektem tego może być przejście na stronę logowania albo po prostu dodanie jakiejś informacji w odpowiedzi na żądanie. Domyślny Challenge zwraca po prostu błąd 401.

Aby zrobić coś swojego, wystarczy przeciążyć metodę HandleChallengeAsync w naszej klasie. Można to zrobić tak:

protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
    Response.Headers.WWWAuthenticate = new StringValues("X-API-KEY");
    return Task.CompletedTask;
}

Podczas wywoływania HandleChallengeAsync przez .Net możemy korzystać z Response – czyli możemy modyfikować sobie odpowiedź do klienta. Standardowym podejściem w takim przypadku jest umieszczenie nagłówka www-authenticate z nazwą schematu lub jakimiś wskazówkami, jak uwierzytelniać się w naszym systemie.

To jest opcjonalne, Domyślny mechanizm, jak mówiłem, zwraca po prostu błąd 401.

Jeśli spróbujesz teraz pobrać dane przez Postmana, oczywiście nie zobaczysz ich, ale zostanie zwrócony Ci właśnie ten nagłówek. Zwróć też uwagę na to, że zwrócony kod operacji (200) oznacza operację zakończoną sukcesem:

ForwardChallenge

Jeśli przyjrzysz się klasie bazowej do opcji uwierzytelniania, zobaczysz taką właściwość jak ForwardChallenge. Możesz tutaj przypisać nazwę schematu, który będzie użyty do Challengowania. Jeśli więc podczas konfiguracji naszego schematu, przypisałbyś takie opcje:

builder.Services.AddAuthentication("ApiKey")
    .AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", o =>
    {
        o.ApiKeyLength = 7;
        o.CheckApiKeyLength = true;
        o.ForwardChallenge = "Bearer";
    });

To wtedy, jeśli Twój schemat nie uwierzytelni użytkownika, Challenge zostanie przekazany do schematu o nazwie Bearer. Oczywiście, jeśli taki schemat nie został zarejestrowany, program się wysypie.

Forbid

To jest metoda, która wykona się, gdy dostęp do zasobu nie został udzielony dla Twojego schematu uwierzytelniania. Inaczej mówiąc, załóżmy że masz dwa schematy uwierzytelniania:

  • Użytkownik podaje login i hasło
  • Klient API podaje klucz API

Teraz, niektóre końcówki mogą wymagać konkretnego schematu uwierzytelniania. Załóżmy, że mamy jakieś końcówki administracyjne, na które nie można się dobić za pomocą uwierzytelniania przez klucz API. One wymagają uwierzytelnienia za pomocą loginu i hasła. Można to w kontrolerze zablokować przekazując po prostu nazwę schematu, który oczekujemy, np:

[Authorize(AuthenticationSchemes = "LoginAndPass")]

I teraz załóżmy taką sytuację. Jakiś klient API został uwierzytelniony przez nasze ApiKeyAuthorizationHandler. Natomiast końcówka wymaga uwierzytelnienia przez jakiś schemat LoginAndPass. W tym momencie zostanie wywołana metoda Forbid w naszym handlerze (ponieważ to nasz handler go uwierzytelnił). Działa to analogicznie do metody Challenge. Domyślnie zwracany jest błąd 403.

Oczywiście tutaj też możemy przekazać Forbid do innego schematu, używając – analogicznie jak przy Challenge – ForwardForbid w opcjach uwierzytelniania.

Inne opcje

Jeśli chodzi o uwierzytelnianie klientów API, istnieje inna opcja, w której właściwie nie musisz pisać tego kodu. Jest to usługa Azure’owa o nazwie Azure API Management, która załatwia to wszystko za Ciebie. Możesz też ustawić limity czasowe/ilościowe dla konkretnych klientów. Czego dusza zapragnie. Usługa daje Ci duuużo więcej (wraz z portalem dla Twoich klientów). Nie jest jednak darmowa.

Basic Authentication

Basic Authentication to standardowy mechanizm uwierzytelniania. Polega on na obecności odpowiedniej wartości w nagłówku Authentication.

A ta wartość to po prostu: Base64(<login>:<hasło>).

Czyli dajesz login i hasło przedzielone dwukropkiem, a następnie konwertujesz to na Base64. Taką wartość umieszcza się w nagłówku Authentication. Jak zapewne się domyślasz, nie jest to zbyt dobra metoda, jednak jest używana. W związku z tym, że przekazywane jest jawnie login i hasło, konieczne jest użycie SSL przy tej formie.

Napiszemy sobie teraz prosty mechanizm uwierzytelniania używający właśnie Basic Authentication. To będzie zrobione analogicznie do tego, co robiliśmy wyżej. Więc możesz po prostu przejrzeć sobie kod:

public class BasicAuthenticationOptions: AuthenticationSchemeOptions
{
}
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
    private readonly UserProvider _userProvider;
    private record UserCredentials(string login, string password);
    
    public BasicAuthenticationHandler(
        UserProvider userProvider,
        IOptionsMonitor<BasicAuthenticationOptions> options, 
        ILoggerFactory logger, 
        UrlEncoder encoder, 
        ISystemClock clock) : base(options, logger, encoder, clock)
    {
        _userProvider = userProvider;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var creds = RetrieveCredentials();
        if (creds == null)
            return AuthenticateResult.Fail("No credentials");

        var userData = _userProvider.GetUser(creds.login, creds.password);
        if (userData == null)
            return AuthenticateResult.Fail("No such user");

        if (userData.Password != creds.password)
            return AuthenticateResult.Fail("Invalid password");

        var principal = CreatePrincipal(userData);
        var ticket = new AuthenticationTicket(principal, "Basic");

        return AuthenticateResult.Success(ticket);
    }

  private UserCredentials RetrieveCredentials()
  {
      if (Context.Request.Headers.Authorization.Count == 0)
          return null;

      var basedValue = Context.Request.Headers.Authorization[0];
      if (basedValue.StartsWith("Basic "))
          basedValue = basedValue.Remove(0, "Basic ".Length);
      else
          return null;

      var byteData = Convert.FromBase64String(basedValue);
      var credsData = Encoding.UTF8.GetString(byteData);

      var credValues = credsData.Split(':');
      if (credValues == null || credValues.Length != 2)
          return null;

      return new UserCredentials(credValues[0], credValues[1]);
  }

    private ClaimsPrincipal CreatePrincipal(UserData user)
    {
        ClaimsIdentity identity = new ClaimsIdentity("Basic");
        identity.AddClaim(new Claim(ClaimTypes.Email, user.Email));
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
        identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));

        return new ClaimsPrincipal(identity);
    }
}

Jedyne, czego tu nie widać, to klasa UserProvider, która wygląda bardzo podobnie jak ApiKeyClientProvider. Możesz zobaczyć całość na GitHub. Wszystko działa tutaj analogicznie.

Dodałem tę metodę, żeby pokazać Ci teraz, w jaki sposób możesz dynamicznie wybrać sobie schemat uwierzytelniania.

Dynamiczny wybór schematu uwierzytelniania

Żeby móc dynamicznie wybrać schemat, musimy dodatkowo dodać politykę. To nie wymaga dużo wysiłku, spójrz na ten kod:

builder.Services.AddAuthentication("ApiKeyOrBasic")
    .AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", o =>
    {
        o.ApiKeyLength = 7;
        o.CheckApiKeyLength = true;
    })
    .AddScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>("Basic", null)
    .AddPolicyScheme("ApiKeyOrBasic", null, o =>
    {
        o.ForwardDefaultSelector = context =>
        {
            if (context.Request.Headers.ContainsKey("X-API-KEY"))
                return "ApiKey";
            else
                return "Basic";
        };
    });

Gdy rejestrujemy mechanizmy uwierzytelniania przez AddAuthentication, zobacz że jako domyślny schemat podajemy nazwę ApiKeyOrBasic – czyli nazwę naszej polityki do wyboru schematu.

Teraz, wykonując AddPolicyScheme, rejestrujemy właśnie taką politykę.

W rezultacie, wywołany zostanie domyślny schemat uwierzytelniania – czyli nasza polityka, która po prostu sprawdzi, czy w żądaniu znajduje się odpowiedni nagłówek. Następnie zwraca nazwę schematu, którym to żądanie powinno być uwierzytelnione. Nazwa trafia do ForwardDefaultSelector.

.NET w kolejnym kroku uruchomi właśnie ten schemat.

Czym jest domyślna nazwa schematu?

W .NET możesz m.in. przy kontrolerach wymagać uwierzytelnienia użytkownika konkretnym schematem. Czyli przykładowo: „Jeśli użytkownik chce wykonać tę operację, MUSI być zalogowany schematem login i hasło„.

Jeśli tego nie podasz jawnie, wtedy do gry wejdzie domyślny schemat uwierzytelniania. Dlatego ważne jest, żeby zawsze go podać.

Dobre praktyki

Kod, który pokazałem nie zawiera dobrych praktyk. Ale dzięki temu jest bardziej czytelny.

W prawdziwym kodzie upewnij się, że stosujesz te dobre praktyki, czyli:

  • Nazwy nagłówków – jeśli wprowadzasz jakieś własne nazwy nagłówków, upewnij się, że NIE zaczynają się od X-. Jest to przestarzała forma, która jest już odradzana przez konsorcjum. Zamiast tego powinieneś w jakiś jednoznaczny sposób nazwać swój nagłówek, np.: MOJ-PROGRAM-API-KEY.
  • Nazwy schematów w gołych stringach – no coś takiego w prawdziwym kodzie woła o pomstę do nieba. Powinieneś stworzyć jakieś stałe w stylu:
class ApiKeyAuthenticationDefaults
{
    public const string SchemeName = "ApiKey";
}

i posługiwać się tymi stałymi.

  • Nazwy nagłówków w gołych stringach – tutaj tak samo. Wszystko powinno iść przez stałe.

Dzięki za przeczytanie tego artykułu. Jeśli czegoś nie rozumiesz lub znalazłeś błąd, koniecznie daj znać w komentarzu. No i udostępnij go osobom, którym się przyda 🙂

Obrazek wyróżniający: Obraz autorstwa macrovector na Freepik

Podziel się artykułem na:
Uwierzytelnianie (autentykacja) w .NET

Uwierzytelnianie (autentykacja) w .NET

Wstęp

Ten artykuł opisuje mechanizm uwierzytelniania w .NET BEZ użycia Microsoft Identity

Na szybko (kliknij, żeby rozwinąć)

  1. Stwórz projekt RazorPages lub Razor MVC z opcją Authentication type ustawioną na NONE
  1. W pliku Project.cs (lub Startup.cs) dodaj serwis autentykacji
using Microsoft.AspNetCore.Authentication.Cookies;
//

builder.Services.AddAuthentication(o =>
{
    o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    o.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
{
    o.Cookie.IsEssential = true;
    o.Cookie.HttpOnly = true; //bezpieczeństwo
    o.Cookie.SameSite = SameSiteMode.Strict; //bezpieczeństwo
    o.Cookie.SecurePolicy = CookieSecurePolicy.Always; //bezpieczeństwo
    o.Cookie.MaxAge = TimeSpan.FromDays(30); //ciastko logowania ważne przez 30 dni
    o.AccessDeniedPath = "/AccessDenied";
    o.LoginPath = "/Login";
    o.ReturnUrlParameter = "return_url";
});
  1. Dodaj uwierzytelnianie do middleware -> pamiętaj, że kolejność jest istotna
app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

Teraz już możesz logować i wylogowywać użytkowników.

Logowanie
  1. Zidentyfikuj użytkownika ręcznie – po prostu w jakiś sposób musisz sprawdzić, czy podał prawidłowe dane logowania (login i hasło)
  2. Stwórz ClaimsPrincipal dla tego użytkownika
  3. Wywołaj HttpContext.SignIn -> to utworzy ciastko logowania i użytkownik będzie już uwierzytelniony w kolejnych żądaniach (HttpContext.User będzie zawierało wartość utworzoną w kroku 2)
Wylogowanie

Wywołaj HttpContext.SignOutAsync -> to zniszczy ciastko logowania. W kolejnych żądaniach obiekt HttpContext.User będzie pusty.

Jeśli masz jakiś problem, przeczytaj pełny artykuł poniżej.

UWAGA

W słowniku języka polskiego NIE ISTNIEJE słowo autentykacja. W naszym języku ten proces nazywa się uwierzytelnianiem. Słowo autentykacja zostało zapożyczone z angielskiego authentication. Dlatego też w tym artykule posługuję się słowem uwierzytelnianie.

Po co komu uwierzytelnianie bez Identity?

Może się to wydawać dziwne, no bo przecież Identity robi całą robotę. Ale jeśli chcesz uwierzytelniać użytkowników za pośrednictwem np. własnego WebApi albo innego mechanizmu, który z Identity po prostu nie współpracuje, to nie ma innej możliwości.

Uwierzytelnianie vs Identity

Musisz zdać sobie sprawę, że mechanizm uwierzytelniania i Identity to dwie różne rzeczy. Identity korzysta z uwierzytelniania, żeby mechanizm był pełny. A jakie są różnice?

Co daje Identity

Od Identity dostajesz CAŁĄ obsługę użytkownika. Tzn:

  • zarządzanie kontami użytkowników (tworzenie, usuwanie, tokeny, dwustopniowe uwierzytelnianie itd.)
  • przechowywanie użytkowników (np. tworzenie odpowiednich tabel w bazie danych lub obsługa innego sposobu przechowywania danych użytkowników)
  • zarządzanie rolami użytkowników
  • i generalnie wiele innych rzeczy, które mogą być potrzebne w standardowej aplikacji

Mechanizm Identity NIE JEST dostępny na „dzień dobry”. Aby go otrzymać, możesz utworzyć nową aplikację z opcją Authentication type ustawioną np. na Individual Accounts.

Możesz też doinstalować odpowiednie NuGety i samemu skonfigurować Identity.

Co daje uwierzytelnianie?

  • tworzenie i usuwanie ciasteczek logowania (lub innego mechanizmu uwierzytelniania użytkownika)
  • tworzenie obiektu User w HttpContext podczas żądania
  • przekierowania użytkowników na odpowiednie strony (np. logowania, gdy nie jest zalogowany)

Jak widzisz, Identity robi dużo więcej i pod spodem korzysta z mechanizmów uwierzytelniania. Podczas konfiguracji Identity konfigurujesz również uwierzytelnianie.

Konfiguracja uwierzytelniania

Najprościej będzie, jeśli utworzysz projekt BEZ żadnej identyfikacji. Po prostu podczas tworzenia nowego projektu upewnij się, że opcja Authentication type jest ustawiona na NONE:

Dzięki temu nie będziesz miał dodanego ani skonfigurowanego mechanizmu Identity. I dobrze, bo jeśli go nie potrzebujesz, to bez sensu, żeby zaciemniał i utrudniał sprawę. Mechanizm Identity możesz sobie dodać w każdym momencie, instalując odpowiednie NuGety.

A teraz jak wygląda konfiguracja uwierzytelniania? Składa się tak naprawdę z trzech etapów:

  • zarejestrowania serwisów dla uwierzytelniania
  • konfiguracji mechanizmu, który będzie używany do odczytywania (zapisywania) informacji o zalogowanym użytkowniku (schematu)
  • dodanie uwierzytelniania do middleware pipeline.

Schemat

Zanim pójdziemy dalej, wyjaśnię Ci czym jest schemat. To nic innego jak określenie sposobu w jaki użytkownicy będą uwierzytelniani. Różne scenariusze mogą wymagać różnych metod uwierzytelniania. Każda z tych metod może wymagać innych danych. To jest właśnie schemat. Pisząc uwierzytelniać mam na myśli taki flow (w skrócie):

Work-flow mechanizmu autentykacji
  1. Klient wysyła żądanie do serwera (np. żądanie wyświetlenia strony z kontem użytkownika)
  2. Mechanizm uwierzytelniania (który jest w middleware pipeline) rusza do roboty. Sprawdza, czy użytkownik jest już zalogowany, odczytując jego dane wg odpowiedniego schematu (z ODPOWIEDNIEGO ciastka, bearer token’a, BasicAuth lub jakiegokolwiek innego mechanizmu)
  3. Na podstawie informacji odczytanych w punkcie 2, tworzony jest HttpContext.User
  4. Rusza kolejny komponent z middleware pipeline

Każdy schemat ma swoją własną nazwę, możesz tworzyć własne schematy o własnych nazwach jeśli czujesz taką potrzebę.

Rejestracja serwisów uwierzytelniania

W pliku Program.cs lub Startup.cs (w metodzie ConfigureServices) możesz zarejestrować wymagane serwisy w taki sposób:

builder.Services.AddAuthentication();

To po prostu zarejestruje standardowe serwisy potrzebne do obsługi uwierzytelniania. Jednak bardziej przydatną formą rejestracji jest ta ze wskazaniem domyślnych schematów:

builder.Services.AddAuthentication(o =>
{
    o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    o.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});

W powyższym kodzie ustawiam domyślne schematy do:

  • Uwierzytelniania
  • Challenge
  • Logowania (tworzenia ciastka logowania)
  • Wylogowania (usuwania ciastka wylogowania)

Jak już wiesz, każdy schemat ma swoją nazwę. W .NET domyślne nazwy różnych schematów są zapisane w stałych. Np. domyślna nazwa schematu opartego na ciastkach (uwierzytelnianie ciastkami) ma nazwę zapisaną w CookieAuthenticationDefaults. Analogicznie domyślna nazwa schematu opartego na JWT Bearer Token – JwtBearerDefaults.

Oczywiście, jeśli masz taką potrzebę, możesz nadać swoją nazwę.

Konfiguracja ciasteczka logowania

To drugi krok, jaki trzeba wykonać. Konfiguracja takiego ciastka może wyglądać tak:

using Microsoft.AspNetCore.Authentication.Cookies;
//
builder.Services.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
{
    o.Cookie.IsEssential = true;
    o.Cookie.HttpOnly = true;
    o.Cookie.SameSite = SameSiteMode.Strict;
    o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    o.Cookie.MaxAge = TimeSpan.FromDays(30);
    o.AccessDeniedPath = "/AccessDenied";
    o.LoginPath = "/Login";
    o.ReturnUrlParameter = "return_url";
});

W pierwszym parametrze podajesz nazwę schematu dla tego ciastka. W drugim ustawiasz domyślne opcje. Jeśli nie wiesz co one oznaczają i dlaczego tak, a nie inaczej, przeczytaj artykuł o ciastkach, w którym to wyjaśniam.

Na koniec ustawiasz dwie ścieżki:

  • ścieżka do strony z informacją o zabronionym dostępie
  • ścieżka do strony logowania

a także parametr return_url – o nim za chwilę.

Po co te ścieżki? To ułatwienie – element mechanizmu uwierzytelniania. Jeśli niezalogowany użytkownik wejdzie na stronę, która wymaga uwierzytelnienia (np. „Napisz nowy post”), wtedy automatycznie zostanie przeniesiony na stronę, którą zdefiniowałeś w LoginPath.

Analogicznie z użytkownikiem, który jest zalogowany, ale nie ma praw dostępu do jakiejś strony (np. modyfikacja użytkowników, do czego dostęp powinien mieć tylko admin) – zostanie przekierowany automatycznie na stronę, którą zdefiniowałeś w AccessDeniedPath.

Dodanie uwierzytelniania do middleware pipeline

Skoro mechanizm uwierzytelniania jest już skonfigurowany, musimy dodać go do pipeline. Pamiętaj, że kolejność komponentów w pipeline jest istotna. Dodaj go tuż przed autoryzacją:

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

To tyle jeśli chodzi o konfigurację. Teraz zacznijmy tego używać…

Logowanie

UWAGA! Nie opisuję tutaj czym jest ViewModel, DataBinding, czy też jak działa HTML. Zakładam, że znasz podstawy pracy z RazorPages lub RazorViews.

Rozwalmy wszystko na części. Najpierw odpowiedz sobie na pytanie „Na czym polega logowanie?”. Logowanie to:

  • uwierzytelnienie użytkownika (sprawdzenie, czy jego login i hasło się zgadzają)
  • zapisanie ciasteczka logowania (lub w jakiś inny sposób przechowanie informacji o tym, że jest zalogowany)

W pierwszym kroku stwórz stronę do logowania. Przykład w RazorPages może wyglądać tak:

C# (serwer):

public class LoginPageModel : PageModel
{
    [BindProperty]
    public string UserName { get; set; } = string.Empty;
    [BindProperty]
    public string Password { get; set; } = string.Empty;

    [BindProperty]
    public bool RememberMe { get; set; }
}

A formularz logowania może wyglądać tak…

HTML (klient)

@page
@model RazorPages_Auth.Pages.LoginPageModel

<form method="post">
    <div class="form-group">
        <label asp-for="@Model.UserName">Nazwa użytkownika</label>
        <input type="text" class="form-control" asp-for="@Model.UserName"/>
    </div>

    <div class="form-group">
        <label asp-for="@Model.Password" >Hasło</label>
        <input type="password" class="form-control" asp-for="@Model.Password" />
    </div>

    <div class="form-check">
        <label asp-for="@Model.RememberMe" class="form-check-label" />
        <input type="checkbox" class="form-check" asp-for="@Model.RememberMe"/>
    </div>

    <div>
        <button type="submit">Zaloguj mnie</button>
    </div>
</form>

To jest zwykły formularz ze stylami bootstrapa. Mamy trzy pola:

  • nazwa użytkownika
  • hasło
  • checkbox – pamiętaj mnie, żeby użytkownik nie musiał logować się za każdym razem

Nie stosuję tutaj żadnych walidacji, żeby nie zaciemniać obrazu.

Obsługa logowania

Teraz trzeba obsłużyć to logowanie – czyli przesłanie formularza. Do modelu strony dodaj metodę OnPostAsync (fragment kodu):

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
        return Page();

    ApplicationUser loggedUser = Authorize(UserName, Password);
    if(loggedUser == null)
    {
        TempData["Error"] = "Niepoprawne dane logowania!";
        return RedirectToPage();
    }
}

ApplicationUser Authorize(string name, string pass)
{
    if (name == "Admin" && pass == "Admin")
    {
        ApplicationUser result = new ApplicationUser();
        result.UserName = "Admin";
        result.Id = 1;

        return result;
    }
    else
        return null!;
}

W trzeciej linijce walidujemy przekazany w formularzu model. Chociaż w tym przypadku testowym nie ma czego walidować, to jednak pamiętaj o tym.

W linijce 6 następuje próba zalogowania użytkownika. Przykładowa metoda Authorize jest oczywiście beznadziejna, ale zwróć tylko uwagę na to, co robi. W jakiś sposób sprawdza, czy login i hasło są poprawne (np. wysyłając dane do WebAPI). I jeśli tak, zwraca konkretnego użytkownika. Jeśli nie można było takiego użytkownika zalogować, zwraca null.

Zawartość metody Authorize zależy całkowicie od Ciebie. W przeciwieństwie do mechanizmu Identity, tutaj sam musisz stwierdzić, czy dane logowania użytkownika są poprawne, czy nie.

W następnej linijce sprawdzam, czy udało się zalogować użytkownika. Jeśli nie, wtedy ustawiam jakiś komunikat błędu i przeładowuję tę stronę.

A co jeśli użytkownika udało się zalogować? Trzeba stworzyć dla niego ciastko logowania. Ale to wymaga utworzenia obiektu ClaimsPrincipal.

Czym jest ClaimsPrincipal?

Krótko mówiąc, jest to zbiór danych, który przechowuje informacje na temat zalogowanego użytkownika. Pewnie chcesz zadać pytanie – czy to nie może być moja super klasa User? Nie, nie może. ClaimsPrincipal to pewien standardowy sposób przechowywania i przesyłania danych.

Wyobraź sobie, że jesteś strażnikiem w dużej firmie. Teraz podchodzi do Ciebie gość, który mówi, że jest dyrektorem z innej firmy, przyszedł na spotkanie i nazywa się Jan Kowalski. Sprawdzasz jego dowód (uwierzytelniasz go) i stwierdzasz, że faktycznie nazywa się Jan Kowalski. Co więcej, możesz stwierdzić że zaiste jest dyrektorem i przyszedł na spotkanie. Wydajesz mu zatem swego rodzaju dowód tożsamości – to może być identyfikator, którym będzie się posługiwał w Twojej firmie.

Teraz tego gościa możemy przyrównać do ClaimsPrincipal, a identyfikator, który mu wydałeś to ClaimsIdentity (będące częścią ClaimsPrincipal).

Na potrzeby tego artykułu potraktuj to właśnie jako zbiór danych identyfikujących zalogowanego użytkownika.

Tworzenie tożsamości (ClaimsPrincipal)

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
        return Page();

    ApplicationUser loggedUser = Authorize(UserName, Password);
    if(loggedUser == null)
    {
        TempData["Error"] = "Niepoprawne dane logowania!";
        return RedirectToPage();
    }

    ClaimsPrincipal principal = CreatePrincipal(loggedUser);
}

ClaimsPrincipal CreatePrincipal(ApplicationUser user)
{
    ClaimsPrincipal result = new ClaimsPrincipal();

    List<Claim> claims = new List<Claim>()
    {
        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
        new Claim(ClaimTypes.Name, user.UserName)
    };

    ClaimsIdentity identity = new ClaimsIdentity(claims);
    result.AddIdentity(identity);

    return result;
}

Tutaj tworzymy tożsamość zalogowanego użytkownika i dajemy mu dwa „poświadczenia” – Id i nazwę użytkownika. Mając utworzony obiekt ClaimsPrincipal, możemy teraz utworzyć ciastko logowania. To ciastko będzie przechowywało dane z ClaimsPrincipal:

await HttpContext.SignInAsync(principal);

Pamiętaj, żeby dodać using: using Microsoft.AspNetCore.Authentication;

Teraz niepełny kod wygląda tak:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
        return Page();

    ApplicationUser loggedUser = Authorize(UserName, Password);
    if(loggedUser == null)
    {
        TempData["Error"] = "Niepoprawne dane logowania!";
        return RedirectToPage();
    }

    ClaimsPrincipal principal = CreatePrincipal(loggedUser);

    await HttpContext.SignInAsync(principal);
}

Podsumujmy tę część:

  1. Walidujesz model otrzymany z formularza
  2. W jakiś sposób sprawdzasz, czy przekazany login i hasło są prawidłowe – „ręcznie” uwierzytelniasz użytkownika
  3. Na podstawie uwierzytelnionego użytkownika tworzysz obiekt ClaimsPrincipal, który jest potrzebny do utworzenia ciastka logowania.
  4. Tworzysz ciastko logowania. Od tego momentu, w każdym żądaniu, obiekt HttpContext.User będzie miał te wartości, które utworzyłeś w kroku 3. Wszystko dzięki ciastku logowania, które przy każdym żądaniu utworzy ten obiekt na podstawie swoich wartości.

Nie musisz tutaj podawać schematu uwierzytelniania, ponieważ zdefiniowałeś domyślny schemat podczas konfiguracji uwierzytelniania.

Pamiętaj mnie

W powyższym kodzie nie ma jeszcze użytej opcji „Pamiętaj mnie”. Ta opcja musi zostać dodana podczas tworzenia ciastka logowania. Wykorzystamy tutaj przeciążoną metodę SignInAsync, która przyjmuje dwa parametry:

AuthenticationProperties props = new AuthenticationProperties();
props.IsPersistent = RememberMe;

await HttpContext.SignInAsync(principal, props);

Czyli do właściwości IsPersistent przekazałeś wartość pobraną od użytkownika, który powiedział, że chce być pamiętany w tej przeglądarce (true) lub nie (false). O tym właśnie mówi IsPersistent.

Ale ten kod wciąż nie jest pełny.

Przekierowanie po logowaniu

Po udanym (lub nieudanym) logowaniu trzeba gdzieś użytkownika przekierować. Najwygodniej dla niego – na stronę, na którą próbował się dostać przed logowaniem. Spójrz na taki przypadek:

  • niezalogowany użytkownik wchodzi na Twoją stronę, żeby zobaczyć informacje o swoim koncie: https://www.example.com/Account
  • System uwierzytelniania widzi, że ta strona wymaga poświadczeń (gdyż jest opatrzona atrybutem Authorize), a użytkownik nie jest zalogowany. Więc zostaje przekierowany na stronę logowania. A skąd wiadomo gdzie jest strona logowania? Ustawiłeś ją podczas konfiguracji ciastka do logowania.
  • Po poprawnym zalogowaniu użytkownik może zostać przekierowany np. na stronę domową: "/Index" albo lepiej – na ostatnią stronę, którą chciał odwiedzić, w tym przypadku: https://www.example.com/Account

Ale skąd masz wiedzieć, na jaką stronę go przekierować? Spójrz jeszcze raz na konfigurację ciastka logowania:

builder.Services.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
{
    o.Cookie.IsEssential = true;
    o.Cookie.HttpOnly = true;
    o.Cookie.SameSite = SameSiteMode.Strict;
    o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    o.Cookie.MaxAge = TimeSpan.FromDays(30);
    o.AccessDeniedPath = "/AccessDenied";
    o.LoginPath = "/Login";
    o.ReturnUrlParameter = "return_url";
});

Jeśli mechanizm uwierzytelniania przekierowuje Cię na stronę logowania, dodaje do adresu parametr, który skonfigurowałeś w ReturnUrlParameter. A więc w tym przypadku "return_url". Ostatecznie niezalogowany użytkownik zostanie przekierowany na taki adres: https://example.com/Login?return_url=/Account

(w przeglądarce nie zauważysz znaku „/”, tylko jego kod URL: %2F)

To znaczy, że na stronie logowania możesz ten parametr pobrać:

public class LoginPageModel : PageModel
{
    [BindProperty]
    public string UserName { get; set; } = string.Empty;
    [BindProperty]
    public string Password { get; set; } = string.Empty;

    [BindProperty]
    public bool RememberMe { get; set; }

    [FromQuery(Name = "return_url")]
    public string? ReturnUrl { get; set; }
  
    //
}

Pamiętaj, że parametru return_url nie będzie, jeśli użytkownik wchodzi bezpośrednio na stronę logowania. Dlatego też zwróć uwagę, żeby oznaczyć go jako opcjonalny – string?, a nie string

Następnie wykorzystaj go podczas logowania:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
        return Page();

    ApplicationUser loggedUser = Authorize(UserName, Password);
    if(loggedUser == null)
    {
        TempData["Error"] = "Niepoprawne dane logowania!";
        return RedirectToPage();
    }

    ClaimsPrincipal principal = CreatePrincipal(loggedUser);

    AuthenticationProperties props = new AuthenticationProperties();
    props.IsPersistent = RememberMe;

    await HttpContext.SignInAsync(principal, props);

    if (string.IsNullOrWhiteSpace(ReturnUrl))
        ReturnUrl = "/Index";

    return RedirectToPage(ReturnUrl);
}

UWAGA!

Pamiętaj, żeby w takim przypadku NIE STOSOWAĆ metody Redirect, tylko RedirectToPage (lub w RazorView – RedirectToAction). Metoda Redirect pozwala na przekierowanie do zewnętrznego serwisu, co w tym przypadku daje podatność na atak „Open Redirect”. Dlatego też stosuj RedirectToPage -> ta metoda nie pozwoli na przekierowanie zewnętrzne.

Wylogowanie

Kiedyś użytkownik być może będzie się chciał wylogować. Na czym polega wylogowanie? Na usunięciu ciastka logowania. Robi się to jedną metodą:

await HttpContext.SignOutAsync();

Ta metoda usunie ciastko logowania i w kolejnych żądaniach obiekt HttpContext.User będzie pusty.


To właściwie tyle jeśli chodzi o mechanizm uwierzytelniania. Jeśli czegoś nie rozumiesz lub znalazłeś błąd w artykule, daj znać w komentarzu. Jeśli uważasz ten artykuł za przydatny, również daj znać. Będzie mi miło 🙂 I koniecznie zapisz się na newsletter, żeby nic Cię nie ominęło.

Podziel się artykułem na: