JWT unter .NET Core 2.0

83

Ich war auf einem ziemlichen Abenteuer, um JWT an DotNet Core 2.0 zum Laufen zu bringen (das heute die endgültige Version erreicht). Es gibt eine Menge Dokumentation, aber der gesamte Beispielcode scheint veraltete APIs zu verwenden und kommt neu in Core. Es ist schwindelerregend herauszufinden, wie genau er implementiert werden soll. Ich habe versucht, Jose zu verwenden, aber App. UseJwtBearerAuthentication ist veraltet und es gibt keine Dokumentation darüber, was als nächstes zu tun ist.

Hat jemand ein Open Source-Projekt, das Dotnet Core 2.0 verwendet, das einfach eine JWT aus dem Autorisierungsheader analysieren und mir erlauben kann, Anforderungen für ein HS256-codiertes JWT-Token zu autorisieren?

Die folgende Klasse wirft keine Ausnahmen aus, aber es sind keine Anfragen autorisiert, und ich erhalte keinen Hinweis darauf, warum sie nicht autorisiert sind. Die Antworten sind leere 401, was für mich bedeutet, dass es keine Ausnahme gab, aber dass das Geheimnis nicht übereinstimmt.

Eine seltsame Sache ist, dass meine Token mit dem HS256-Algorithmus verschlüsselt sind, aber ich sehe keinen Indikator, der ihn anweist, ihn zu zwingen, diesen Algorithmus irgendwo zu verwenden.

Hier ist die Klasse, die ich bisher habe:

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json.Linq;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace Site.Authorization
{
    public static class SiteAuthorizationExtensions
    {
        public static IServiceCollection AddSiteAuthorization(this IServiceCollection services)
        {
            var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("SECRET_KEY"));

            var tokenValidationParameters = new TokenValidationParameters
            {
                // The signing key must match!
                ValidateIssuerSigningKey = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                IssuerSigningKeys = new List<SecurityKey>{ signingKey },


                // Validate the token expiry
                ValidateLifetime = true,
            };

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;


            })

            .AddJwtBearer(o =>
            {
                o.IncludeErrorDetails = true;
                o.TokenValidationParameters  = tokenValidationParameters;
                o.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();

                        c.Response.StatusCode = 401;
                        c.Response.ContentType = "text/plain";

                        return c.Response.WriteAsync(c.Exception.ToString());
                    }

                };
            });

            return services;
        }
    }
}
Michael Draper
quelle
Ich habe die Signaturvalidierung ohne Änderung deaktiviert, alle Anfragen mit [Authorize] 401
Michael Draper
2
Könnten Sie den vollständigen Code posten? oder ein Demo-Projekt Ich würde gerne sehen, wie Sie dies zum
Laufen gebracht haben
Ich brauche Code für die Ausgabe von Token von Controller
Alerya
Es gibt noch einen weiteren Beitrag, der Ihnen helfen kann. stackoverflow.com/a/48295906/8417618
Marco Barbero

Antworten:

87

Hier ist ein voll funktionsfähiges Minimalbeispiel mit einem Controller. Ich hoffe, Sie können es mit Postman oder JavaScript-Aufruf überprüfen.

  1. appsettings.json, appsettings.Development.json. Fügen Sie einen Abschnitt hinzu. Hinweis: Der Schlüssel sollte ziemlich lang sein und der Aussteller ist eine Adresse des Dienstes:

    ...
    ,"Tokens": {
        "Key": "Rather_very_long_key",
        "Issuer": "http://localhost:56268/"
    }
    ...

    !!! Behalten Sie in einem realen Projekt den Schlüssel nicht in der Datei appsettings.json. Es sollte in der Umgebungsvariablen gehalten werden und so aussehen:

    Environment.GetEnvironmentVariable("JWT_KEY");

UPDATE : Um zu sehen, wie die .net-Kerneinstellungen funktionieren, müssen Sie sie nicht genau aus der Umgebung übernehmen. Sie können die Einstellung verwenden. Stattdessen schreiben wir diese Variable möglicherweise in Umgebungsvariablen in der Produktion. Dann bevorzugt unser Code Umgebungsvariablen anstelle der Konfiguration.

  1. AuthRequest.cs: Um Werte für die Übergabe von Login und Passwort beizubehalten:

    public class AuthRequest
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
  2. Startup.cs in der Configure () -Methode VOR app.UseMvc ():

    app.UseAuthentication();
  3. Startup.cs in ConfigureServices ():

    services.AddAuthentication()
        .AddJwtBearer(cfg =>
        {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
    
            cfg.TokenValidationParameters = new TokenValidationParameters()
            {
                ValidIssuer = Configuration["Tokens:Issuer"],
                ValidAudience = Configuration["Tokens:Issuer"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
            };
    
        });
  4. Fügen Sie einen Controller hinzu:

        [Route("api/[controller]")]
        public class TokenController : Controller
        {
            private readonly IConfiguration _config;
            private readonly IUserManager _userManager;
    
            public TokenController(IConfiguration configuration, IUserManager userManager)
            {
                _config = configuration;
                _userManager = userManager;
            }
    
            [HttpPost("")]
            [AllowAnonymous]
            public IActionResult Login([FromBody] AuthRequest authUserRequest)
            {
                var user = _userManager.FindByEmail(model.UserName);
    
                if (user != null)
                {
                    var checkPwd = _signInManager.CheckPasswordSignIn(user, model.authUserRequest);
                    if (checkPwd)
                    {
                        var claims = new[]
                        {
                            new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                            new Claim(JwtRegisteredClaimNames.Jti, user.Id.ToString()),
                        };
    
                        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
                        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    
                        var token = new JwtSecurityToken(_config["Tokens:Issuer"],
                        _config["Tokens:Issuer"],
                        claims,
                        expires: DateTime.Now.AddMinutes(30),
                        signingCredentials: creds);
    
                        return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
                    }
                }
    
                return BadRequest("Could not create token");
            }}

Das war's Leute! Prost!

UPDATE: Die Leute fragen, wie sie den aktuellen Benutzer bekommen. Machen:

  1. In Startup.cs in ConfigureServices () hinzufügen

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
  2. In einem Controller zum Konstruktor hinzufügen:

    private readonly int _currentUser;
    public MyController(IHttpContextAccessor httpContextAccessor)
    {
       _currentUser = httpContextAccessor.CurrentUser();
    }
  3. Fügen Sie irgendwo eine Erweiterung hinzu und verwenden Sie sie in Ihrem Controller (mit ....)

    public static class IHttpContextAccessorExtension
    {
        public static int CurrentUser(this IHttpContextAccessor httpContextAccessor)
        {
            var stringId = httpContextAccessor?.HttpContext?.User?.FindFirst(JwtRegisteredClaimNames.Jti)?.Value;
            int.TryParse(stringId ?? "0", out int userId);
    
            return userId;
        }
    }
Alerya
quelle
1
Das war sehr hilfreich für mich. Das einzige, was mir noch unklar ist, ist, wie das Token auf nachfolgende Anrufe überprüft wird oder wie ermittelt wird, wer der aktuell angemeldete Benutzer ist.
Travesty3
1
Alter, das war eine große Hilfe, vielen Dank für die ausführliche Antwort!
Ryanman
1
@alerya Ich habe eine Frage. Tolles Beispiel, aber es sollte nicht auch AspNetUserLogins und AspNetUserTokens füllen? Ich habe alles eingerichtet, Token wird als Ergebnis gesendet, aber diese beiden Tabellen sind immer noch leer und konnten keinen Grund dafür finden.
Alexandru Stefan
1
Vielen Dank für die folgenden Informationen. Es rettete mich vor 401 nicht autorisierten Fehlern. 3.Startup.cs in der Configure () -Methode BEVOR app.UseMvc (): app.UseAuthentication ();
Sumia
1
Dies ist ein Widerspruch. Wir sollten den Schlüssel nicht in der App-Einstellungsdatei speichern, sondern ihn hier abrufen. Konfiguration ["Tokens: Key"]). Wenn wir einen anderen Dienst haben, der den Schlüssel sicher abruft, wie würden wir diesen in ConfigureServices integrieren?
War Gravy
18

Meine tokenValidationParametersArbeiten, wenn sie so aussehen:

 var tokenValidationParameters = new TokenValidationParameters
  {
      ValidateIssuerSigningKey = true,
      IssuerSigningKey = GetSignInKey(),
      ValidateIssuer = true,
      ValidIssuer = GetIssuer(),
      ValidateAudience = true,
      ValidAudience = GetAudience(),
      ValidateLifetime = true,
      ClockSkew = TimeSpan.Zero
   };

und

    static private SymmetricSecurityKey GetSignInKey()
    {
        const string secretKey = "very_long_very_secret_secret";
        var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));

        return signingKey;
    }

    static private string GetIssuer()
    {
        return "issuer";
    }

    static private string GetAudience()
    {
        return "audience";
    }

Fügen Sie außerdem options.RequireHttpsMetadata = false wie folgt hinzu:

         .AddJwtBearer(options =>
       {         
           options.TokenValidationParameters =tokenValidationParameters         
           options.RequireHttpsMetadata = false;
       });

EDIT :

Vergessen Sie nicht anzurufen

 app.UseAuthentication();

in Startup.cs -> Methode vor app.UseMvc () konfigurieren ;

Adrian Księżarczyk
quelle
Ich wette, es ist die App.UseAuthentication (); Anruf, der den Trick macht, ich wusste nicht, dass ich das brauchte. Danke dir!
Michael Draper
Ich denke, Sie müssen auch ValidAudience, ValidIssuer und IssuerSigningKey angeben. Es hat nicht ohne es für mich
funktioniert
@ AdrianKsiężarczyk hast du ein vollständiges Beispiel, das du posten kannst? Wie kann ich das Token erstellen und dabei zB nach einem Anmeldevorgang. Zuvor hatte ich einen ApplicationOAuthProvider und einen GrantResourceOwnerCredentials.
Piotr Stulinski
3
+1 für eine app.UseAuthentication();Notiz, die vorher aufgerufen wurde, app.UseMvc();wenn Sie dies nicht tun, erhalten Sie 401, auch wenn der Token erfolgreich autorisiert wurde - ich habe ungefähr 2 Tage damit verbracht, diesen zu erarbeiten!
PCDEV
1
"app.UseAuthentication ();", habe ich einen ganzen Tag damit verbracht, das 401-Problem zu beheben, nachdem ich den .net-Kern von 1.0 auf 2.0 aktualisiert habe, aber die Lösung erst gefunden, als ich diesen Beitrag gesehen habe. Danke Adrian.
Chan
7

Hier ist eine Lösung für Sie.

Konfigurieren Sie es in Ihrer startup.cs zunächst als Dienste:

  services.AddAuthentication().AddJwtBearer(cfg =>
        {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
            cfg.TokenValidationParameters = new TokenValidationParameters()
            {
                IssuerSigningKey = "somethong",
                ValidAudience = "something",
                :
            };
        });

Zweitens rufen Sie diese Dienste in config auf

          app.UseAuthentication();

Jetzt können Sie es in Ihrem Controller verwenden, indem Sie ein Attribut hinzufügen

          [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
          [HttpGet]
          public IActionResult GetUserInfo()
          {

Ausführliche Informationen zum Quellcode, der Angular als Frond-End verwendet, finden Sie hier

Langes Feld
quelle
Dies war die Antwort, die meinen Speck rettete! Es wäre schön, nur [Authorize] verwenden zu können. Stellen Sie sich vor, dies kann mit Startup.cs
Simon
1
Simon, weil Sie mehr als ein Schema in derselben asp.net-Kern-MVC-Anwendung haben können, wie services.AddAuthentication (). AddCookie (). AddJwtBearer ();
Long Field
1
Sie können auch ein Standardauthentifizierungsschema über die services.AddAuthorizationFunktion beim Start festlegen .
Neville Nazerane
Um die Anweisung von @NevilleNazerane weiterzuverfolgen, wird der Code zum Festlegen eines Standardauthentifizierungsschemas (das mit einem einfachen [Authorize] -Dekorator verwendet wird) für diese Frage beantwortet. Es ist services.AddAuthentication (sharedOptions => {sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;})
Ryanman
Wenn ich versuche, diesem Beispiel für den IssuerSigningKey zu folgen, wird eine Fehlermeldung angezeigt. Der Quelltyp 'Zeichenfolge' kann nicht in den Zieltyp 'Microsoft.IdentityModel.Tokens.SecurityKey' konvertiert werden
Kirsten Greed
7

Implementierung der Asp.net Core 2.0 JWT Bearer Token-Authentifizierung mit Web Api Demo

Paket hinzufügen " Microsoft.AspNetCore.Authentication.JwtBearer "

Startup.cs ConfigureServices ()

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;

                cfg.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidIssuer = "me",
                    ValidAudience = "you",
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")) //Secret
                };

            });

Startup.cs Configure ()

// ===== Use Authentication ======
        app.UseAuthentication();

User.cs // Es ist nur eine Modellklasse, zum Beispiel. Es kann alles sein.

public class User
{
    public Int32 Id { get; set; }
    public string Username { get; set; }
    public string Country { get; set; }
    public string Password { get; set; }
}

UserContext.cs // Es ist nur eine Kontextklasse . Es kann alles sein.

public class UserContext : DbContext
{
    public UserContext(DbContextOptions<UserContext> options) : base(options)
    {
        this.Database.EnsureCreated();
    }

    public DbSet<User> Users { get; set; }
}

AccountController.cs

[Route("[controller]")]
public class AccountController : Controller
{

    private readonly UserContext _context;

    public AccountController(UserContext context)
    {
        _context = context;
    }

    [AllowAnonymous]
    [Route("api/token")]
    [HttpPost]
    public async Task<IActionResult> Token([FromBody]User user)
    {
        if (!ModelState.IsValid) return BadRequest("Token failed to generate");
        var userIdentified = _context.Users.FirstOrDefault(u => u.Username == user.Username);
            if (userIdentified == null)
            {
                return Unauthorized();
            }
            user = userIdentified;

        //Add Claims
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.UniqueName, "data"),
            new Claim(JwtRegisteredClaimNames.Sub, "data"),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")); //Secret
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken("me",
            "you",
            claims,
            expires: DateTime.Now.AddMinutes(30),
            signingCredentials: creds);

        return Ok(new
        {
            access_token = new JwtSecurityTokenHandler().WriteToken(token),
            expires_in = DateTime.Now.AddMinutes(30),
            token_type = "bearer"
        });
    }
}

UserController.cs

[Authorize]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private readonly UserContext _context;

    public UserController(UserContext context)
    {
        _context = context;
        if(_context.Users.Count() == 0 )
        {
            _context.Users.Add(new User { Id = 0, Username = "Abdul Hameed Abdul Sattar", Country = "Indian", Password = "123456" });
            _context.SaveChanges();
        }
    }

    [HttpGet("[action]")]
    public IEnumerable<User> GetList()
    {
        return _context.Users.ToList();
    }

    [HttpGet("[action]/{id}", Name = "GetUser")]
    public IActionResult GetById(long id)
    {
        var user = _context.Users.FirstOrDefault(u => u.Id == id);
        if(user == null)
        {
            return NotFound();
        }
        return new ObjectResult(user);
    }


    [HttpPost("[action]")]
    public IActionResult Create([FromBody] User user)
    {
        if(user == null)
        {
            return BadRequest();
        }

        _context.Users.Add(user);
        _context.SaveChanges();

        return CreatedAtRoute("GetUser", new { id = user.Id }, user);

    }

    [HttpPut("[action]/{id}")]
    public IActionResult Update(long id, [FromBody] User user)
    {
        if (user == null)
        {
            return BadRequest();
        }

        var userIdentified = _context.Users.FirstOrDefault(u => u.Id == id);
        if (userIdentified == null)
        {
            return NotFound();
        }

        userIdentified.Country = user.Country;
        userIdentified.Username = user.Username;

        _context.Users.Update(userIdentified);
        _context.SaveChanges();
        return new NoContentResult();
    }


    [HttpDelete("[action]/{id}")]
    public IActionResult Delete(long id)
    {
        var user = _context.Users.FirstOrDefault(u => u.Id == id);
        if (user == null)
        {
            return NotFound();
        }

        _context.Users.Remove(user);
        _context.SaveChanges();

        return new NoContentResult();
    }
}

Test auf PostMan: Sie erhalten als Antwort ein Token.

Übergeben Sie TokenType und AccessToken im Header in anderen Webservices. Geben Sie hier die Bildbeschreibung ein

Viel Glück! Ich bin nur Anfänger. Ich habe nur eine Woche damit verbracht, asp.net core zu lernen.

Abdul Hameed
quelle
Ich erhalte die InvalidOperationException: Der Dienst für den Typ 'WebApplication8.UserContext' kann nicht aufgelöst werden, während versucht wird, 'AccountController' zu aktivieren. Wenn ich den Postboten versuche, rufe Post an, um Konto / api / token
Kirsten Greed
4

Hier ist meine Implementierung für eine .Net Core 2.0-API:

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services
        services.AddMvc(
        config =>
        {
            // This enables the AuthorizeFilter on all endpoints
            var policy = new AuthorizationPolicyBuilder()
                                .RequireAuthenticatedUser()
                                .Build();
            config.Filters.Add(new AuthorizeFilter(policy));
            
        }
        ).AddJsonOptions(opt =>
        {
            opt.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
        });

        services.AddLogging();

        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.Audience = Configuration["AzureAD:Audience"];  
            options.Authority = Configuration["AzureAD:AADInstance"] + Configuration["AzureAD:TenantId"];
        });            
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseAuthentication(); // THIS METHOD MUST COME BEFORE UseMvc...() !!
        app.UseMvcWithDefaultRoute();            
    }

appsettings.json:

{
  "AzureAD": {
    "AADInstance": "https://login.microsoftonline.com/",
    "Audience": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "Domain": "mydomain.com",
    "TenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  ...
}

Der obige Code aktiviert die Authentifizierung auf allen Controllern. Um anonymen Zugriff zu ermöglichen, können Sie einen gesamten Controller dekorieren:

[Route("api/[controller]")]
[AllowAnonymous]
public class AnonymousController : Controller
{
    ...
}

oder dekorieren Sie einfach eine Methode, um einen einzelnen Endpunkt zuzulassen:

    [AllowAnonymous]
    [HttpPost("anonymousmethod")]
    public async Task<IActionResult> MyAnonymousMethod()
    {
        ...
    }

Anmerkungen:

  • Dies ist mein erster Versuch mit AD-Authentifizierung - wenn etwas nicht stimmt, lassen Sie es mich bitte wissen!

  • Audiencemuss mit der vom Client angeforderten Ressourcen-ID übereinstimmen . In unserem Fall wurde unser Client (eine Angular-Webanwendung) separat in Azure AD registriert und verwendete seine Client-ID, die wir als Zielgruppe in der API registriert haben

  • ClientIdwird im Azure-Portal als Anwendungs-ID bezeichnet (warum?), die Anwendungs-ID der App-Registrierung für die API.

  • TenantIdwird im Azure-Portal als Verzeichnis-ID bezeichnet (warum ??) und befindet sich unter Azure Active Directory> Eigenschaften

  • Stellen Sie beim Bereitstellen der API als von Azure gehostete Webanwendung sicher, dass Sie die Anwendungseinstellungen festlegen:

    z.B. AzureAD: Zielgruppe / xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

pcdev
quelle
3

Um die hervorragende Antwort von @alerya zu aktualisieren, musste ich die Hilfsklasse so ändern, dass sie so aussieht.

public static class IHttpContextAccessorExtension
    {
        public static string CurrentUser(this IHttpContextAccessor httpContextAccessor)
        {           
            var userId = httpContextAccessor?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; 
            return userId;
        }
    }

Dann könnte ich die Benutzer-ID in meiner Service-Schicht erhalten. Ich weiß, dass es im Controller einfach ist, aber eine Herausforderung weiter unten.

Spankymac
quelle