ASP.NET Core Web API-Authentifizierung

93

Ich habe Probleme beim Einrichten der Authentifizierung in meinem Webdienst. Der Dienst wird mit der ASP.NET Core-Web-API erstellt.

Alle meine Clients (WPF-Anwendungen) sollten dieselben Anmeldeinformationen verwenden, um die Webdienstvorgänge aufzurufen.

Nach einigen Recherchen habe ich mir eine grundlegende Authentifizierung ausgedacht - das Senden eines Benutzernamens und eines Passworts im Header der HTTP-Anfrage. Nach stundenlangen Recherchen scheint mir die grundlegende Authentifizierung in ASP.NET Core jedoch nicht der richtige Weg zu sein.

Die meisten Ressourcen, die ich gefunden habe, implementieren die Authentifizierung mit OAuth oder einer anderen Middleware. Aber das scheint für mein Szenario und die Verwendung des Identitätsteils von ASP.NET Core übergroß zu sein.

Was ist der richtige Weg, um mein Ziel zu erreichen - einfache Authentifizierung mit Benutzername und Passwort in einem ASP.NET Core-Webdienst?

Danke im Voraus!

Felix
quelle

Antworten:

71

Sie können eine Middleware implementieren, die die Standardauthentifizierung übernimmt.

public async Task Invoke(HttpContext context)
{
    var authHeader = context.Request.Headers.Get("Authorization");
    if (authHeader != null && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
    {
        var token = authHeader.Substring("Basic ".Length).Trim();
        System.Console.WriteLine(token);
        var credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token));
        var credentials = credentialstring.Split(':');
        if(credentials[0] == "admin" && credentials[1] == "admin")
        {
            var claims = new[] { new Claim("name", credentials[0]), new Claim(ClaimTypes.Role, "Admin") };
            var identity = new ClaimsIdentity(claims, "Basic");
            context.User = new ClaimsPrincipal(identity);
        }
    }
    else
    {
        context.Response.StatusCode = 401;
        context.Response.Headers.Set("WWW-Authenticate", "Basic realm=\"dotnetthoughts.net\"");
    }
    await _next(context);
}

Dieser Code ist in einer Beta-Version von asp.net core geschrieben. Ich hoffe es hilft.

Anuraj
quelle
1
Danke für deine Antwort! Genau das habe ich gesucht - eine einfache Lösung für die Basisauthentifizierung.
Felix
1
In diesem Code ist ein Fehler aufgrund der Verwendung von credentialstring.Split (':') aufgetreten. Kennwörter, die einen Doppelpunkt enthalten, werden nicht korrekt verarbeitet. Der Code in der Antwort von Felix leidet nicht unter diesem Problem.
Phil Dennis
107

Nachdem ich in die richtige Richtung gezeigt wurde, ist hier meine vollständige Lösung:

Dies ist die Middleware-Klasse, die bei jeder eingehenden Anforderung ausgeführt wird und prüft, ob die Anforderung die richtigen Anmeldeinformationen enthält. Wenn keine Anmeldeinformationen vorhanden sind oder wenn sie falsch sind, antwortet der Dienst sofort mit einem 401 Unauthorized- Fehler.

public class AuthenticationMiddleware
{
    private readonly RequestDelegate _next;

    public AuthenticationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        string authHeader = context.Request.Headers["Authorization"];
        if (authHeader != null && authHeader.StartsWith("Basic"))
        {
            //Extract credentials
            string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
            Encoding encoding = Encoding.GetEncoding("iso-8859-1");
            string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));

            int seperatorIndex = usernamePassword.IndexOf(':');

            var username = usernamePassword.Substring(0, seperatorIndex);
            var password = usernamePassword.Substring(seperatorIndex + 1);

            if(username == "test" && password == "test" )
            {
                await _next.Invoke(context);
            }
            else
            {
                context.Response.StatusCode = 401; //Unauthorized
                return;
            }
        }
        else
        {
            // no authorization header
            context.Response.StatusCode = 401; //Unauthorized
            return;
        }
    }
}

Die Middleware-Erweiterung muss in der Configure-Methode der Service Startup-Klasse aufgerufen werden

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseMiddleware<AuthenticationMiddleware>();

    app.UseMvc();
}

Und das ist alles! :) :)

Eine sehr gute Ressource für Middleware in .Net Core und Authentifizierung finden Sie hier: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/

Felix
quelle
4
Vielen Dank für die Veröffentlichung der vollständigen Lösung. Ich musste jedoch die Zeile 'context.Response.Headers.Add ("WWW-Authenticate", "Basic Realm =" Realm "); Klicken Sie im Abschnitt "Header ohne Autorisierung" auf, damit der Browser Anmeldeinformationen anfordert.
m0n0ph0n
Wie sicher ist diese Authentifizierung? Was ist, wenn jemand am Anforderungsheader schnüffelt und den Benutzernamen / das Passwort erhält?
Bewar Salah
5
@BewarSalah Sie müssen diese Art von Lösung über https
Wal
2
Einige Controller sollten anonym zulassen. Diese Middleware-Lösung schlägt in diesem Fall fehl, da bei jeder Anforderung nach dem Autorisierungsheader gesucht wird.
Karthik
27

Um dies beispielsweise nur für bestimmte Steuerungen zu verwenden, verwenden Sie Folgendes:

app.UseWhen(x => (x.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)), 
            builder =>
            {
                builder.UseMiddleware<AuthenticationMiddleware>();
            });
mr_squall
quelle
20

Ich denke, Sie können mit JWT (Json Web Tokens) gehen.

Zuerst müssen Sie das Paket System.IdentityModel.Tokens.Jwt installieren:

$ dotnet add package System.IdentityModel.Tokens.Jwt

Sie müssen einen Controller für die Token-Generierung und -Authentifizierung wie diesen hinzufügen:

public class TokenController : Controller
{
    [Route("/token")]

    [HttpPost]
    public IActionResult Create(string username, string password)
    {
        if (IsValidUserAndPasswordCombination(username, password))
            return new ObjectResult(GenerateToken(username));
        return BadRequest();
    }

    private bool IsValidUserAndPasswordCombination(string username, string password)
    {
        return !string.IsNullOrEmpty(username) && username == password;
    }

    private string GenerateToken(string username)
    {
        var claims = new Claim[]
        {
            new Claim(ClaimTypes.Name, username),
            new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
            new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds().ToString()),
        };

        var token = new JwtSecurityToken(
            new JwtHeader(new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
                                         SecurityAlgorithms.HmacSha256)),
            new JwtPayload(claims));

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Nach diesem Update sieht die Startup.cs-Klasse wie folgt aus:

namespace WebAPISecurity
{   
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)
    {
        services.AddMvc();

        services.AddAuthentication(options => {
            options.DefaultAuthenticateScheme = "JwtBearer";
            options.DefaultChallengeScheme = "JwtBearer";
        })
        .AddJwtBearer("JwtBearer", jwtBearerOptions =>
        {
            jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
                ValidateIssuer = false,
                //ValidIssuer = "The name of the issuer",
                ValidateAudience = false,
                //ValidAudience = "The name of the audience",
                ValidateLifetime = true, //validate the expiration and not before values in the token
                ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
            };
        });

    }

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

        app.UseMvc();
    }
}

Und das war's, was jetzt noch übrig ist, ist zu sagen [Authorize] gewünschten Controllern oder Aktionen Attribute zuzuweisen.

Hier ist ein Link zu einem vollständigen, einfachen Tutorial.

http://www.blinkingcaret.com/2017/09/06/secure-web-api-in-asp-net-core/

AJ -
quelle
9

Ich habe BasicAuthenticationHandlerfür die Basisauthentifizierung implementiert, damit Sie es mit Standardattributen Authorizeund verwenden können AllowAnonymous.

public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var authHeader = (string)this.Request.Headers["Authorization"];

        if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
        {
            //Extract credentials
            string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
            Encoding encoding = Encoding.GetEncoding("iso-8859-1");
            string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));

            int seperatorIndex = usernamePassword.IndexOf(':', StringComparison.OrdinalIgnoreCase);

            var username = usernamePassword.Substring(0, seperatorIndex);
            var password = usernamePassword.Substring(seperatorIndex + 1);

            //you also can use this.Context.Authentication here
            if (username == "test" && password == "test")
            {
                var user = new GenericPrincipal(new GenericIdentity("User"), null);
                var ticket = new AuthenticationTicket(user, new AuthenticationProperties(), Options.AuthenticationScheme);
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
            else
            {
                return Task.FromResult(AuthenticateResult.Fail("No valid user."));
            }
        }

        this.Response.Headers["WWW-Authenticate"]= "Basic realm=\"yourawesomesite.net\"";
        return Task.FromResult(AuthenticateResult.Fail("No credentials."));
    }
}

public class BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
{
    public BasicAuthenticationMiddleware(
       RequestDelegate next,
       IOptions<BasicAuthenticationOptions> options,
       ILoggerFactory loggerFactory,
       UrlEncoder encoder)
       : base(next, options, loggerFactory, encoder)
    {
    }

    protected override AuthenticationHandler<BasicAuthenticationOptions> CreateHandler()
    {
        return new BasicAuthenticationHandler();
    }
}

public class BasicAuthenticationOptions : AuthenticationOptions
{
    public BasicAuthenticationOptions()
    {
        AuthenticationScheme = "Basic";
        AutomaticAuthenticate = true;
    }
}

Registrierung bei Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. Mit diesem Code können Sie jeden Controller mit dem Standardattribut Autorize einschränken:

[Authorize(ActiveAuthenticationSchemes = "Basic")]
[Route("api/[controller]")]
public class ValuesController : Controller

und verwenden Sie das Attribut, AllowAnonymouswenn Sie den Autorisierungsfilter auf Anwendungsebene anwenden.

Ivan R.
quelle
1
Ich habe Ihren Code verwendet, aber ich habe festgestellt, egal ob die Autorisierung (ActiveAuthenticationSchemes = "Basic")] bei jedem Aufruf festgelegt ist oder nicht. Die Middleware wird aktiviert, sodass jeder Controller auch dann validiert wird, wenn dies nicht gewünscht wird.
CSharper
Ich mag diese Antwort
KTOV
1
Arbeitsbeispiel hier: jasonwatmore.com/post/2018/09/08/…
bside
Ich denke, diese Antwort ist der richtige Weg, da Sie damit die Standardattribute authorize / allowanonymous weiter oben in der Lösung verwenden können.
Darüber hinaus
0

In diesem öffentlichen Github-Repo https://github.com/boskjoett/BasicAuthWebApi sehen Sie ein einfaches Beispiel für eine ASP.NET Core 2.2-Web-API mit Endpunkten, die durch die Standardauthentifizierung geschützt sind.

Bo Christian Skjøtt
quelle
Wenn Sie die authentifizierte Identität in Ihrem Controller (SecureValuesController) verwenden möchten, reicht das Erstellen eines Tickets nicht aus, da das Request.User-Objekt leer ist. Müssen wir diesen ClaimsPrincipal noch dem aktuellen Kontext im AuthenticationHandler zuweisen? So haben wir es in älteren WebApi gemacht ...
Pseabury
0

Wie in früheren Beiträgen zu Recht erwähnt, besteht eine Möglichkeit darin, eine benutzerdefinierte Basisauthentifizierungs-Middleware zu implementieren. In diesem Blog habe ich den besten Arbeitscode mit Erläuterungen gefunden: Basic Auth mit benutzerdefinierter Middleware

Ich habe auf denselben Blog verwiesen, musste aber zwei Anpassungen vornehmen:

  1. Fügen Sie beim Hinzufügen der Middleware zur Startdatei -> Konfigurationsfunktion immer eine benutzerdefinierte Middleware hinzu, bevor Sie app.UseMvc () hinzufügen.
  2. Fügen Sie beim Lesen des Benutzernamens und des Kennworts aus der Datei appsettings.json eine statische schreibgeschützte Eigenschaft in die Startdatei ein. Dann lesen Sie aus appsettings.json. Lesen Sie abschließend die Werte von einer beliebigen Stelle im Projekt. Beispiel:

    public class Startup
    {
      public Startup(IConfiguration configuration)
      {
        Configuration = configuration;
      }
    
      public IConfiguration Configuration { get; }
      public static string UserNameFromAppSettings { get; private set; }
      public static string PasswordFromAppSettings { get; private set; }
    
      //set username and password from appsettings.json
      UserNameFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("UserName").Value;
      PasswordFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("Password").Value;
    }
Palash Roy
quelle