Uwierzytelnianie oparte na tokenach w ASP.NET Core

161

Pracuję z aplikacją ASP.NET Core. Próbuję wdrożyć uwierzytelnianie oparte na tokenach, ale nie mogę dowiedzieć się, jak używać nowego systemu zabezpieczeń w mojej sprawie. Przejrzałem przykłady, ale niewiele mi pomogły, używają uwierzytelniania plików cookie lub uwierzytelniania zewnętrznego (GitHub, Microsoft, Twitter).

Jaki jest mój scenariusz: aplikacja angularjs powinna zażądać adresu /tokenURL przekazującego nazwę użytkownika i hasło. WebApi powinno autoryzować użytkownika i zwrot, access_tokenktóry będzie używany przez aplikację angularjs w kolejnych żądaniach.

Znalazłem świetny artykuł o wdrażaniu dokładnie tego, czego potrzebuję w aktualnej wersji ASP.NET - uwierzytelnianie oparte na tokenach przy użyciu ASP.NET Web API 2, Owin i Identity . Ale nie jest dla mnie oczywiste, jak zrobić to samo w ASP.NET Core.

Moje pytanie brzmi: jak skonfigurować aplikację ASP.NET Core WebApi do pracy z uwierzytelnianiem opartym na tokenach?

Dotacja
źródło
Mam ten sam problem i planowałem zrobić to wszystko samodzielnie, do Twojej wiadomości jest jeszcze jedno pytanie stackoverflow.com/questions/29055477/… ale nie ma jeszcze odpowiedzi, zobaczmy, co się stanie
Son_of_Sam
Mam również ten sam problem, ale nie mogę jeszcze znaleźć rozwiązania ... Muszę napisać niestandardowe uwierzytelnianie za pomocą innej usługi, która uwierzytelnia mój token.
Mayank Gupta

Odpowiedzi:

137

Aktualizacja dla .Net Core 3.1:

David Fowler (architekt zespołu ASP .NET Core) stworzył niezwykle prosty zestaw aplikacji zadaniowych, w tym prostą aplikację demonstrującą JWT . Wkrótce wprowadzę jego aktualizacje i uproszczony styl do tego postu.

Zaktualizowano dla .Net Core 2:

Poprzednie wersje tej odpowiedzi wykorzystywały RSA; naprawdę nie jest to konieczne, jeśli ten sam kod, który generuje tokeny, również weryfikuje tokeny. Jeśli jednak rozdzielasz odpowiedzialność, prawdopodobnie nadal chcesz to zrobić przy użyciu wystąpienia Microsoft.IdentityModel.Tokens.RsaSecurityKey.

  1. Utwórz kilka stałych, których będziemy używać później; oto co zrobiłem:

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
  2. Dodaj to do swojego Startup.cs ConfigureServices. Później użyjemy iniekcji zależności, aby uzyskać dostęp do tych ustawień. Zakładam, że twój authenticationConfigurationto obiekt ConfigurationSectionlub Configurationtaki, że możesz mieć inną konfigurację do debugowania i produkcji. Upewnij się, że przechowujesz klucz w bezpiecznym miejscu! Może to być dowolny ciąg.

    var keySecret = authenticationConfiguration["JwtSigningKey"];
    var symmetricKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keySecret));
    
    services.AddTransient(_ => new JwtSignInHandler(symmetricKey));
    
    services.AddAuthentication(options =>
    {
        // This causes the default authentication scheme to be JWT.
        // Without this, the Authorization header is not checked and
        // you'll get no results. However, this also means that if
        // you're already using cookies in your app, they won't be 
        // checked by default.
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters.ValidateIssuerSigningKey = true;
            options.TokenValidationParameters.IssuerSigningKey = symmetricKey;
            options.TokenValidationParameters.ValidAudience = JwtSignInHandler.TokenAudience;
            options.TokenValidationParameters.ValidIssuer = JwtSignInHandler.TokenIssuer;
        });

    Widziałem inne odpowiedzi zmieniające inne ustawienia, takie jak ClockSkew; wartości domyślne są ustawione tak, że powinno działać w środowiskach rozproszonych, w których zegary nie są dokładnie zsynchronizowane. To jedyne ustawienia, które musisz zmienić.

  3. Skonfiguruj uwierzytelnianie. Powinieneś mieć ten wiersz przed jakimkolwiek oprogramowaniem pośredniczącym, które wymaga twoich Userinformacji, takim jak app.UseMvc().

    app.UseAuthentication();

    Pamiętaj, że nie spowoduje to wyemitowania tokenu z rozszerzeniem SignInManager lub czymkolwiek innym. Będziesz musiał zapewnić własny mechanizm wyprowadzania tokenów JWT - patrz poniżej.

  4. Możesz chcieć określić AuthorizationPolicy. Umożliwi to określenie kontrolerów i akcji, które zezwalają tylko na tokeny okaziciela jako uwierzytelnianie przy użyciu [Authorize("Bearer")].

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
  5. Oto trudna część: zbudowanie tokena.

    class JwtSignInHandler
    {
        public const string TokenAudience = "Myself";
        public const string TokenIssuer = "MyProject";
        private readonly SymmetricSecurityKey key;
    
        public JwtSignInHandler(SymmetricSecurityKey symmetricKey)
        {
            this.key = symmetricKey;
        }
    
        public string BuildJwt(ClaimsPrincipal principal)
        {
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    
            var token = new JwtSecurityToken(
                issuer: TokenIssuer,
                audience: TokenAudience,
                claims: principal.Claims,
                expires: DateTime.Now.AddMinutes(20),
                signingCredentials: creds
            );
    
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }

    Następnie w kontrolerze, w którym chcesz mieć swój token, na przykład:

    [HttpPost]
    public string AnonymousSignIn([FromServices] JwtSignInHandler tokenFactory)
    {
        var principal = new System.Security.Claims.ClaimsPrincipal(new[]
        {
            new System.Security.Claims.ClaimsIdentity(new[]
            {
                new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "Demo User")
            })
        });
        return tokenFactory.BuildJwt(principal);
    }

    Tutaj zakładam, że masz już dyrektora. Jeśli używasz tożsamości, możesz użyć, IUserClaimsPrincipalFactory<>aby przekształcić swój Userplik w ClaimsPrincipal.

  6. By to sprawdzić : Get token, umieścić go w formularzu na jwt.io . Instrukcje, które podałem powyżej, pozwalają również na użycie sekretu z konfiguracji do weryfikacji podpisu!

  7. Jeśli renderowałeś to w częściowym widoku na swojej stronie HTML w połączeniu z uwierzytelnianiem tylko na okaziciela w .Net 4.5, możesz teraz użyć a, ViewComponentaby zrobić to samo. Jest w większości taki sam jak powyższy kod akcji kontrolera.

Matt DeKrey
źródło
1
Będziesz musiał faktycznie wstrzyknąć, IOptions<OAuthBearerAuthenticationOptions>aby użyć opcji; używanie obiektu Options bezpośrednio nie jest obsługiwane ze względu na nazwaną konfigurację obsługiwaną przez strukturę modelu opcji.
Matt DeKrey
2
Zaktualizowano do tego, czego używam, chociaż teraz odpowiedź powinna zostać przepisana. Dzięki za szturchnięcie mnie!
Matt DeKrey
5
Od tego czasu punkt 5 został zmieniony na następujący w Microsoft.AspNet.Authentication.OAuthBearer - beta 5 - 6 i prawdopodobnie we wcześniejszych wersjach beta, ale ich nie potwierdzono. auth.AddPolicy ("Bearer", new AuthorizationPolicyBuilder () .AddAuthenticationSchemes (OAuthBearerAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticatedUser (). Build ());
dynamiclynk
5
@MattDeKrey Użyłem tej odpowiedzi jako punktu wyjścia dla przykładu prostego uwierzytelniania opartego na tokenach i zaktualizowałem ją, aby działała przeciwko beta 7 - zobacz github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample - również zawiera kilka wskazówek z tych komentarzy.
Mark Hughes
2
Zaktualizowano ponownie dla RC1 - stare wersje dla Beta7 i Beta8 dostępnej w oddziałach na GitHub.
Mark Hughes,
83

Praca z bajecznym odpowiedź Matt Dekrey za , I utworzeniu przykład pełni funkcjonalny uwierzytelniania tokenów, działa przeciwko ASP.NET rdzeń (1.0.1). Możesz znaleźć pełny kod w tym repozytorium na GitHub (alternatywne gałęzie dla 1.0.0-rc1 , beta8 , beta7 ), ale w skrócie, ważne kroki to:

Wygeneruj klucz dla swojej aplikacji

W moim przykładzie generuję losowy klucz za każdym razem, gdy aplikacja się uruchamia, musisz go wygenerować i gdzieś przechowywać i dostarczyć do swojej aplikacji. Zobacz ten plik, aby dowiedzieć się, jak generuję klucz losowy i jak możesz go zaimportować z pliku .json . Jak zasugerował w komentarzach @kspearrin, API ochrony danych wydaje się idealnym kandydatem do „prawidłowego” zarządzania kluczami, ale nie doszedłem jeszcze do wniosku, czy to możliwe. Prześlij żądanie ściągnięcia, jeśli to rozwiązujesz!

Startup.cs - ConfigureServices

Tutaj musimy załadować klucz prywatny do podpisania naszych tokenów, którego użyjemy również do weryfikacji tokenów, gdy są prezentowane. Przechowujemy klucz w zmiennej na poziomie klasy, keyktórej użyjemy ponownie w poniższej metodzie Configure. TokenAuthOptions to prosta klasa, która przechowuje tożsamość podpisu, odbiorców i wystawcę, których będziemy potrzebować w kontrolerze TokenController do tworzenia naszych kluczy.

// Replace this with some sort of loading from config / file.
RSAParameters keyParams = RSAKeyUtils.GetRandomKey();

// Create the key, and a set of token options to record signing credentials 
// using that key, along with the other parameters we will need in the 
// token controlller.
key = new RsaSecurityKey(keyParams);
tokenOptions = new TokenAuthOptions()
{
    Audience = TokenAudience,
    Issuer = TokenIssuer,
    SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.Sha256Digest)
};

// Save the token options into an instance so they're accessible to the 
// controller.
services.AddSingleton<TokenAuthOptions>(tokenOptions);

// Enable the use of an [Authorize("Bearer")] attribute on methods and
// classes to protect.
services.AddAuthorization(auth =>
{
    auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
        .RequireAuthenticatedUser().Build());
});

Skonfigurowaliśmy również zasady autoryzacji, aby umożliwić nam używanie [Authorize("Bearer")]na punktach końcowych i klasach, które chcemy chronić.

Startup.cs - Skonfiguruj

Tutaj musimy skonfigurować JwtBearerAuthentication:

app.UseJwtBearerAuthentication(new JwtBearerOptions {
    TokenValidationParameters = new TokenValidationParameters {
        IssuerSigningKey = key,
        ValidAudience = tokenOptions.Audience,
        ValidIssuer = tokenOptions.Issuer,

        // When receiving a token, check that it is still valid.
        ValidateLifetime = true,

        // This defines the maximum allowable clock skew - i.e.
        // provides a tolerance on the token expiry time 
        // when validating the lifetime. As we're creating the tokens 
        // locally and validating them on the same machines which 
        // should have synchronised time, this can be set to zero. 
        // Where external tokens are used, some leeway here could be 
        // useful.
        ClockSkew = TimeSpan.FromMinutes(0)
    }
});

TokenController

W kontrolerze tokenów musisz mieć metodę generowania podpisanych kluczy przy użyciu klucza załadowanego w Startup.cs. Zarejestrowaliśmy wystąpienie TokenAuthOptions w Startup, więc musimy wstrzyknąć to w konstruktorze dla TokenController:

[Route("api/[controller]")]
public class TokenController : Controller
{
    private readonly TokenAuthOptions tokenOptions;

    public TokenController(TokenAuthOptions tokenOptions)
    {
        this.tokenOptions = tokenOptions;
    }
...

Następnie musisz wygenerować token w programie obsługi dla punktu końcowego logowania, w moim przykładzie biorę nazwę użytkownika i hasło i sprawdzam je za pomocą instrukcji if, ale kluczową rzeczą, którą musisz zrobić, jest utworzenie lub załadowanie oświadczeń opartą na tożsamości i wygeneruj w tym celu token:

public class AuthRequest
{
    public string username { get; set; }
    public string password { get; set; }
}

/// <summary>
/// Request a new token for a given username/password pair.
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
[HttpPost]
public dynamic Post([FromBody] AuthRequest req)
{
    // Obviously, at this point you need to validate the username and password against whatever system you wish.
    if ((req.username == "TEST" && req.password == "TEST") || (req.username == "TEST2" && req.password == "TEST"))
    {
        DateTime? expires = DateTime.UtcNow.AddMinutes(2);
        var token = GetToken(req.username, expires);
        return new { authenticated = true, entityId = 1, token = token, tokenExpires = expires };
    }
    return new { authenticated = false };
}

private string GetToken(string user, DateTime? expires)
{
    var handler = new JwtSecurityTokenHandler();

    // Here, you should create or look up an identity for the user which is being authenticated.
    // For now, just creating a simple generic identity.
    ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user, "TokenAuth"), new[] { new Claim("EntityID", "1", ClaimValueTypes.Integer) });

    var securityToken = handler.CreateToken(new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor() {
        Issuer = tokenOptions.Issuer,
        Audience = tokenOptions.Audience,
        SigningCredentials = tokenOptions.SigningCredentials,
        Subject = identity,
        Expires = expires
    });
    return handler.WriteToken(securityToken);
}

I to powinno być to. Po prostu dodaj [Authorize("Bearer")]do dowolnej metody lub klasy, którą chcesz chronić, a przy próbie uzyskania do niej dostępu bez tokena powinien pojawić się błąd. Jeśli chcesz zwrócić błąd 401 zamiast błędu 500, musisz zarejestrować niestandardową procedurę obsługi wyjątków, tak jak w moim przykładzie tutaj .

Mark Hughes
źródło
1
To jest naprawdę doskonały przykład i zawiera wszystkie brakujące elementy, których potrzebowałem, aby przykład @ MattDeKrey działał, wielkie dzięki! Zauważ, że każdy, kto nadal celuje w beta7 zamiast beta8, wciąż może znaleźć ten przykład w historii github
nickspoon
1
Cieszę się, że pomogło @nickspoon - jeśli masz z tym jakiekolwiek problemy, daj mi znać lub wyślij żądanie ściągnięcia na github, a zaktualizuję!
Mark Hughes,
2
Dzięki za to, jednak nie do końca rozumiem, dlaczego coś, co działało po wyjęciu z pudełka w ASP.Net 4 Web API, wymaga teraz sporo konfiguracji w ASP.Net 5. Wydaje się, że jest to krok wstecz.
JMK
1
Myślę, że naprawdę naciskają na „social auth” dla ASP.NET 5, co wydaje mi się sensowne, ale są aplikacje, które nie są odpowiednie, więc nie jestem pewien, czy zgadzam się z ich kierunkiem @JMK
Mark Hughes
1
@YuriyP Muszę zaktualizować tę odpowiedź dla RC2 - Nie zaktualizowałem jeszcze naszej wewnętrznej aplikacji, która korzysta z tego do RC2, więc nie jestem pewien, o co chodzi. Zaktualizuję, gdy
Mark Hughes,
3

Możesz rzucić okiem na przykłady OpenId connect, które ilustrują, jak radzić sobie z różnymi mechanizmami uwierzytelniania, w tym tokenami JWT:

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Samples

Jeśli spojrzysz na projekt Cordova Backend, konfiguracja interfejsu API wygląda następująco:

           // Create a new branch where the registered middleware will be executed only for non API calls.
        app.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api")), branch => {
            // Insert a new cookies middleware in the pipeline to store
            // the user identity returned by the external identity provider.
            branch.UseCookieAuthentication(new CookieAuthenticationOptions {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                AuthenticationScheme = "ServerCookie",
                CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie",
                ExpireTimeSpan = TimeSpan.FromMinutes(5),
                LoginPath = new PathString("/signin"),
                LogoutPath = new PathString("/signout")
            });

            branch.UseGoogleAuthentication(new GoogleOptions {
                ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
                ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"
            });

            branch.UseTwitterAuthentication(new TwitterOptions {
                ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g",
                ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"
            });
        });

Warto też przyjrzeć się logice w /Providers/AuthorizationProvider.cs i RessourceController tego projektu;).

Alternatywnie możesz również użyć następującego kodu do weryfikacji tokenów (istnieje również fragment kodu, aby działał z signalR):

        // Add a new middleware validating access tokens.
        app.UseOAuthValidation(options =>
        {
            // Automatic authentication must be enabled
            // for SignalR to receive the access token.
            options.AutomaticAuthenticate = true;

            options.Events = new OAuthValidationEvents
            {
                // Note: for SignalR connections, the default Authorization header does not work,
                // because the WebSockets JS API doesn't allow setting custom parameters.
                // To work around this limitation, the access token is retrieved from the query string.
                OnRetrieveToken = context =>
                {
                    // Note: when the token is missing from the query string,
                    // context.Token is null and the JWT bearer middleware will
                    // automatically try to retrieve it from the Authorization header.
                    context.Token = context.Request.Query["access_token"];

                    return Task.FromResult(0);
                }
            };
        });

Do wystawienia tokena możesz użyć pakietów serwerowych openId Connect w następujący sposób:

        // Add a new middleware issuing access tokens.
        app.UseOpenIdConnectServer(options =>
        {
            options.Provider = new AuthenticationProvider();
            // Enable the authorization, logout, token and userinfo endpoints.
            //options.AuthorizationEndpointPath = "/connect/authorize";
            //options.LogoutEndpointPath = "/connect/logout";
            options.TokenEndpointPath = "/connect/token";
            //options.UserinfoEndpointPath = "/connect/userinfo";

            // Note: if you don't explicitly register a signing key, one is automatically generated and
            // persisted on the disk. If the key cannot be persisted, an exception is thrown.
            // 
            // On production, using a X.509 certificate stored in the machine store is recommended.
            // You can generate a self-signed certificate using Pluralsight's self-cert utility:
            // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.zip
            // 
            // options.SigningCredentials.AddCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75");
            // 
            // Alternatively, you can also store the certificate as an embedded .pfx resource
            // directly in this assembly or in a file published alongside this project:
            // 
            // options.SigningCredentials.AddCertificate(
            //     assembly: typeof(Startup).GetTypeInfo().Assembly,
            //     resource: "Nancy.Server.Certificate.pfx",
            //     password: "Owin.Security.OpenIdConnect.Server");

            // Note: see AuthorizationController.cs for more
            // information concerning ApplicationCanDisplayErrors.
            options.ApplicationCanDisplayErrors = true // in dev only ...;
            options.AllowInsecureHttp = true // in dev only...;
        });

EDYCJA: Zaimplementowałem aplikację jednostronicową z implementacją uwierzytelniania opartą na tokenach przy użyciu frameworka Aurelia i rdzenia ASP.NET. Istnieje również trwałe połączenie sygnału R. Jednak nie wykonałem żadnej implementacji DB. Kod można zobaczyć tutaj: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

Mam nadzieję że to pomoże,

Najlepsza,

Alex

Darxtar
źródło
1

Spójrz na OpenIddict - to nowy projekt (w momencie pisania), który ułatwia konfigurację tworzenia tokenów JWT i tokenów odświeżania w ASP.NET 5. Walidacja tokenów jest obsługiwana przez inne oprogramowanie.

Zakładając, że używasz Identitywith Entity Framework, ostatnia linia jest tym, co dodasz do swojej ConfigureServicesmetody:

services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddOpenIddictCore<Application>(config => config.UseEntityFramework());

W programie Configurekonfigurujesz OpenIddict do obsługi tokenów JWT:

app.UseOpenIddictCore(builder =>
{
    // tell openiddict you're wanting to use jwt tokens
    builder.Options.UseJwtTokens();
    // NOTE: for dev consumption only! for live, this is not encouraged!
    builder.Options.AllowInsecureHttp = true;
    builder.Options.ApplicationCanDisplayErrors = true;
});

Konfigurujesz również walidację tokenów w Configure:

// use jwt bearer authentication
app.UseJwtBearerAuthentication(options =>
{
    options.AutomaticAuthenticate = true;
    options.AutomaticChallenge = true;
    options.RequireHttpsMetadata = false;
    options.Audience = "http://localhost:58292/";
    options.Authority = "http://localhost:58292/";
});

Jest jeszcze jedna lub dwie inne pomniejsze rzeczy, takie jak DbContext musi pochodzić z OpenIddictContext.

Pełne wyjaśnienie można znaleźć w tym poście na blogu: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

Funkcjonujące demo jest dostępne pod adresem : https://github.com/capesean/openiddict-test

Sean
źródło