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:

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: