Authentification de l'API Web ASP.NET Core

96

J'ai du mal à configurer l'authentification dans mon service Web. Le service est généré avec l'API Web ASP.NET Core.

Tous mes clients (applications WPF) doivent utiliser les mêmes informations d'identification pour appeler les opérations de service Web.

Après quelques recherches, j'ai trouvé une authentification de base - l'envoi d'un nom d'utilisateur et d'un mot de passe dans l'en-tête de la requête HTTP. Mais après des heures de recherche, il me semble que l'authentification de base n'est pas la voie à suivre dans ASP.NET Core.

La plupart des ressources que j'ai trouvées implémentent l'authentification à l'aide d'OAuth ou d'un autre middleware. Mais cela semble être surdimensionné pour mon scénario, ainsi que pour l'utilisation de la partie Identité d'ASP.NET Core.

Alors, quelle est la bonne façon d'atteindre mon objectif - une authentification simple avec nom d'utilisateur et mot de passe dans un service Web ASP.NET Core?

Merci d'avance!

Félix
la source

Réponses:

73

Vous pouvez implémenter un middleware qui gère l'authentification de base.

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

Ce code est écrit dans une version bêta du noyau asp.net. J'espère que ça aide.

Anuraj
la source
1
Merci pour votre réponse! C'est exactement ce que je recherchais - une solution simple pour l'authentification de base.
Felix
1
Il y a un bogue dans ce code en raison de l'utilisation de credentialstring.Split (':') - il ne gérera pas correctement les mots de passe qui contiennent un deux-points. Le code de la réponse de Felix ne souffre pas de ce problème.
Phil Dennis
110

Maintenant, après avoir été pointé dans la bonne direction, voici ma solution complète:

Il s'agit de la classe middleware qui est exécutée à chaque demande entrante et vérifie si la demande a les informations d'identification correctes. Si aucune information d'identification n'est présente ou si elles sont erronées, le service répond immédiatement avec une erreur 401 non autorisée .

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

L'extension middleware doit être appelée dans la méthode Configure de la classe de démarrage du service

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

    app.UseMiddleware<AuthenticationMiddleware>();

    app.UseMvc();
}

Et c'est tout! :)

Une très bonne ressource pour le middleware dans .Net Core et l'authentification peut être trouvée ici: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/

Félix
la source
4
Merci d'avoir publié la solution complète. Cependant, j'ai dû ajouter la ligne 'context.Response.Headers.Add ("WWW-Authenticate", "Basic realm = \" realm \ "");' à la section «pas d'en-tête d'autorisation» afin que le navigateur demande les informations d'identification.
m0n0ph0n
Dans quelle mesure cette authentification est-elle sécurisée? Que faire si quelqu'un renifle l'en-tête de la requête et obtient le nom d'utilisateur / mot de passe?
Bewar Salah
5
@BewarSalah vous devez servir ce genre de solution sur https
wal
2
Certains contrôleurs devraient autoriser les fichiers anonymes. Cette solution middleware échouera dans ce cas car elle vérifiera l'en-tête d'autorisation dans chaque demande.
Karthik
28

Pour utiliser ceci uniquement pour des contrôleurs spécifiques, par exemple, utilisez ceci:

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

Je pense que vous pouvez utiliser JWT (Json Web Tokens).

Vous devez d'abord installer le package System.IdentityModel.Tokens.Jwt:

$ dotnet add package System.IdentityModel.Tokens.Jwt

Vous devrez ajouter un contrôleur pour la génération de jetons et l'authentification comme celui-ci:

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

Après cette mise à jour de la classe Startup.cs pour ressembler à ci-dessous:

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

Et c'est tout, il ne reste plus qu'à mettre un [Authorize]attribut sur les contrôleurs ou les actions que vous souhaitez.

Voici un lien vers un didacticiel complet et simple.

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

UN J -
la source
9

J'ai implémenté BasicAuthenticationHandlerpour l'authentification de base afin que vous puissiez l'utiliser avec des attributs standart Authorizeet 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;
    }
}

Inscription sur Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. Avec ce code, vous pouvez restreindre n'importe quel contrôleur avec l'attribut standard Autorize:

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

et utilisez l'attribut AllowAnonymoussi vous appliquez le filtre d'autorisation au niveau de l'application.

Ivan R.
la source
1
J'ai utilisé votre code, mais j'ai remarqué que peu importe si le Authorize (ActiveAuthenticationSchemes = "Basic")] est défini ou non à chaque appel, le middleware est activé, ce qui entraîne la validation de chaque contrôleur également lorsque ce n'est pas souhaité.
CSharper le
J'aime cette réponse
KTOV
1
exemple de travail ici: jasonwatmore.com/post/2018/09/08/…
bside
Je pense que cette réponse est la voie à suivre, car elle vous permet d'utiliser les attributs standard authorize / allowanonymous plus haut dans la solution. À côté de cela, il devrait être facile d'utiliser un autre schéma d'authentification plus tard dans la phase du projet si cela est nécessaire
Frederik Gheysels
0

Dans ce référentiel Github public https://github.com/boskjoett/BasicAuthWebApi, vous pouvez voir un exemple simple d'API Web ASP.NET Core 2.2 avec des points de terminaison protégés par l'authentification de base.

Bo Christian Skjøtt
la source
Si vous souhaitez utiliser l'identité authentifiée dans votre contrôleur (SecureValuesController), la création d'un ticket ne suffit pas car l'objet Request.User est vide. Avons-nous encore besoin d'attribuer ce ClaimsPrincipal au contexte actuel dans AuthenticationHandler? C'est ainsi que nous l'avons fait dans l'ancien WebApi ...
pseabury
0

Comme indiqué à juste titre dans les articles précédents, l'un des moyens consiste à implémenter un middleware d'authentification de base personnalisé. J'ai trouvé le meilleur code de travail avec une explication dans ce blog: Auth de base avec un middleware personnalisé

J'ai fait référence au même blog mais j'ai dû faire 2 adaptations:

  1. Lors de l'ajout du middleware dans le fichier de démarrage -> Configurez la fonction, ajoutez toujours un middleware personnalisé avant d'ajouter app.UseMvc ().
  2. Lors de la lecture du nom d'utilisateur et du mot de passe du fichier appsettings.json, ajoutez une propriété statique en lecture seule dans le fichier de démarrage. Ensuite, lisez à partir de appsettings.json. Enfin, lisez les valeurs de n'importe où dans le projet. Exemple:

    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
la source
0

Vous pouvez utiliser un 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();
    }
}

et ajoutez l'attribut à votre contrôleur

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

Luca Ziegler
la source