Autenticazione basata su token in ASP.NET Core


161

Sto lavorando con l'applicazione ASP.NET Core. Sto cercando di implementare l'autenticazione basata su token ma non riesco a capire come utilizzare il nuovo sistema di sicurezza per il mio caso. Ho fatto degli esempi ma non mi hanno aiutato molto, stanno usando l'autenticazione con cookie o autenticazione esterna (GitHub, Microsoft, Twitter).

Qual è il mio scenario: l'applicazione angularjs dovrebbe richiedere l' /tokenURL passando username e password. WebApi dovrebbe autorizzare l'utente e restituire access_tokenche verrà utilizzato dall'app angularjs nelle seguenti richieste.

Ho trovato un ottimo articolo sull'implementazione esattamente di ciò di cui ho bisogno nell'attuale versione di ASP.NET - Autenticazione basata su token usando ASP.NET Web API 2, Owin e Identity . Ma non è ovvio per me come fare la stessa cosa in ASP.NET Core.

La mia domanda è: come configurare l'applicazione ASP.NET Core WebApi affinché funzioni con l'autenticazione basata su token?


Io avendo lo stesso problema e mi è stato piallatura a fare tutto ciò che io stesso, FYI c'è un'altra domanda stackoverflow.com/questions/29055477/... ma non anserw ancora, vediamo cosa succede
Son_of_Sam


Anche io sto affrontando lo stesso problema ma non sono ancora riuscito a trovare una soluzione ... Devo scrivere un'autenticazione personalizzata utilizzando un altro servizio che autentica il mio token.
Mayank Gupta,

Risposte:


137

Aggiornamento per .Net Core 3.1:

David Fowler (architetto del team ASP .NET Core) ha messo insieme un insieme incredibilmente semplice di applicazioni task, tra cui una semplice applicazione che dimostra JWT . Presto integrerò i suoi aggiornamenti e il suo stile semplicistico in questo post.

Aggiornato per .Net Core 2:

Le versioni precedenti di questa risposta utilizzavano RSA; non è realmente necessario se lo stesso codice che sta generando i token sta verificando anche i token. Tuttavia, se stai distribuendo la responsabilità, probabilmente vorrai comunque farlo utilizzando un'istanza di Microsoft.IdentityModel.Tokens.RsaSecurityKey.

  1. Crea alcune costanti che useremo in seguito; ecco cosa ho fatto:

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
  2. Aggiungi questo al tuo Startup.cs ConfigureServices. Utilizzeremo l'iniezione di dipendenza in seguito per accedere a queste impostazioni. Suppongo che il tuo authenticationConfigurationsia unConfigurationSectionConfiguration oggetto o tale che tu possa avere una configurazione diversa per il debug e la produzione. Assicurati di conservare la chiave in modo sicuro! Può essere qualsiasi stringa.

    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;
        });

    Ho visto altre risposte cambiare altre impostazioni, come ad esempio ClockSkew ; le impostazioni predefinite sono impostate in modo tale che dovrebbe funzionare per ambienti distribuiti i cui orologi non sono esattamente sincronizzati. Queste sono le uniche impostazioni che è necessario modificare.

  3. Imposta autenticazione. Dovresti avere questa riga prima di qualsiasi middleware che richiede le tue Userinformazioni, come ad esempio app.UseMvc().

    app.UseAuthentication();

    Notare che ciò non causerà l'emissione del token con SignInManagero altro. Dovrai fornire il tuo meccanismo per produrre il tuo JWT - vedi sotto.

  4. Potresti voler specificare un AuthorizationPolicy. Ciò consentirà di specificare controller e azioni che consentono l'utilizzo dei token Bearer solo come autenticazione [Authorize("Bearer")].

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
  5. Ecco la parte difficile: costruire il token.

    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);
        }
    }

    Quindi, nel controller in cui desideri il token, qualcosa di simile al seguente:

    [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);
    }

    Qui, presumo che tu abbia già un preside. Se stai usando Identity, puoi usare IUserClaimsPrincipalFactory<>per trasformare il tuoUser in a ClaimsPrincipal.

  6. Per testarlo : ottieni un token, inseriscilo nel modulo all'indirizzo jwt.io . Le istruzioni che ho fornito sopra ti consentono anche di utilizzare il segreto della tua configurazione per convalidare la firma!

  7. Se stavi eseguendo il rendering in una vista parziale sulla tua pagina HTML in combinazione con l'autenticazione solo al portatore in .Net 4.5, ora puoi usare a ViewComponentper fare lo stesso. È principalmente lo stesso del codice di azione del controller sopra.


1
Dovrai effettivamente iniettare IOptions<OAuthBearerAuthenticationOptions>per usare le Opzioni; l'utilizzo diretto di un oggetto Opzioni non è supportato a causa della configurazione denominata supportata dal framework Modello opzioni.
Matt DeKrey,

2
Aggiornato a quello che sto usando, anche se ora la risposta dovrebbe essere riscritta. Grazie per avermi beccato!
Matt DeKrey,

5
Da allora, il numero 5 è stato modificato come segue in Microsoft.AspNet.Authentication.OAuthBearer - beta 5 - 6 e possibilmente beta precedenti, ma non li ho confermati. auth.AddPolicy ("Bearer", nuovo AuthorizationPolicyBuilder () .AddAuthenticationSchemes (OAuthBearerAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticatedUser (). Build ());
dynamiclynk,

5
@MattDeKrey Ho usato questa risposta come punto di partenza per un esempio di semplice autorizzazione basata su token e l'ho aggiornata per funzionare con la beta 7 - vedi github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample - incorpora anche alcuni dei suggerimenti di questi commenti.
Mark Hughes,

2
Aggiornato di nuovo per RC1 - vecchie versioni per Beta7 e Beta8 disponibili nelle filiali su GitHub.
Mark Hughes,

83

Lavorando dalla favolosa risposta di Matt Dekrey , ho creato un esempio pienamente funzionante di autenticazione basata su token, lavorando con ASP.NET Core (1.0.1). Puoi trovare il codice completo in questo repository su GitHub (rami alternativi per 1.0.0-rc1 , beta8 , beta7 ), ma in breve, i passaggi importanti sono:

Genera una chiave per la tua applicazione

Nel mio esempio, creo una chiave casuale ogni volta che viene avviata l'app, dovrai generarne una e memorizzarla da qualche parte e fornirla alla tua applicazione. Vedi questo file per come sto generando una chiave casuale e come potresti importarla da un file .json . Come suggerito nei commenti di @kspearrin, l' API per la protezione dei dati sembra un candidato ideale per gestire le chiavi "correttamente", ma non ho ancora capito se è ancora possibile. Invia una richiesta pull se la risolvi!

Startup.cs - ConfigureServices

Qui, dobbiamo caricare una chiave privata per i nostri token con cui firmare, che useremo anche per verificare i token man mano che vengono presentati. Stiamo memorizzando la chiave in una variabile a livello di classe keyche riutilizzeremo nel metodo Configura di seguito. TokenAuthOptions è una classe semplice che contiene l'identità della firma, il pubblico e l'emittente di cui avremo bisogno in TokenController per creare le nostre chiavi.

// 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());
});

Abbiamo anche impostato una politica di autorizzazione per consentirci di utilizzare [Authorize("Bearer")]gli endpoint e le classi che desideriamo proteggere.

Startup.cs: configura

Qui, dobbiamo configurare 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

Nel controller token, è necessario disporre di un metodo per generare chiavi firmate utilizzando la chiave caricata in Startup.cs. Abbiamo registrato un'istanza TokenAuthOptions in Startup, quindi dobbiamo iniettarla nel costruttore per TokenController:

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

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

Quindi dovrai generare il token nel tuo gestore per l'endpoint di accesso, nel mio esempio sto prendendo un nome utente e una password e convalidando quelli usando un'istruzione if, ma la cosa chiave che devi fare è creare o caricare un reclamo identità basata e generare il token per quello:

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);
}

E questo dovrebbe essere tutto. Basta aggiungere [Authorize("Bearer")]a qualsiasi metodo o classe che si desidera proteggere e si dovrebbe ricevere un errore se si tenta di accedervi senza un token presente. Se vuoi restituire un 401 invece di un errore 500, dovrai registrare un gestore di eccezioni personalizzato come nel mio esempio qui .


1
Questo è un esempio davvero eccellente e ha incluso tutti i pezzi mancanti di cui avevo bisogno per far funzionare l'esempio di @ MattDeKrey, grazie mille! Si noti che chiunque abbia ancora preso di mira beta7 anziché beta8 può ancora trovare quell'esempio nella storia di github
nickspoon

1
Sono contento che abbia aiutato @nickspoon - se hai qualche problema, fammi sapere o fai una richiesta pull su github e aggiornerò!
Mark Hughes,

2
Grazie per questo, tuttavia non capisco bene perché qualcosa che ha funzionato all'improvviso nell'API Web ASP.Net 4 ora richiede un bel po 'di configurazione in ASP.Net 5. Sembra un passo indietro.
JMK,

1
Penso che stiano davvero spingendo "social auth" per ASP.NET 5, il che ha un senso suppongo, ma ci sono applicazioni per cui non è appropriato, quindi non sono sicuro di essere d'accordo con la loro direzione @JMK
Mark Hughes

1
@YuriyP Devo aggiornare questa risposta per RC2 - Non ho ancora aggiornato la nostra app interna che lo utilizza in RC2, quindi non sono sicuro di cosa sia coinvolto. Aggiornerò una volta individuata la differenza ...
Mark Hughes,

3

Puoi dare un'occhiata agli esempi OpenId connect che illustrano come gestire diversi meccanismi di autenticazione, inclusi i token JWT:

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

Se guardi al progetto Cordova Backend, la configurazione per l'API è così:

           // 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"
            });
        });

Anche la logica in /Providers/AuthorizationProvider.cs e il RessourceController di quel progetto meritano una visita;).

In alternativa puoi anche usare il codice seguente per validare i token (c'è anche uno snippet per farlo funzionare con 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);
                }
            };
        });

Per l'emissione di token è possibile utilizzare i pacchetti del server openId Connect in questo modo:

        // 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...;
        });

EDIT: ho implementato un'applicazione a pagina singola con implementazione di autenticazione basata su token utilizzando il framework front-end Aurelia e core ASP.NET. C'è anche una connessione persistente del segnale R. Tuttavia non ho eseguito alcuna implementazione del DB. Il codice può essere visualizzato qui: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

Spero che questo ti aiuti,

Migliore,

alex


1

Dai un'occhiata a OpenIddict: è un nuovo progetto (al momento della stesura) che semplifica la configurazione della creazione di token JWT e l'aggiornamento dei token in ASP.NET 5. La convalida dei token è gestita da altri software.

Supponendo che si utilizza Identitycon Entity Framework, l'ultima riga è quello che si aggiunge al tuo ConfigureServicesmetodo:

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

In Configure, si imposta OpenIddict per servire i token 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;
});

È inoltre possibile configurare la convalida dei token in 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/";
});

Ci sono una o due altre cose minori, come il tuo DbContext deve derivare da OpenIddictContext.

Puoi vedere una spiegazione integrale su questo post del blog: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

Una demo funzionante è disponibile all'indirizzo: https://github.com/capesean/openiddict-test

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.