Filtres d’authentification dans API Web ASP.NET 2
par Mike Wasson
Un filtre d’authentification est un composant qui authentifie une requête HTTP. L’API Web 2 et MVC 5 prennent en charge les filtres d’authentification, mais ils diffèrent légèrement, principalement dans les conventions d’affectation de noms de l’interface de filtre. Cette rubrique décrit les filtres d’authentification de l’API web.
Les filtres d’authentification vous permettent de définir un schéma d’authentification pour des contrôleurs ou des actions individuels. De cette façon, votre application peut prendre en charge différents mécanismes d’authentification pour différentes ressources HTTP.
Dans cet article, je vais afficher le code de l’exemple d’authentification de base sur https://github.com/aspnet/samples. L’exemple montre un filtre d’authentification qui implémente le schéma d’authentification d’accès de base HTTP (RFC 2617). Le filtre est implémenté dans une classe nommée IdentityBasicAuthenticationAttribute
. Je n’affiche pas tout le code de l’exemple, mais seulement les parties qui illustrent comment écrire un filtre d’authentification.
Définition d’un filtre d’authentification
Comme d’autres filtres, les filtres d’authentification peuvent être appliqués par contrôleur, par action ou globalement à tous les contrôleurs d’API web.
Pour appliquer un filtre d’authentification à un contrôleur, décorez la classe de contrôleur avec l’attribut filter. Le code suivant définit le [IdentityBasicAuthentication]
filtre sur une classe de contrôleur, qui active l’authentification de base pour toutes les actions du contrôleur.
[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
public IHttpActionResult Post() { . . . }
}
Pour appliquer le filtre à une action, décorez l’action avec le filtre. Le code suivant définit le [IdentityBasicAuthentication]
filtre sur la méthode du Post
contrôleur.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
[IdentityBasicAuthentication] // Enable Basic authentication for this action.
public IHttpActionResult Post() { . . . }
}
Pour appliquer le filtre à tous les contrôleurs d’API web, ajoutez-le à GlobalConfiguration.Filters.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new IdentityBasicAuthenticationAttribute());
// Other configuration code not shown...
}
}
Implémentation d’un filtre d’authentification d’API web
Dans l’API web, les filtres d’authentification implémentent l’interface System.Web.Http.Filters.IAuthenticationFilter . Ils doivent également hériter de System.Attribute, afin d’être appliqués en tant qu’attributs.
L’interface IAuthenticationFilter a deux méthodes :
- AuthenticateAsync authentifie la demande en validant les informations d’identification dans la demande, le cas échéant.
- ChallengeAsync ajoute un défi d’authentification à la réponse HTTP, si nécessaire.
Ces méthodes correspondent au flux d’authentification défini dans RFC 2612 et RFC 2617 :
- Le client envoie des informations d’identification dans l’en-tête Authorization. Cela se produit généralement après que le client a reçu une réponse 401 (non autorisée) du serveur. Toutefois, un client peut envoyer des informations d’identification avec n’importe quelle demande, pas seulement après avoir obtenu un 401.
- Si le serveur n’accepte pas les informations d’identification, il retourne une réponse 401 (Non autorisé). La réponse inclut un en-tête Www-Authenticate qui contient un ou plusieurs défis. Chaque défi spécifie un schéma d’authentification reconnu par le serveur.
Le serveur peut également retourner 401 à partir d’une requête anonyme. En fait, c’est généralement ainsi que le processus d’authentification est lancé :
- Le client envoie une demande anonyme.
- Le serveur retourne 401.
- Les clients renvoient la demande avec des informations d’identification.
Ce flux comprend à la fois les étapes d’authentification et d’autorisation .
- L’authentification prouve l’identité du client.
- L’autorisation détermine si le client peut accéder à une ressource particulière.
Dans l’API web, les filtres d’authentification gèrent l’authentification, mais pas l’autorisation. L’autorisation doit être effectuée par un filtre d’autorisation ou à l’intérieur de l’action du contrôleur.
Voici le flux dans le pipeline API Web 2 :
- Avant d’appeler une action, l’API web crée une liste des filtres d’authentification pour cette action. Cela inclut les filtres avec l’étendue d’action, l’étendue du contrôleur et l’étendue globale.
- L’API web appelle AuthenticateAsync sur chaque filtre de la liste. Chaque filtre peut valider les informations d’identification dans la demande. Si un filtre valide correctement les informations d’identification, le filtre crée un IPrincipal et l’attache à la requête. Un filtre peut également déclencher une erreur à ce stade. Si c’est le cas, le reste du pipeline ne s’exécute pas.
- En supposant qu’il n’y a pas d’erreur, la requête transite par le reste du pipeline.
- Enfin, l’API web appelle la méthode ChallengeAsync de chaque filtre d’authentification. Les filtres utilisent cette méthode pour ajouter un défi à la réponse, si nécessaire. En général (mais pas toujours) cela se produit en réponse à une erreur 401.
Les diagrammes suivants montrent deux cas possibles. Dans le premier, le filtre d’authentification authentifie correctement la demande, un filtre d’autorisation autorise la demande et l’action du contrôleur retourne 200 (OK).
Dans le deuxième exemple, le filtre d’authentification authentifie la demande, mais le filtre d’autorisation retourne 401 (non autorisé). Dans ce cas, l’action du contrôleur n’est pas appelée. Le filtre d’authentification ajoute un en-tête Www-Authenticate à la réponse.
D’autres combinaisons sont possibles: par exemple, si l’action du contrôleur autorise les demandes anonymes, vous pouvez avoir un filtre d’authentification, mais aucune autorisation.
Implémentation de la méthode AuthenticateAsync
La méthode AuthenticateAsync tente d’authentifier la demande. Voici la signature de méthode :
Task AuthenticateAsync(
HttpAuthenticationContext context,
CancellationToken cancellationToken
)
La méthode AuthenticateAsync doit effectuer l’une des opérations suivantes :
- Rien (no-op).
- Créez un IPrincipal et définissez-le sur la demande.
- Définissez un résultat d’erreur.
L’option (1) signifie que la demande n’avait pas d’informations d’identification comprises par le filtre. L’option (2) signifie que le filtre a correctement authentifié la demande. L’option (3) signifie que la demande avait des informations d’identification non valides (comme un mot de passe incorrect), ce qui déclenche une réponse d’erreur.
Voici un plan général pour l’implémentation d’AuthenticateAsync.
- Recherchez les informations d’identification dans la demande.
- S’il n’y a pas d’informations d’identification, ne faites rien et retournez (sans opération).
- S’il existe des informations d’identification mais que le filtre ne reconnaît pas le schéma d’authentification, ne faites rien et retournez (sans opération). Un autre filtre dans le pipeline peut comprendre le schéma.
- Si le filtre comprend des informations d’identification, essayez de les authentifier.
- Si les informations d’identification sont incorrectes, retournez 401 en définissant
context.ErrorResult
. - Si les informations d’identification sont valides, créez un IPrincipal et définissez
context.Principal
.
Le code suivant montre la méthode AuthenticateAsync de l’exemple d’authentification de base . Les commentaires indiquent chaque étape. Le code affiche plusieurs types d’erreur : un en-tête d’autorisation sans informations d’identification, des informations d’identification incorrectes et un nom d’utilisateur/mot de passe incorrect.
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// 1. Look for credentials in the request.
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// 2. If there are no credentials, do nothing.
if (authorization == null)
{
return;
}
// 3. If there are credentials but the filter does not recognize the
// authentication scheme, do nothing.
if (authorization.Scheme != "Basic")
{
return;
}
// 4. If there are credentials that the filter understands, try to validate them.
// 5. If the credentials are bad, set the error result.
if (String.IsNullOrEmpty(authorization.Parameter))
{
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return;
}
Tuple<string, string> userNameAndPassword = ExtractUserNameAndPassword(authorization.Parameter);
if (userNameAndPassword == null)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
}
string userName = userNameAndPassword.Item1;
string password = userNameAndPassword.Item2;
IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);
if (principal == null)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
// 6. If the credentials are valid, set principal.
else
{
context.Principal = principal;
}
}
Définition d’un résultat d’erreur
Si les informations d’identification ne sont pas valides, le filtre doit définir context.ErrorResult
sur un IHttpActionResult qui crée une réponse d’erreur. Pour plus d’informations sur IHttpActionResult, consultez Résultats de l’action dans l’API web 2.
L’exemple d’authentification de base inclut une AuthenticationFailureResult
classe qui convient à cet effet.
public class AuthenticationFailureResult : IHttpActionResult
{
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
{
ReasonPhrase = reasonPhrase;
Request = request;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}
}
Implémentation de ChallengeAsync
L’objectif de la méthode ChallengeAsync est d’ajouter des défis d’authentification à la réponse, si nécessaire. Voici la signature de méthode :
Task ChallengeAsync(
HttpAuthenticationChallengeContext context,
CancellationToken cancellationToken
)
La méthode est appelée sur chaque filtre d’authentification dans le pipeline de requête.
Il est important de comprendre que ChallengeAsync est appelé avant la création de la réponse HTTP et éventuellement avant l’exécution de l’action du contrôleur. Lorsque ChallengeAsync est appelé, context.Result
contient un IHttpActionResult, qui sera utilisé ultérieurement pour créer la réponse HTTP. Ainsi, quand ChallengeAsync est appelé, vous ne savez pas encore quoi que ce soit sur la réponse HTTP. La méthode ChallengeAsync doit remplacer la valeur d’origine de context.Result
par un nouveau IHttpActionResult. Cette IHttpActionResult doit encapsuler l’élément d’origine context.Result
.
J’appellerai le résultat internedu IHttpActionResult d’origine et le nouveau IHttpActionResult le résultat externe. Le résultat externe doit effectuer les opérations suivantes :
- Appelez le résultat interne pour créer la réponse HTTP.
- Examinez la réponse.
- Ajoutez un défi d’authentification à la réponse, si nécessaire.
L’exemple suivant est tiré de l’exemple d’authentification de base. Il définit un IHttpActionResult pour le résultat externe.
public class AddChallengeOnUnauthorizedResult : IHttpActionResult
{
public AddChallengeOnUnauthorizedResult(AuthenticationHeaderValue challenge, IHttpActionResult innerResult)
{
Challenge = challenge;
InnerResult = innerResult;
}
public AuthenticationHeaderValue Challenge { get; private set; }
public IHttpActionResult InnerResult { get; private set; }
public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response = await InnerResult.ExecuteAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
// Only add one challenge per authentication scheme.
if (!response.Headers.WwwAuthenticate.Any((h) => h.Scheme == Challenge.Scheme))
{
response.Headers.WwwAuthenticate.Add(Challenge);
}
}
return response;
}
}
La InnerResult
propriété contient le IHttpActionResult interne. La Challenge
propriété représente un en-tête Www-Authentication. Notez qu’ExecuteAsync appelle InnerResult.ExecuteAsync
d’abord pour créer la réponse HTTP, puis ajoute le défi si nécessaire.
Vérifiez le code de réponse avant d’ajouter le défi. La plupart des schémas d’authentification n’ajoutent un défi que si la réponse est 401, comme illustré ici. Toutefois, certains schémas d’authentification ajoutent un défi à une réponse de réussite. Par exemple, consultez Négocier (RFC 4559).
Compte tenu de la AddChallengeOnUnauthorizedResult
classe, le code réel dans ChallengeAsync est simple. Il vous suffit de créer le résultat et de l’attacher à context.Result
.
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
var challenge = new AuthenticationHeaderValue("Basic");
context.Result = new AddChallengeOnUnauthorizedResult(challenge, context.Result);
return Task.FromResult(0);
}
Remarque : L’exemple d’authentification de base extrait un peu cette logique en la plaçant dans une méthode d’extension.
Combinaison de filtres d’authentification avec l’authentification Host-Level
L’authentification au niveau de l’hôte est l’authentification effectuée par l’hôte (par exemple, IIS), avant que la requête n’atteigne l’infrastructure d’API web.
Souvent, vous pouvez activer l’authentification au niveau de l’hôte pour le reste de votre application, mais la désactiver pour vos contrôleurs d’API web. Par exemple, un scénario classique consiste à activer l’authentification par formulaire au niveau de l’hôte, mais à utiliser l’authentification basée sur des jetons pour l’API web.
Pour désactiver l’authentification au niveau de l’hôte à l’intérieur du pipeline d’API web, appelez config.SuppressHostPrincipal()
votre configuration. Cela entraîne la suppression de l’API web de toute requête qui entre dans le pipeline d’API web. En fait, il « dés-authentifie » la demande.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.SuppressHostPrincipal();
// Other configuration code not shown...
}
}
Ressources supplémentaires
filtres de sécurité API Web ASP.NET (MSDN Magazine)