ChatGPT解决这个技术问题 Extra ChatGPT

ASP.NET Core Web API Authentication

I'm struggling with how to set up authentication in my web service. The service is build with the ASP.NET Core web api.

All my clients (WPF applications) should use the same credentials to call the web service operations.

After some research, I came up with basic authentication - sending a username and password in the header of the HTTP request. But after hours of research, it seems to me that basic authentication is not the way to go in ASP.NET Core.

Most of the resources I found are implementing authentication using OAuth or some other middleware. But that seems to be oversized for my scenario, as well as using the Identity part of ASP.NET Core.

So what is the right way to achieve my goal - simple authentication with username and password in a ASP.NET Core web service?

Thanks in advance!


F
Felix

Now, after I was pointed in the right direction, here's my complete solution:

This is the middleware class which is executed on every incoming request and checks if the request has the correct credentials. If no credentials are present or if they are wrong, the service responds with a 401 Unauthorized error immediately.

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

The middleware extension needs to be called in the Configure method of the service Startup class

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

    app.UseMiddleware<AuthenticationMiddleware>();

    app.UseMvc();
}

And that's all! :)

A very good resource for middleware in .Net Core and authentication can be found here: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/


Thanks for posting the complete solution. However, I had to add the line 'context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"realm\"");' to the 'no authorization header' section in order to have the browser requesting credentials.
How much this authentication is secure? What if somebody sniffs to the request header and get the username/password?
@BewarSalah you must serve this kind of solution over https
Some controllers should allow anonymous. This middleware solution will fail in that case becuase it will check for the authorization header in each request.
A
Anuraj

You can implement a middleware which handles Basic authentication.

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

This code is written in a beta version of asp.net core. Hope it helps.


Thanks for your answer! This is exactly what I was looking for - a simple solution for basic authentication.
There is a bug in this code due to use of credentialstring.Split(':') - it will not correctly handle passwords which contain a colon. The code in the answer by Felix does not suffer from this issue.
m
mr_squall

To use this only for specific controllers for example use this:

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

E
Eric J.

I think you can go with JWT (Json Web Tokens).

First you need to install the package System.IdentityModel.Tokens.Jwt:

$ dotnet add package System.IdentityModel.Tokens.Jwt

You will need to add a controller for token generation and authentication like this one:

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

After that update Startup.cs class to look like below:

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

And that's it, what is left now is to put [Authorize] attribute on the Controllers or Actions you want.

Here is a link of a complete straight forward tutorial.

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


B
Bruno Zell

I have implemented BasicAuthenticationHandler for basic authentication so you can use it with standart attributes Authorize and 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;
    }
}

Registration at Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. With this code, you can restrict any controller with standart attribute Autorize:

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

and use attribute AllowAnonymous if you apply authorize filter on application level.


I used your code, but I noticed no matter if the Authorize(ActiveAuthenticationSchemes = "Basic")] is set or not in every call the middleware gets activated resulting in every controller validated also when is not desired.
I like this answer
working example here: jasonwatmore.com/post/2018/09/08/…
I think this is answer is the way to go, as it allows you to use the standard authorize / allowanonymous attributes further up in the solution. Next to that, it should be easy to use another authentication scheme later in the phase of the project should that be required
P
Palash Roy

As rightly said by previous posts, one of way is to implement a custom basic authentication middleware. I found the best working code with explanation in this blog: Basic Auth with custom middleware

I referred the same blog but had to do 2 adaptations:

While adding the middleware in startup file -> Configure function, always add custom middleware before adding app.UseMvc(). While reading the username, password from appsettings.json file, add static read only property in Startup file. Then read from appsettings.json. Finally, read the values from anywhere in the project. Example: 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; }


L
Luca Ziegler

You can use an 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();
    }
}

and add the attribute to your controller

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


B
Bo Christian Skjøtt

In this public Github repo https://github.com/boskjoett/BasicAuthWebApi you can see a simple example of a ASP.NET Core 2.2 web API with endpoints protected by Basic Authentication.


If you want to the use the Authenticated Identity in your controller (SecureValuesController), creating a ticket isn't enough as the Request.User object is empty. Do we still need to assign this ClaimsPrincipal to the current Context in the AuthenticationHandler? That's the way we did it in older WebApi...
R
Rahul Uttarkar

ASP.NET Core 2.0 with Angular

https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login

Make sure to use type of authentication filter

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]