Uwierzytelnianie interfejsu API sieci Web ASP.NET Core

98

Mam problem z konfiguracją uwierzytelniania w mojej usłudze internetowej. Usługa jest tworzona przy użyciu internetowego interfejsu API ASP.NET Core.

Wszyscy moi klienci (aplikacje WPF) powinni używać tych samych poświadczeń do wywoływania operacji usługi sieci Web.

Po kilku poszukiwaniach wymyśliłem uwierzytelnianie podstawowe - wysyłanie nazwy użytkownika i hasła w nagłówku żądania HTTP. Ale po wielu godzinach badań wydaje mi się, że podstawowe uwierzytelnianie nie jest właściwą drogą w ASP.NET Core.

Większość znalezionych zasobów implementuje uwierzytelnianie przy użyciu protokołu OAuth lub innego oprogramowania pośredniego. Ale wydaje się, że jest to zbyt duże w moim scenariuszu, a także przy użyciu części Identity ASP.NET Core.

Jaki jest więc właściwy sposób osiągnięcia celu - prostego uwierzytelniania przy użyciu nazwy użytkownika i hasła w usłudze sieci Web ASP.NET Core?

Z góry dziękuję!

Felix
źródło

Odpowiedzi:

75

Możesz zaimplementować oprogramowanie pośredniczące, które obsługuje uwierzytelnianie podstawowe.

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

Ten kod jest napisany w wersji beta asp.net core. Mam nadzieję, że to pomoże.

Anuraj
źródło
1
Dzięki za odpowiedź! Właśnie tego szukałem - prostego rozwiązania do podstawowego uwierzytelnienia.
Felix
1
W tym kodzie jest błąd spowodowany użyciem poświadczeń.Split (':') - nie będzie poprawnie obsługiwał haseł zawierających dwukropek. Kod w odpowiedzi Felixa nie cierpi z powodu tego problemu.
Phil Dennis
111

Teraz, gdy zostałem wskazany we właściwym kierunku, oto moje kompletne rozwiązanie:

Jest to klasa oprogramowania pośredniego, która jest wykonywana przy każdym przychodzącym żądaniu i sprawdza, czy żądanie ma prawidłowe poświadczenia. Jeśli nie ma poświadczeń lub są one błędne, usługa natychmiast odpowiada błędem 401 Unauthorized .

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

Rozszerzenie oprogramowania pośredniego należy wywołać w metodzie Configure klasy startowej usługi

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

    app.UseMiddleware<AuthenticationMiddleware>();

    app.UseMvc();
}

I to wszystko! :)

Bardzo dobre źródło oprogramowania pośredniego w .Net Core i uwierzytelniania można znaleźć tutaj: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/

Felix
źródło
4
Dziękujemy za przesłanie kompletnego rozwiązania. Jednak musiałem dodać wiersz 'context.Response.Headers.Add ("WWW-Authenticate", "Basic realm = \" realm \ "");' do sekcji „brak nagłówka autoryzacji”, aby przeglądarka zażądała danych logowania.
m0n0ph0n
W jakim stopniu to uwierzytelnienie jest bezpieczne? Co się stanie, jeśli ktoś podsłucha nagłówek żądania i otrzyma nazwę użytkownika / hasło?
Bewar Salah
5
@BewarSalah musisz obsługiwać tego rodzaju rozwiązanie przez https
wal
2
Niektóre kontrolery powinny zezwalać na anonimowość. W takim przypadku to rozwiązanie pośredniczące zakończy się niepowodzeniem, ponieważ sprawdzi nagłówek autoryzacji w każdym żądaniu.
Karthik
28

Aby użyć tego tylko dla określonych kontrolerów, na przykład użyj tego:

app.UseWhen(x => (x.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)), 
            builder =>
            {
                builder.UseMiddleware<AuthenticationMiddleware>();
            });
mr_squall
źródło
22

Myślę, że możesz skorzystać z JWT (Json Web Tokens).

Najpierw musisz zainstalować pakiet System.IdentityModel.Tokens.Jwt:

$ dotnet add package System.IdentityModel.Tokens.Jwt

Będziesz musiał dodać kontroler do generowania tokenów i uwierzytelniania, taki jak ten:

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

Następnie zaktualizuj klasę Startup.cs, aby wyglądała jak poniżej:

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

I to wszystko, co teraz pozostaje, to umieścić [Authorize]atrybut na kontrolerach lub akcjach, które chcesz.

Oto link do pełnego, prostego samouczka.

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

AJ -
źródło
9

Zaimplementowałem BasicAuthenticationHandlerdla podstawowego uwierzytelniania, więc możesz go używać ze standardowymi atrybutami Authorizei 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;
    }
}

Rejestracja na Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. Za pomocą tego kodu możesz ograniczyć dowolny kontroler za pomocą atrybutu standardowego Autorize:

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

i użyj atrybutu, AllowAnonymousjeśli zastosujesz filtr autoryzacji na poziomie aplikacji.

Ivan R.
źródło
1
Użyłem twojego kodu, ale zauważyłem, że niezależnie od tego, czy autoryzacja (ActiveAuthenticationSchemes = "Basic")] jest ustawiona, czy nie w każdym wywołaniu, oprogramowanie pośredniczące jest aktywowane, co powoduje, że każdy kontroler jest sprawdzany również wtedy, gdy nie jest pożądany.
CSharper
Podoba mi się ta odpowiedź
KTOV
1
działający przykład tutaj: jasonwatmore.com/post/2018/09/08/…
bside
Myślę, że ta odpowiedź jest właściwą drogą, ponieważ pozwala na użycie standardowych atrybutów autoryzacji / uprawnień na dalszych etapach rozwiązania.
Poza
0

W tym publicznym repozytorium Github https://github.com/boskjoett/BasicAuthWebApi można zobaczyć prosty przykład internetowego interfejsu API ASP.NET Core 2,2 z punktami końcowymi chronionymi przez uwierzytelnianie podstawowe.

Bo Christian Skjøtt
źródło
Jeśli chcesz użyć uwierzytelnionej tożsamości w kontrolerze (SecureValuesController), utworzenie biletu nie wystarczy, ponieważ obiekt Request.User jest pusty. Czy nadal musimy przypisywać to ClaimsPrincipal do bieżącego kontekstu w AuthenticationHandler? Tak właśnie zrobiliśmy w starszym WebApi ...
pseabury
0

Jak słusznie powiedziano w poprzednich postach, jednym ze sposobów jest zaimplementowanie niestandardowego oprogramowania pośredniczącego do podstawowego uwierzytelniania. Znalazłem najlepszy działający kod z wyjaśnieniem na tym blogu: Basic Auth with custom middleware

Odniosłem się do tego samego bloga, ale musiałem zrobić 2 adaptacje:

  1. Podczas dodawania oprogramowania pośredniego w pliku startowym -> Konfiguruj funkcję, zawsze dodawaj niestandardowe oprogramowanie pośredniczące przed dodaniem app.UseMvc ().
  2. Podczas odczytywania nazwy użytkownika i hasła z pliku appsettings.json dodaj statyczną właściwość tylko do odczytu w pliku startowym. Następnie przeczytaj plik appsettings.json. Na koniec przeczytaj wartości z dowolnego miejsca w projekcie. Przykład:

    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
źródło
0

Możesz użyć ActionFilterAttribute

public class BasicAuthAttribute : ActionFilterAttribute
{
    public string BasicRealm { get; set; }
    protected NetworkCredential Nc { get; set; }

    public BasicAuthAttribute(string user,string pass)
    {
        this.Nc = new NetworkCredential(user,pass);
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var req = filterContext.HttpContext.Request;
        var auth = req.Headers["Authorization"].ToString();
        if (!String.IsNullOrEmpty(auth))
        {
            var cred = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(auth.Substring(6)))
                .Split(':');
            var user = new {Name = cred[0], Pass = cred[1]};
            if (user.Name == Nc.UserName && user.Pass == Nc.Password) return;
        }

        filterContext.HttpContext.Response.Headers.Add("WWW-Authenticate",
            String.Format("Basic realm=\"{0}\"", BasicRealm ?? "Ryadel"));
        filterContext.Result = new UnauthorizedResult();
    }
}

i dodaj atrybut do kontrolera

[BasicAuth("USR", "MyPassword")]

Luca Ziegler
źródło