Laden...

JWT Authentifizierung in WebAPI Core 2.0

Erstellt von Marcel vor 6 Jahren Letzter Beitrag vor 6 Jahren 2.317 Views
M
Marcel Themenstarter:in
210 Beiträge seit 2005
vor 6 Jahren
JWT Authentifizierung in WebAPI Core 2.0

Hallo Leute.

Vorweg: Ich bin relativ neu in JWT Tokens und WebAPI. Auch NET.Core ist mir neu. Bitte also um Verständnis und hoffentlich leicht verständliche Antworten 😃

Ich experimentiere gerade etwas herum mit diesem Thema und habe dafür ein Testprojekt aufgesetzt. Ich habe versucht die Nutzung der Tokens einzusetzen und es scheint nicht zu fruchten. Es ist egal ob ich mit einem Athentification Header mit den Token (ob gültig oder nicht) einen Request starte, es klappt immer. Scheinbar wirkt das ganze System nicht. Eventuell weiß ja jemand hier wo der gedankliche oder der Codierungsfehler steckt.

Ich habe mich bei der ganzen Sachen an diesem Tutorial orientiert: Link

Ich generiere einen Token in einem Controller namens SecurityController:

 
    [Produces("application/json")]
    [Route("api")]
    public class SecurityController : ControllerBase
    {
        [AllowAnonymous]
        [HttpGet("login")]
        public IActionResult Token(string user, string pass)
        {
            // Prüfe Benutzer und Passwort
            if (!ModelState.IsValid) return BadRequest("Token failed to generate");
            if (user != "myuser" || pass != "mypass") return StatusCode(401);

            // Erstelle Token (https://de.wikipedia.org/wiki/JSON_Web_Token)
            // Ein Token besteht aus Header, Payload und der Signatur.
            // Header: Der Header ist ein JSON-Element, welches beschreibt um welchen Token-Typ es sich handelt und welche Verschlüsselungsmethode zum Einsatz kommt.
            // Payload: Beim Payload handelt es sich um ein JSON-Element, welches die Claims beschreibt.
            // Signatur: Die Signatur wird dadurch erzeugt, dass der Header und der Payload im Base64 kodierten und durch einen Punkt getrennten Format mit der spezifizierten Hashmethode gehashed wird            
            long exp_time = new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds();
            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // Eine eindeutige case-sensitive Zeichenfolge, welche das Token eindeutig identifiziert. Hiermit kann verhindert werden, dass das Token mehrfach verwendet wird. Hierbei kann es sich etwa um eine durchgezählte Nummer, einen GUID oder einen Hashwert handeln.
                new Claim(JwtRegisteredClaimNames.Exp, exp_time.ToString()) // Ablaufzeitpunkt für Gültigkeit des Tokens 
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secrect_key_must_be_here"));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); // Signatur enthält verschlüsselt den Header und Payload. So wird verhindert das dieser im Klartext Bereich manipuliert wird. Ungleichheit führt zu Ungültigkeit des Tokens
            
            JwtSecurityToken token = new JwtSecurityToken(new JwtHeader(creds), new JwtPayload(claims));
            JwtSecurityTokenHandler token_handler = new JwtSecurityTokenHandler();
            return StatusCode(200, token_handler.WriteToken(token));
        }
    }

Wenn der Client nun diesen Login erfolgreich getätigt hat, so mein Verständnis, muss dieser Token bei jedem neuen Request mitgegeben werden und wird dann auf dem Server valiert. Das alles läuft über die Konfiguration in der Startup.cs. Diese sieht wie folgt aus:


    public class Startup
    {
        public Startup(IConfiguration configuration)
        {            
            Configuration = configuration;            
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            if (services == null)
                throw new ArgumentNullException(nameof(services));

            // Jwt als Authetifikationsverfahren einrichten
            AuthenticationBuilder auth_builder = services.AddAuthentication(o => {
                o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            });
            // Jwt Validierung Parameter
            TokenValidationParameters token_valid_params =  new TokenValidationParameters()
            {
                ValidateIssuerSigningKey = true, // Validiere Signatur
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secrect_key_must_be_here")),
                ValidateIssuer = false,
                ValidateAudience = false,                
                ValidateLifetime = true // Validiere "expiration" und "not before"
            };
            auth_builder.AddJwtBearer(o => {
                o.RequireHttpsMetadata = false; // Https wieder aktivieren für produktion
                o.SaveToken = true;
                o.TokenValidationParameters = token_valid_params;
                o.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();
                        c.Response.StatusCode = 401;
                        c.Response.ContentType = "text/plain";
                        return c.Response.WriteAsync(c.Exception.ToString());                        
                    }
                };
            });

            var mvc_builder = services.AddMvcCore();
            mvc_builder.AddFormatterMappings();            
            mvc_builder.AddJsonFormatters();
            mvc_builder.AddCors();            
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseAuthentication(); // Aktiviere Validierungsverfahren (Muss vor UseMvc() stehen)
            app.UseMvc();
        }
        
    }

Um festzulegen für welche Endpunkte diese Authetifizierung gilt wird das Attribut vor die Methode bzw. in meinem Fall vor die Klasse gesetzt:


    // ../api
    [Authorize]
    [Route("api")]
    public class ValuesController : ControllerBase
    {       
        // GET ../api        
        [HttpGet]
        public IActionResult Get()
        {            
            object result =  new string[] { "Das", "ist", "das", "Ergebnis"};
            return StatusCode(200, result);
        }
        ...

Wenn ich nun einen Aufruf mache, dann scheint das ganze System nicht zu ziehen. Meinem Verständnis nach müsste ein Aufruf mit einem Request ohne Authentification Header zu einem Fehler führen genauso wie eine ungültiger Token.

Wo liest der Fehler?

2.207 Beiträge seit 2011
vor 6 Jahren

Hallo Marcel,

Wenn ich nun einen Aufruf mache, dann scheint das ganze System nicht zu ziehen.

was heisst das genau? Kannst du dir einen Token ausstellen? Schickst du ihn im Header mit (Prefix "Bearer ")?

Hast du mal einen Blick auf den IdentityServer geworfen um deinen Fall abzudecken?

Gruss

Coffeebean

M
Marcel Themenstarter:in
210 Beiträge seit 2005
vor 6 Jahren

Zum weiteren Verständnis mal ein Bild von Anfrage- und Antwortkopf: ([Bild siehe Anhang [Hinweis] Wie poste ich richtig? ] - Coffeebean

Den Token kann ich mir ausstellen. Wenn ich den eben NICHT mitschicke, funktioniert es trotzdem. Das ist das was ich nicht verstehe?! D.h. ich kann die Abfrage problemlos ausführen obwohl ich keinen Token mitsende.

Das mit dem IdentityServer habe ich nur überflogen. Ich hoffte das meien Codierung ausreicht um das ganze mit den Tokens mal zu testen.

16.833 Beiträge seit 2008
vor 6 Jahren

Bitte verzichte auf externe Bildhoster; das bringt in Zukunft nicht mehr viel.
Siehe auch [Hinweis] Wie poste ich richtig?

M
Marcel Themenstarter:in
210 Beiträge seit 2005
vor 6 Jahren

Wenn ich einen Token mitgebe, der völlig falsch ist (hier: "wrong_token") bekomme ich dennoch meine Antwort (siehe Anhang)

2.207 Beiträge seit 2011
vor 6 Jahren

Hallo Marcel,

hast du in den Launchsettings die windowsAuthentication auf "true"?

Gruss

Coffeebean

16.833 Beiträge seit 2008
vor 6 Jahren

Hast Du denn mal das Demo Projekt heruntergeladen und geschaut, ob überhaupt das Demo-Projekt korrekt funktioniert?
Ich seh prinzipiell einige Unterschiede zu Deinem Code und dem auf GitHub des Demo-Projekts.

Nichts desto trotz ist der IdentityServer4 der de facto Standard im ASP.NET Core Umfeld und wird von Microsoft auch aktiv empfohlen.
IdentityServer4 ist sowohl wirklich zertifiziert, auditiert und auch Teil der .NET Foundation.

Keine Notwendigkeit also etwas "zu basteln".
Prinzipiell kann man das natürlich machen; beim Thema Security ohne wirklich tiefe Kenntnis aber nicht zu empfehlen.
Mein Beispiel dazu ist IdentityServer4 Plattform Beispiel basierend auf .NET Core und ASP.NET Core, wobei es natürlich auch zahlreich andere wie zB von Coffeebean gibt.

PS: Samples und Tutorials zu ASP.NET Core 1.x sind obsolete.
Der gesamte Auth-Stack hat sich bei 2.0 geändert; wissen leider viele nicht, daher hier als Hinweis.

M
Marcel Themenstarter:in
210 Beiträge seit 2005
vor 6 Jahren

So, ich hab das Problem lokalisiert.
Nun bekomme ich bei einem ungültigen Token auch wie erwartet einen 401 zurück:


{
   StatusCode: 401, 
   ReasonPhrase: 'Unauthorized', 
   Version: 1.1,
   Content: System.Net.Http.StreamContent, 
   Headers:
   {
        WWW-Authenticate: Bearer error="invalid_token"
   }
}

Das Problem war, dass ich relativ blind folgenden Code als Ersatz für AddMvc() integriert habe.
Hintergund ist, dass ich keine Oberfläche habe und daher kein View aus MVC. Darauf gestoßen bin ich durch diesen Link

Aus dem Artikel habe ich den Code entnommen und dann anstatt AdddMVC() folgendes gemacht..


// Funktioniert nicht, wie ich jetzt weiß
var mvc_builder = services.AddMvcCore();
mvc_builder.AddFormatterMappings();            
mvc_builder.AddJsonFormatters();
mvc_builder.AddCors();          

ergänze ich das durch

mvc_builder.AddAuthorization()

oder ersetzte das ganze durch

services.AddMvc()

klappt es auch.

Mir war nicht klar dass das Problem bei Mvc liegen könnte.
Offensichtlich hat dadurch das Attribute "[Authorize]" seine Wirkung verloren.
Liege ich da richtig?